From eaef289544d8dd55e2c5cf66bc80da58d8b97845 Mon Sep 17 00:00:00 2001 From: Alex de Mulder Date: Fri, 11 Jul 2014 15:19:42 +0200 Subject: [PATCH 1/3] Added multiple smoothCurve methods, added hideNodesOnDrag and hideEdgesOnDrag options. Added two examples. Added inheritColor option. --- HISTORY.md | 6 + docs/network.html | 57 +- examples/network/02_random_nodes.html | 10 +- examples/network/23_hierarchical_layout.html | 1 + .../24_hierarchical_layout_userdefined.html | 35 +- examples/network/26_staticSmoothCurves.html | 75 + examples/network/27_world_cup_network.html | 10089 ++++++++++++++++ lib/network/Edge.js | 308 +- lib/network/Groups.js | 20 +- lib/network/Network.js | 73 +- lib/network/mixins/HierarchicalLayoutMixin.js | 15 +- lib/network/mixins/ManipulationMixin.js | 3 + lib/network/mixins/physics/PhysicsMixin.js | 31 +- 13 files changed, 10620 insertions(+), 103 deletions(-) create mode 100644 examples/network/26_staticSmoothCurves.html create mode 100644 examples/network/27_world_cup_network.html diff --git a/HISTORY.md b/HISTORY.md index f59ca9f8..93077a0d 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -12,6 +12,12 @@ http://visjs.org - Fixed range where the `end` of the first is equal to the `start` of the second sometimes being stacked instead of put besides each other when `item.margin=0`. +### Network (formerly named Graph) + +- Expanded smoothCurves options for improved support for large clusters. +- Added multiple types of smoothCurve drawing for greatly improved performance. +- Option for inherited edge colors from connected nodes. +- Option to disable the drawing of nodes or edges on drag. ## 2014-07-07, version 3.0.0 diff --git a/docs/network.html b/docs/network.html index e2ffb70b..eec39fef 100644 --- a/docs/network.html +++ b/docs/network.html @@ -672,6 +672,14 @@ When using a DataSet, the network is automatically updating to changes in the Da physics.[method].springLength The resting length of the edge when modeled as a spring. By default the springLength determined by the physics is used. By using this setting you can make certain edges have different resting lengths. + + + inheritColor + String | Boolean + false + Possible values: "to","from", true, false. If this value is set to false, the edge color information is used. If the value is set to true or "from", + the color data from the borders of the "from" node is used. If this value is "to", the color data from the borders of the "to" node is used. + title string | function @@ -874,7 +882,22 @@ var options = { Toggle if the nodes can be dragged. This will not affect the dragging of the network. - + + hideNodesOnDrag + Boolean + false + + Toggle if the nodes are drawn during a drag. This can greatly improve performance if you have many nodes. + + + + hideEdgesOnDrag + Boolean + false + + Toggle if the edges are drawn during a drag. This can greatly improve performance if you have many edges. + + navigation Object @@ -895,11 +918,31 @@ var options = { smoothCurves + Boolean || object + object + If true, edges are drawn as smooth curves. This is more computationally intensive since the edge now is a quadratic Bezier curve. This can be further configured by the options below. + + + + smoothCurves.dynamic Boolean true - If true, edges are drawn as smooth curves. This is more computationally intensive since the edge now is a quadratic Bezier curve with control points on both nodes and an invisible node in the center of the edge. This support node is also handed by the physics simulation. + By default, the edges are dynamic. This means there are support nodes placed in the middle of the edge. This support node is also handed by the physics simulation. If false, the smoothness will be based on the + relative positions of the to and from nodes. This is computationally cheaper but there is no self organisation. + + + smoothCurves.adaptive + Boolean + true + This option only affects NONdynamic smooth curves. Adaptivity allows the smoothness to transition nicely if there is node movement. The downside is that the maximum roundness is less if adaptivity is enabled. Adaptivity can be disabled for static network. + Sudden switching between orientations can happen on an animated network. + + + smoothCurves.roundness + Number + 0.5 + This only affects NONdynamic smooth curves. The roundness can be tweaked with the parameter. The value range is from 0 to 1 with a maximum roundness at 0.5. - selectable Boolean @@ -1223,6 +1266,14 @@ var options = { The resting length of the edge when modeled as a spring. By default the springLength determined by the physics is used. By using this setting you can make certain edges have different resting lengths. + + inheritColor + String | Boolean + false + Possible values: "to","from", true, false. If this value is set to false, the edge color information is used. If the value is set to true or "from", + the color data from the borders of the "from" node is used. If this value is "to", the color data from the borders of the "to" node is used. + + style String diff --git a/examples/network/02_random_nodes.html b/examples/network/02_random_nodes.html index 9a8b1c00..4681ddb8 100644 --- a/examples/network/02_random_nodes.html +++ b/examples/network/02_random_nodes.html @@ -81,12 +81,12 @@ }, stabilize: false }; - network = new vis.Network(container, data, options); + network = new vis.Network(container, data, options); - // add event listeners - network.on('select', function(params) { - document.getElementById('selection').innerHTML = 'Selection: ' + params.nodes; - }); + // add event listeners + network.on('select', function(params) { + document.getElementById('selection').innerHTML = 'Selection: ' + params.nodes; + }); } diff --git a/examples/network/23_hierarchical_layout.html b/examples/network/23_hierarchical_layout.html index b4724ff8..bdde2b54 100644 --- a/examples/network/23_hierarchical_layout.html +++ b/examples/network/23_hierarchical_layout.html @@ -82,6 +82,7 @@ edges: { }, stabilize: false, + smoothCurves: false, hierarchicalLayout: { direction: directionInput.value } diff --git a/examples/network/24_hierarchical_layout_userdefined.html b/examples/network/24_hierarchical_layout_userdefined.html index a8df871e..50981a16 100644 --- a/examples/network/24_hierarchical_layout_userdefined.html +++ b/examples/network/24_hierarchical_layout_userdefined.html @@ -20,6 +20,7 @@ var nodes = null; var edges = null; var network = null; + var directionInput = document.getElementById("direction"); function draw() { nodes = []; @@ -113,7 +114,9 @@ }; var options = { - hierarchicalLayout:true + hierarchicalLayout: { + direction: directionInput.value + } }; network = new vis.Network(container, data, options); @@ -122,6 +125,7 @@ document.getElementById('selection').innerHTML = 'Selection: ' + params.nodes; }); } + @@ -129,11 +133,40 @@

Hierarchical Layout - User-defined

This example shows a user-defined hierarchical layout. If the user defines levels for nodes but does not do so for all nodes, an alert will show up and hierarchical layout will be disabled. Either all or none can be defined. + If the smooth curves appear to be inverted, the direction of the edge is not in the same direction as the network.
+ + + + +

+ diff --git a/examples/network/26_staticSmoothCurves.html b/examples/network/26_staticSmoothCurves.html new file mode 100644 index 00000000..c12606af --- /dev/null +++ b/examples/network/26_staticSmoothCurves.html @@ -0,0 +1,75 @@ + + + + Network | Static smooth curves + + + + + + + + +

Static smooth curves

+
+ All the smooth curves in the examples so far have been using dynamic smooth curves. This means that each curve has a + support node which takes part in the physics simulation. For large networks or dense clusters, this may not be the ideal + solution. To solve this, static smooth curves have been added. The static smooth curves are based only on the positions of the connected + nodes. There are multiple ways to determine the way this curve is drawn. This example shows the effect of the different + types.

+ Drag the nodes around each other to see how the smooth curves are drawn for each setting. For animated system, we + recommend only the continuous mode. In the next example you can see the effect of these methods on a large network. Keep in mind + that the direction (the from and to) of the curve matters. +

+
+ +Smooth curve type: + +
+ + + + + diff --git a/examples/network/27_world_cup_network.html b/examples/network/27_world_cup_network.html new file mode 100644 index 00000000..3b305871 --- /dev/null +++ b/examples/network/27_world_cup_network.html @@ -0,0 +1,10089 @@ + + + + Network | Static smooth curves - World Cup Network + + + + + + + + + +

Static smooth curves - World Cup Network

+
+ The static smooth curves are based only on the positions of the connected nodes. + There are multiple ways to determine the way this curve is drawn. + This example shows the effect of the different types on a large network. +

+ Also shown in this example is the inheritColor option of the edges as well as the roundness factor.
+ The roundness factor is only functional with "continuous", "discrete" and "diagonalCross". +

+ To improve performance, the physics have been disabled with: +
{barnesHut: {gravitationalConstant: 0, centralGravity: 0, springConstant: 0}}
and we have enabled + the toggle hideEdgesOnDrag. +

+
+ +Smooth curve type: +
+Roundness (0..1): +
+Hide edges on drag: +Hide nodes on drag: + +
+ + + + + + diff --git a/lib/network/Edge.js b/lib/network/Edge.js index c5fdf072..fa723608 100644 --- a/lib/network/Edge.js +++ b/lib/network/Edge.js @@ -41,8 +41,10 @@ function Edge (properties, network, constants) { this.customLength = false; this.selected = false; this.hover = false; - this.smooth = constants.smoothCurves; + this.smoothCurves = constants.smoothCurves; + this.dynamicSmoothCurves = constants.dynamicSmoothCurves; this.arrowScaleFactor = constants.edges.arrowScaleFactor; + this.inheritColor = constants.edges.inheritColor; this.from = null; // a node this.to = null; // a node @@ -114,6 +116,8 @@ Edge.prototype.setProperties = function(properties, constants) { // scale the arrow if (properties.arrowScaleFactor !== undefined) {this.arrowScaleFactor = properties.arrowScaleFactor;} + if (properties.inheritColor !== undefined) {this.inheritColor = properties.inheritColor;} + // Added to support dashed lines // David Jordan // 2012-08-08 @@ -258,6 +262,28 @@ Edge.prototype.isOverlappingWith = function(obj) { } }; +Edge.prototype._getColor = function() { + var colorObj = this.color; + if (this.inheritColor == "to") { + colorObj = { + highlight: this.to.color.highlight.border, + hover: this.to.color.hover.border, + color: this.to.color.border + }; + } + else if (this.inheritColor == "from" || this.inheritColor == true) { + colorObj = { + highlight: this.from.color.highlight.border, + hover: this.from.color.hover.border, + color: this.from.color.border + }; + } + + if (this.selected == true) {return colorObj.highlight;} + else if (this.hover == true) {return colorObj.hover;} + else {return colorObj.color;} +} + /** * Redraw a edge as a line @@ -268,21 +294,19 @@ Edge.prototype.isOverlappingWith = function(obj) { */ Edge.prototype._drawLine = function(ctx) { // set style - if (this.selected == true) {ctx.strokeStyle = this.color.highlight;} - else if (this.hover == true) {ctx.strokeStyle = this.color.hover;} - else {ctx.strokeStyle = this.color.color;} - ctx.lineWidth = this._getLineWidth(); + ctx.strokeStyle = this._getColor(); + ctx.lineWidth = this._getLineWidth(); if (this.from != this.to) { // draw line - this._line(ctx); + var via = this._line(ctx); // draw label var point; if (this.label) { - if (this.smooth == true) { - var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x)); - var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y)); + if (this.smoothCurves.enabled == true && via != null) { + var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x)); + var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y)); point = {x:midpointX, y:midpointY}; } else { @@ -332,6 +356,154 @@ Edge.prototype._getLineWidth = function() { } }; +Edge.prototype._getViaCoordinates = function () { + var xVia = null; + var yVia = null; + var factor = this.smoothCurves.roundness; + var type = this.smoothCurves.type; + if (factor == 0) { + return {x:null,y:null}; + } + + var dx = Math.abs(this.from.x - this.to.x); + var dy = Math.abs(this.from.y - this.to.y); + if (type == 'discrete' || type == 'diagonalCross') { + if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { + if (this.from.y > this.to.y) { + if (this.from.x < this.to.x) { + xVia = this.from.x + factor * dy; + yVia = this.from.y - factor * dy; + } + else if (this.from.x > this.to.x) { + xVia = this.from.x - factor * dy; + yVia = this.from.y - factor * dy; + } + } + else if (this.from.y < this.to.y) { + if (this.from.x < this.to.x) { + xVia = this.from.x + factor * dy; + yVia = this.from.y + factor * dy; + } + else if (this.from.x > this.to.x) { + xVia = this.from.x - factor * dy; + yVia = this.from.y + factor * dy; + } + } + if (type == "discrete") { + xVia = dx < factor * dy ? this.from.x : xVia; + } + } + else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { + if (this.from.y > this.to.y) { + if (this.from.x < this.to.x) { + xVia = this.from.x + factor * dx; + yVia = this.from.y - factor * dx; + } + else if (this.from.x > this.to.x) { + xVia = this.from.x - factor * dx; + yVia = this.from.y - factor * dx; + } + } + else if (this.from.y < this.to.y) { + if (this.from.x < this.to.x) { + xVia = this.from.x + factor * dx; + yVia = this.from.y + factor * dx; + } + else if (this.from.x > this.to.x) { + xVia = this.from.x - factor * dx; + yVia = this.from.y + factor * dx; + } + } + if (type == "discrete") { + yVia = dy < factor * dx ? this.from.y : yVia; + } + } + } + else if (type == "straightCross") { + if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { + xVia = this.from.x; + yVia = this.to.y + } + else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { + xVia = this.to.x; + yVia = this.from.y; + } + } + else if (type == 'horizontal') { + xVia = this.to.x; + yVia = this.from.y; + } + else if (type == 'vertical') { + xVia = this.from.x; + yVia = this.to.y; + } + else { // continuous + if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { + if (this.from.y > this.to.y) { + if (this.from.x < this.to.x) { +// console.log(1) + xVia = this.from.x + factor * dy; + yVia = this.from.y - factor * dy; + xVia = this.to.x < xVia ? this.to.x : xVia; + } + else if (this.from.x > this.to.x) { +// console.log(2) + xVia = this.from.x - factor * dy; + yVia = this.from.y - factor * dy; + xVia = this.to.x > xVia ? this.to.x :xVia; + } + } + else if (this.from.y < this.to.y) { + if (this.from.x < this.to.x) { +// console.log(3) + xVia = this.from.x + factor * dy; + yVia = this.from.y + factor * dy; + xVia = this.to.x < xVia ? this.to.x : xVia; + } + else if (this.from.x > this.to.x) { +// console.log(4, this.from.x, this.to.x) + xVia = this.from.x - factor * dy; + yVia = this.from.y + factor * dy; + xVia = this.to.x > xVia ? this.to.x : xVia; + } + } + } + else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { + if (this.from.y > this.to.y) { + if (this.from.x < this.to.x) { +// console.log(5) + xVia = this.from.x + factor * dx; + yVia = this.from.y - factor * dx; + yVia = this.to.y > yVia ? this.to.y : yVia; + } + else if (this.from.x > this.to.x) { +// console.log(6) + xVia = this.from.x - factor * dx; + yVia = this.from.y - factor * dx; + yVia = this.to.y > yVia ? this.to.y : yVia; + } + } + else if (this.from.y < this.to.y) { + if (this.from.x < this.to.x) { +// console.log(7) + xVia = this.from.x + factor * dx; + yVia = this.from.y + factor * dx; + yVia = this.to.y < yVia ? this.to.y : yVia; + } + else if (this.from.x > this.to.x) { +// console.log(8) + xVia = this.from.x - factor * dx; + yVia = this.from.y + factor * dx; + yVia = this.to.y < yVia ? this.to.y : yVia; + } + } + } + } + + + return {x:xVia, y:yVia}; +} + /** * Draw a line between two nodes * @param {CanvasRenderingContext2D} ctx @@ -341,13 +513,33 @@ Edge.prototype._line = function (ctx) { // draw a straight line ctx.beginPath(); ctx.moveTo(this.from.x, this.from.y); - if (this.smooth == true) { + if (this.smoothCurves.enabled == true) { + if (this.smoothCurves.dynamic == false) { + var via = this._getViaCoordinates(); + if (via.x == null) { + ctx.lineTo(this.to.x, this.to.y); + ctx.stroke(); + return null; + } + else { +// this.via.x = via.x; +// this.via.y = via.y; + ctx.quadraticCurveTo(via.x,via.y,this.to.x, this.to.y); + ctx.stroke(); + return via; + } + } + else { ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y); + ctx.stroke(); + return this.via; + } } else { ctx.lineTo(this.to.x, this.to.y); + ctx.stroke(); + return null; } - ctx.stroke(); }; /** @@ -411,11 +603,9 @@ Edge.prototype._drawDashLine = function(ctx) { ctx.lineWidth = this._getLineWidth(); + var via = null; // only firefox and chrome support this method, else we use the legacy one. if (ctx.mozDash !== undefined || ctx.setLineDash !== undefined) { - ctx.beginPath(); - ctx.moveTo(this.from.x, this.from.y); - // configure the dash pattern var pattern = [0]; if (this.dash.length !== undefined && this.dash.gap !== undefined) { @@ -436,13 +626,7 @@ Edge.prototype._drawDashLine = function(ctx) { } // draw the line - if (this.smooth == true) { - ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y); - } - else { - ctx.lineTo(this.to.x, this.to.y); - } - ctx.stroke(); + via = this._line(ctx); // restore the dash settings. if (typeof ctx.setLineDash !== 'undefined') { //Chrome @@ -479,9 +663,9 @@ Edge.prototype._drawDashLine = function(ctx) { // draw label if (this.label) { var point; - if (this.smooth == true) { - var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x)); - var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y)); + if (this.smoothCurves.enabled == true && via != null) { + var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x)); + var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y)); point = {x:midpointX, y:midpointY}; } else { @@ -538,14 +722,14 @@ Edge.prototype._drawArrowCenter = function(ctx) { if (this.from != this.to) { // draw line - this._line(ctx); + var via = this._line(ctx); var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x)); var length = (10 + 5 * this.width) * this.arrowScaleFactor; // draw an arrow halfway the line - if (this.smooth == true) { - var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x)); - var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y)); + if (this.smoothCurves.enabled == true && via != null) { + var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x)); + var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y)); point = {x:midpointX, y:midpointY}; } else { @@ -625,20 +809,27 @@ Edge.prototype._drawArrow = function(ctx) { var xFrom = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x; var yFrom = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y; + var via; + if (this.smoothCurves.dynamic == true && this.smoothCurves.enabled == true ) { + via = this.via; + } + else if (this.smoothCurves.enabled == true) { + via = this._getViaCoordinates(); + } - if (this.smooth == true) { - angle = Math.atan2((this.to.y - this.via.y), (this.to.x - this.via.x)); - dx = (this.to.x - this.via.x); - dy = (this.to.y - this.via.y); + if (this.smoothCurves.enabled == true && via.x != null) { + angle = Math.atan2((this.to.y - via.y), (this.to.x - via.x)); + dx = (this.to.x - via.x); + dy = (this.to.y - via.y); edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); } var toBorderDist = this.to.distanceToBorder(ctx, angle); var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength; var xTo,yTo; - if (this.smooth == true) { - xTo = (1 - toBorderPoint) * this.via.x + toBorderPoint * this.to.x; - yTo = (1 - toBorderPoint) * this.via.y + toBorderPoint * this.to.y; + if (this.smoothCurves.enabled == true && via.x != null) { + xTo = (1 - toBorderPoint) * via.x + toBorderPoint * this.to.x; + yTo = (1 - toBorderPoint) * via.y + toBorderPoint * this.to.y; } else { xTo = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x; @@ -647,8 +838,8 @@ Edge.prototype._drawArrow = function(ctx) { ctx.beginPath(); ctx.moveTo(xFrom,yFrom); - if (this.smooth == true) { - ctx.quadraticCurveTo(this.via.x,this.via.y,xTo, yTo); + if (this.smoothCurves.enabled == true && via.x != null) { + ctx.quadraticCurveTo(via.x,via.y,xTo, yTo); } else { ctx.lineTo(xTo, yTo); @@ -664,9 +855,9 @@ Edge.prototype._drawArrow = function(ctx) { // draw label if (this.label) { var point; - if (this.smooth == true) { - var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x)); - var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y)); + if (this.smoothCurves.enabled == true && via != null) { + var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x)); + var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y)); point = {x:midpointX, y:midpointY}; } else { @@ -736,13 +927,23 @@ Edge.prototype._drawArrow = function(ctx) { */ Edge.prototype._getDistanceToEdge = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point if (this.from != this.to) { - if (this.smooth == true) { + if (this.smoothCurves.enabled == true) { + var xVia, yVia; + if (this.smoothCurves.enabled == true && this.smoothCurves.dynamic == true) { + xVia = this.via.x; + yVia = this.via.y; + } + else { + var via = this._getViaCoordinates(); + xVia = via.x; + yVia = via.y; + } var minDistance = 1e9; var i,t,x,y,dx,dy; for (i = 0; i < 10; i++) { t = 0.1*i; - x = Math.pow(1-t,2)*x1 + (2*t*(1 - t))*this.via.x + Math.pow(t,2)*x2; - y = Math.pow(1-t,2)*y1 + (2*t*(1 - t))*this.via.y + Math.pow(t,2)*y2; + x = Math.pow(1-t,2)*x1 + (2*t*(1 - t))*xVia + Math.pow(t,2)*x2; + y = Math.pow(1-t,2)*y1 + (2*t*(1 - t))*yVia + Math.pow(t,2)*y2; dx = Math.abs(x3-x); dy = Math.abs(y3-y); minDistance = Math.min(minDistance,Math.sqrt(dx*dx + dy*dy)); @@ -943,20 +1144,27 @@ Edge.prototype.getControlNodePositions = function(ctx) { var xFrom = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x; var yFrom = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y; + var via; + if (this.smoothCurves.dynamic == true && this.smoothCurves.enabled == true) { + via = this.via; + } + else if (this.smoothCurves.enabled == true) { + via = this._getViaCoordinates(); + } - if (this.smooth == true) { - angle = Math.atan2((this.to.y - this.via.y), (this.to.x - this.via.x)); - dx = (this.to.x - this.via.x); - dy = (this.to.y - this.via.y); + if (this.smoothCurves.enabled == true && via.x != null) { + angle = Math.atan2((this.to.y - via.y), (this.to.x - via.x)); + dx = (this.to.x - via.x); + dy = (this.to.y - via.y); edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); } var toBorderDist = this.to.distanceToBorder(ctx, angle); var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength; var xTo,yTo; - if (this.smooth == true) { - xTo = (1 - toBorderPoint) * this.via.x + toBorderPoint * this.to.x; - yTo = (1 - toBorderPoint) * this.via.y + toBorderPoint * this.to.y; + if (this.smoothCurves.enabled == true && via.x != null) { + xTo = (1 - toBorderPoint) * via.x + toBorderPoint * this.to.x; + yTo = (1 - toBorderPoint) * via.y + toBorderPoint * this.to.y; } else { xTo = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x; diff --git a/lib/network/Groups.js b/lib/network/Groups.js index 90e21d3c..84bb4185 100644 --- a/lib/network/Groups.js +++ b/lib/network/Groups.js @@ -14,16 +14,16 @@ function Groups() { * default constants for group colors */ Groups.DEFAULT = [ - {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue - {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}}, // yellow - {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}}, // red - {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}}, // green - {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}}, // magenta - {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}}, // purple - {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}}, // orange - {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue - {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}}, // pink - {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}} // mint + {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}, hover: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue + {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}, hover: {border: "#FFA500", background: "#FFFFA3"}}, // yellow + {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}, hover: {border: "#FA0A10", background: "#FFAFB1"}}, // red + {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}, hover: {border: "#41A906", background: "#A1EC76"}}, // green + {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}, hover: {border: "#E129F0", background: "#F0B3F5"}}, // magenta + {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}, hover: {border: "#7C29F0", background: "#D3BDF0"}}, // purple + {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}, hover: {border: "#C37F00", background: "#FFCA66"}}, // orange + {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}, hover: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue + {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}, hover: {border: "#FD5A77", background: "#FFD1D9"}}, // pink + {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}, hover: {border: "#4AD63A", background: "#E6FFE3"}} // mint ]; diff --git a/lib/network/Network.js b/lib/network/Network.js index f61ed05e..1e0283ba 100644 --- a/lib/network/Network.js +++ b/lib/network/Network.js @@ -56,9 +56,9 @@ function Network (container, data, options) { // set constant values this.constants = { nodes: { - radiusMin: 5, - radiusMax: 20, - radius: 5, + radiusMin: 10, + radiusMax: 30, + radius: 10, shape: 'ellipse', image: undefined, widthMin: 16, // px @@ -106,7 +106,8 @@ function Network (container, data, options) { length: 10, gap: 5, altLength: undefined - } + }, + inheritColor: false // to, from, false, true (== from) }, configurePhysics:false, physics: { @@ -178,7 +179,13 @@ function Network (container, data, options) { direction: "UD" // UD, DU, LR, RL }, freezeForStabilization: false, - smoothCurves: true, + smoothCurves: { + enabled: true, + dynamic: true, + type: "continuous", + roundness: 0.5 + }, + dynamicSmoothCurves: true, maxVelocity: 10, minVelocity: 0.1, // px/s stabilizationIterations: 1000, // maximum number of iteration to stabilize @@ -213,10 +220,12 @@ function Network (container, data, options) { dragNetwork: true, dragNodes: true, zoomable: true, - hover: false + hover: false, + hideEdgesOnDrag: false, + hideNodesOnDrag: false }; this.hoverObj = {nodes:{},edges:{}}; - + this.controlNodesActive = false; // Node variables var network = this; @@ -549,7 +558,6 @@ Network.prototype.setOptions = function (options) { if (options.height !== undefined) {this.height = options.height;} if (options.stabilize !== undefined) {this.stabilize = options.stabilize;} if (options.selectable !== undefined) {this.selectable = options.selectable;} - if (options.smoothCurves !== undefined) {this.constants.smoothCurves = options.smoothCurves;} if (options.freezeForStabilization !== undefined) {this.constants.freezeForStabilization = options.freezeForStabilization;} if (options.configurePhysics !== undefined){this.constants.configurePhysics = options.configurePhysics;} if (options.stabilizationIterations !== undefined) {this.constants.stabilizationIterations = options.stabilizationIterations;} @@ -557,6 +565,8 @@ Network.prototype.setOptions = function (options) { if (options.dragNodes !== undefined) {this.constants.dragNodes = options.dragNodes;} if (options.zoomable !== undefined) {this.constants.zoomable = options.zoomable;} if (options.hover !== undefined) {this.constants.hover = options.hover;} + if (options.hideEdgesOnDrag !== undefined) {this.constants.hideEdgesOnDrag = options.hideEdgesOnDrag;} + if (options.hideNodesOnDrag !== undefined) {this.constants.hideNodesOnDrag = options.hideNodesOnDrag;} // TODO: deprecated since version 3.0.0. Cleanup some day if (options.dragGraph !== undefined) { @@ -622,6 +632,20 @@ Network.prototype.setOptions = function (options) { } } + if (options.smoothCurves !== undefined) { + if (typeof options.smoothCurves == 'boolean') { + this.constants.smoothCurves.enabled = options.smoothCurves; + } + else { + this.constants.smoothCurves.enabled = true; + for (prop in options.smoothCurves) { + if (options.smoothCurves.hasOwnProperty(prop)) { + this.constants.smoothCurves[prop] = options.smoothCurves[prop]; + } + } + } + } + if (options.hierarchicalLayout) { this.constants.hierarchicalLayout.enabled = true; for (prop in options.hierarchicalLayout) { @@ -693,7 +717,6 @@ Network.prototype.setOptions = function (options) { } } - if (options.edges.color !== undefined) { if (util.isString(options.edges.color)) { this.constants.edges.color = {}; @@ -1023,10 +1046,11 @@ Network.prototype._handleOnDrag = function(event) { this._setTranslation( this.drag.translation.x + diffX, - this.drag.translation.y + diffY); + this.drag.translation.y + diffY + ); this._redraw(); - this.moving = true; - this.start(); +// this.moving = true; +// this.start(); } } }; @@ -1045,6 +1069,7 @@ Network.prototype._onDragEnd = function () { s.node.yFixed = s.yFixed; }); } + this._redraw(); }; /** @@ -1740,10 +1765,19 @@ Network.prototype._redraw = function() { "y": this._YconvertDOMtoCanvas(this.frame.canvas.clientHeight) }; + this._doInAllSectors("_drawAllSectorNodes",ctx); - this._doInAllSectors("_drawEdges",ctx); - this._doInAllSectors("_drawNodes",ctx,false); - this._doInAllSectors("_drawControlNodes",ctx); + if (this.drag.dragging == false || this.drag.dragging === undefined || this.constants.hideEdgesOnDrag == false) { + this._doInAllSectors("_drawEdges",ctx); + } + + if (this.drag.dragging == false || this.drag.dragging === undefined || this.constants.hideNodesOnDrag == false) { + this._doInAllSectors("_drawNodes",ctx,false); + } + + if (this.controlNodesActive == true) { + this._doInAllSectors("_drawControlNodes",ctx); + } // this._doInSupportSector("_drawNodes",ctx,true); // this._drawTree(ctx,"#F00F0F"); @@ -2055,7 +2089,7 @@ Network.prototype._discreteStepNodes = function() { this.moving = true; } else { - this.moving = this._isMoving(vminCorrected); + this.moving = this._isMoving(vminCorrected) || this.constants.configurePhysics; } } }; @@ -2104,10 +2138,12 @@ Network.prototype._animationStep = function() { timeRequired = Date.now() - calculationTime; maxSteps++; } + // start the rendering process var renderTime = Date.now(); this._redraw(); this.renderTime = Date.now() - renderTime; + }; if (typeof window !== 'undefined') { @@ -2191,8 +2227,7 @@ Network.prototype._configureSmoothCurves = function(disableStart) { if (disableStart === undefined) { disableStart = true; } - - if (this.constants.smoothCurves == true) { + if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { this._createBezierNodes(); } else { @@ -2220,7 +2255,7 @@ Network.prototype._configureSmoothCurves = function(disableStart) { * @private */ Network.prototype._createBezierNodes = function() { - if (this.constants.smoothCurves == true) { + if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { for (var edgeId in this.edges) { if (this.edges.hasOwnProperty(edgeId)) { var edge = this.edges[edgeId]; diff --git a/lib/network/mixins/HierarchicalLayoutMixin.js b/lib/network/mixins/HierarchicalLayoutMixin.js index 4be8aab5..78f75373 100644 --- a/lib/network/mixins/HierarchicalLayoutMixin.js +++ b/lib/network/mixins/HierarchicalLayoutMixin.js @@ -23,6 +23,17 @@ exports._setupHierarchicalLayout = function() { else { this.constants.hierarchicalLayout.levelSeparation = Math.abs(this.constants.hierarchicalLayout.levelSeparation); } + + if (this.constants.hierarchicalLayout.direction == "RL" || this.constants.hierarchicalLayout.direction == "LR") { + if (this.constants.smoothCurves.enabled == true) { + this.constants.smoothCurves.type = "vertical"; + } + } + else { + if (this.constants.smoothCurves.enabled == true) { + this.constants.smoothCurves.type = "horizontal"; + } + } // get the size of the largest hubs and check if the user has defined a level for a node. var hubsize = 0; var node, nodeId; @@ -210,7 +221,9 @@ exports._changeConstants = function() { this.constants.physics.barnesHut.enabled = false; this.constants.physics.hierarchicalRepulsion.enabled = true; this._loadSelectedForceSolver(); - this.constants.smoothCurves = false; + if (this.constants.smoothCurves.enabled == true) { + this.constants.smoothCurves.dynamic = false; + } this._configureSmoothCurves(); }; diff --git a/lib/network/mixins/ManipulationMixin.js b/lib/network/mixins/ManipulationMixin.js index 2413ec3f..be34011a 100644 --- a/lib/network/mixins/ManipulationMixin.js +++ b/lib/network/mixins/ManipulationMixin.js @@ -63,10 +63,12 @@ exports._createManipulatorBar = function() { if (this.boundFunction) { this.off('select', this.boundFunction); } + if (this.edgeBeingEdited !== undefined) { this.edgeBeingEdited._disableControlNodes(); this.edgeBeingEdited = undefined; this.selectedControlNode = null; + this.controlNodesActive = false; } // restore overloaded functions @@ -226,6 +228,7 @@ exports._createAddEdgeToolbar = function() { exports._createEditEdgeToolbar = function() { // clear the toolbar this._clearManipulatorBar(); + this.controlNodesActive = true; if (this.boundFunction) { this.off('select', this.boundFunction); diff --git a/lib/network/mixins/physics/PhysicsMixin.js b/lib/network/mixins/physics/PhysicsMixin.js index 1ab07620..18c4de5e 100644 --- a/lib/network/mixins/physics/PhysicsMixin.js +++ b/lib/network/mixins/physics/PhysicsMixin.js @@ -95,15 +95,17 @@ exports._calculateForces = function () { this._calculateGravitationalForces(); this._calculateNodeForces(); - if (this.constants.smoothCurves == true) { - this._calculateSpringForcesWithSupport(); - } - else { - if (this.constants.physics.hierarchicalRepulsion.enabled == true) { - this._calculateHierarchicalSpringForces(); + if (this.constants.springConstant > 0) { + if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { + this._calculateSpringForcesWithSupport(); } else { - this._calculateSpringForces(); + if (this.constants.physics.hierarchicalRepulsion.enabled == true) { + this._calculateHierarchicalSpringForces(); + } + else { + this._calculateSpringForces(); + } } } }; @@ -118,7 +120,7 @@ exports._calculateForces = function () { * @private */ exports._updateCalculationNodes = function () { - if (this.constants.smoothCurves == true) { + if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { this.calculationNodes = {}; this.calculationNodeIndices = []; @@ -457,7 +459,7 @@ exports._loadPhysicsConfiguration = function () { graph_toggleSmooth.onclick = graphToggleSmoothCurves.bind(this); graph_repositionNodes.onclick = graphRepositionNodes.bind(this); graph_generateOptions.onclick = graphGenerateOptions.bind(this); - if (this.constants.smoothCurves == true) { + if (this.constants.smoothCurves == true && this.constants.dynamicSmoothCurves == false) { graph_toggleSmooth.style.background = "#A4FF56"; } else { @@ -498,9 +500,9 @@ exports._overWriteGraphConstants = function (constantsVariableName, value) { * this function is bound to the toggle smooth curves button. That is also why it is not in the prototype. */ function graphToggleSmoothCurves () { - this.constants.smoothCurves = !this.constants.smoothCurves; + this.constants.smoothCurves.enabled = !this.constants.smoothCurves.enabled; var graph_toggleSmooth = document.getElementById("graph_toggleSmooth"); - if (this.constants.smoothCurves == true) {graph_toggleSmooth.style.background = "#A4FF56";} + if (this.constants.smoothCurves.enabled == true) {graph_toggleSmooth.style.background = "#A4FF56";} else {graph_toggleSmooth.style.background = "#FF8532";} this._configureSmoothCurves(false); @@ -552,10 +554,10 @@ function graphGenerateOptions () { } options += '}}' } - if (this.constants.smoothCurves != this.backupConstants.smoothCurves) { + if (this.constants.smoothCurves.enabled != this.backupConstants.smoothCurves.enabled) { if (optionsSpecific.length == 0) {options = "var options = {";} else {options += ", "} - options += "smoothCurves: " + this.constants.smoothCurves; + options += "smoothCurves: " + this.constants.smoothCurves.enabled; } if (options != "No options are required, default values used.") { options += '};' @@ -653,6 +655,7 @@ function switchConfigurations () { this.constants.hierarchicalLayout.enabled = true; this.constants.physics.hierarchicalRepulsion.enabled = true; this.constants.physics.barnesHut.enabled = false; + this.constants.smoothCurves.enabled = false; this._setupHierarchicalLayout(); } } @@ -663,7 +666,7 @@ function switchConfigurations () { } this._loadSelectedForceSolver(); var graph_toggleSmooth = document.getElementById("graph_toggleSmooth"); - if (this.constants.smoothCurves == true) {graph_toggleSmooth.style.background = "#A4FF56";} + if (this.constants.smoothCurves.enabled == true) {graph_toggleSmooth.style.background = "#A4FF56";} else {graph_toggleSmooth.style.background = "#FF8532";} this.moving = true; this.start(); From f7b2d67ffcb0cd0bf826683a489fa749d530875a Mon Sep 17 00:00:00 2001 From: Alex de Mulder Date: Fri, 11 Jul 2014 15:20:29 +0200 Subject: [PATCH 2/3] updated example index --- dist/vis.js | 20329 +++++++++++++++++----------------- examples/network/index.html | 2 + 2 files changed, 10297 insertions(+), 10034 deletions(-) diff --git a/dist/vis.js b/dist/vis.js index 01389cc3..40366c50 100644 --- a/dist/vis.js +++ b/dist/vis.js @@ -5,7 +5,7 @@ * A dynamic, browser-based visualization library. * * @version 3.0.1-SNAPSHOT - * @date 2014-07-10 + * @date 2014-07-11 * * @license * Copyright (C) 2011-2014 Almende B.V, http://almende.com @@ -88,16 +88,16 @@ return /******/ (function(modules) { // webpackBootstrap exports.DataView = __webpack_require__(4); // Graph3d - exports.Graph3d = __webpack_require__(5); + exports.Graph3d = __webpack_require__(11); // Timeline - exports.Timeline = __webpack_require__(6); + exports.Timeline = __webpack_require__(5); exports.Graph2d = __webpack_require__(7); exports.timeline= { - DataStep: __webpack_require__(8), - Range: __webpack_require__(9), - stack: __webpack_require__(10), - TimeStep: __webpack_require__(11), + DataStep: __webpack_require__(6), + Range: __webpack_require__(8), + stack: __webpack_require__(9), + TimeStep: __webpack_require__(10), components: { items: { @@ -1282,7 +1282,6 @@ return /******/ (function(modules) { // webpackBootstrap */ exports.binarySearch = function(orderedItems, range, field, field2) { var array = orderedItems; - var interval = range.end - range.start; var found = false; var low = 0; @@ -2836,6007 +2835,6007 @@ return /******/ (function(modules) { // webpackBootstrap /***/ function(module, exports, __webpack_require__) { var Emitter = __webpack_require__(41); + var Hammer = __webpack_require__(50); + var util = __webpack_require__(1); var DataSet = __webpack_require__(3); var DataView = __webpack_require__(4); - var Point3d = __webpack_require__(33); - var Point2d = __webpack_require__(34); - var Filter = __webpack_require__(35); - var StepNumber = __webpack_require__(36); + var Range = __webpack_require__(8); + var TimeAxis = __webpack_require__(21); + var CurrentTime = __webpack_require__(13); + var CustomTime = __webpack_require__(14); + var ItemSet = __webpack_require__(18); /** - * @constructor Graph3d - * Graph3d displays data in 3d. - * - * Graph3d is developed in javascript as a Google Visualization Chart. - * - * @param {Element} container The DOM element in which the Graph3d will - * be created. Normally a div element. - * @param {DataSet | DataView | Array} [data] - * @param {Object} [options] + * Create a timeline visualization + * @param {HTMLElement} container + * @param {vis.DataSet | Array | google.visualization.DataTable} [items] + * @param {Object} [options] See Timeline.setOptions for the available options. + * @constructor */ - function Graph3d(container, data, options) { - if (!(this instanceof Graph3d)) { + function Timeline (container, items, options) { + if (!(this instanceof Timeline)) { throw new SyntaxError('Constructor must be called with the new operator'); } - // create variables and set default values - this.containerElement = container; - this.width = '400px'; - this.height = '400px'; - this.margin = 10; // px - this.defaultXCenter = '55%'; - this.defaultYCenter = '50%'; + var me = this; + this.defaultOptions = { + start: null, + end: null, - this.xLabel = 'x'; - this.yLabel = 'y'; - this.zLabel = 'z'; - this.filterLabel = 'time'; - this.legendLabel = 'value'; + autoResize: true, - this.style = Graph3d.STYLE.DOT; - this.showPerspective = true; - this.showGrid = true; - this.keepAspectRatio = true; - this.showShadow = false; - this.showGrayBottom = false; // TODO: this does not work correctly - this.showTooltip = false; - this.verticalRatio = 0.5; // 0.1 to 1.0, where 1.0 results in a 'cube' + orientation: 'bottom', + width: null, + height: null, + maxHeight: null, + minHeight: null + }; + this.options = util.deepExtend({}, this.defaultOptions); - this.animationInterval = 1000; // milliseconds - this.animationPreload = false; + // Create the DOM, props, and emitter + this._create(container); - this.camera = new Graph3d.Camera(); - this.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window? + // all components listed here will be repainted automatically + this.components = []; - this.dataTable = null; // The original data table - this.dataPoints = null; // The table with point objects + this.body = { + dom: this.dom, + domProps: this.props, + emitter: { + on: this.on.bind(this), + off: this.off.bind(this), + emit: this.emit.bind(this) + }, + util: { + snap: null, // will be specified after TimeAxis is created + toScreen: me._toScreen.bind(me), + toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width + toTime: me._toTime.bind(me), + toGlobalTime : me._toGlobalTime.bind(me) + } + }; - // the column indexes - this.colX = undefined; - this.colY = undefined; - this.colZ = undefined; - this.colValue = undefined; - this.colFilter = undefined; + // range + this.range = new Range(this.body); + this.components.push(this.range); + this.body.range = this.range; - this.xMin = 0; - this.xStep = undefined; // auto by default - this.xMax = 1; - this.yMin = 0; - this.yStep = undefined; // auto by default - this.yMax = 1; - this.zMin = 0; - this.zStep = undefined; // auto by default - this.zMax = 1; - this.valueMin = 0; - this.valueMax = 1; - this.xBarWidth = 1; - this.yBarWidth = 1; - // TODO: customize axis range + // time axis + this.timeAxis = new TimeAxis(this.body); + this.components.push(this.timeAxis); + this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis); - // constants - this.colorAxis = '#4D4D4D'; - this.colorGrid = '#D3D3D3'; - this.colorDot = '#7DC1FF'; - this.colorDotBorder = '#3267D2'; + // current time bar + this.currentTime = new CurrentTime(this.body); + this.components.push(this.currentTime); - // create a frame and canvas - this.create(); + // custom time bar + // Note: time bar will be attached in this.setOptions when selected + this.customTime = new CustomTime(this.body); + this.components.push(this.customTime); - // apply options (also when undefined) - this.setOptions(options); + // item set + this.itemSet = new ItemSet(this.body); + this.components.push(this.itemSet); - // apply data - if (data) { - this.setData(data); + this.itemsData = null; // DataSet + this.groupsData = null; // DataSet + + // apply options + if (options) { + this.setOptions(options); + } + + // create itemset + if (items) { + this.setItems(items); + } + else { + this.redraw(); } } - // Extend Graph3d with an Emitter mixin - Emitter(Graph3d.prototype); + // turn Timeline into an event emitter + Emitter(Timeline.prototype); /** - * @class Camera - * The camera is mounted on a (virtual) camera arm. The camera arm can rotate - * The camera is always looking in the direction of the origin of the arm. - * This way, the camera always rotates around one fixed point, the location - * of the camera arm. - * - * Documentation: - * http://en.wikipedia.org/wiki/3D_projection + * Create the main DOM for the Timeline: a root panel containing left, right, + * top, bottom, content, and background panel. + * @param {Element} container The container element where the Timeline will + * be attached. + * @private */ - Graph3d.Camera = function () { - this.armLocation = new Point3d(); - this.armRotation = {}; - this.armRotation.horizontal = 0; - this.armRotation.vertical = 0; - this.armLength = 1.7; + Timeline.prototype._create = function (container) { + this.dom = {}; - this.cameraLocation = new Point3d(); - this.cameraRotation = new Point3d(0.5*Math.PI, 0, 0); + this.dom.root = document.createElement('div'); + this.dom.background = document.createElement('div'); + this.dom.backgroundVertical = document.createElement('div'); + this.dom.backgroundHorizontal = document.createElement('div'); + this.dom.centerContainer = document.createElement('div'); + this.dom.leftContainer = document.createElement('div'); + this.dom.rightContainer = document.createElement('div'); + this.dom.center = document.createElement('div'); + this.dom.left = document.createElement('div'); + this.dom.right = document.createElement('div'); + this.dom.top = document.createElement('div'); + this.dom.bottom = document.createElement('div'); + this.dom.shadowTop = document.createElement('div'); + this.dom.shadowBottom = document.createElement('div'); + this.dom.shadowTopLeft = document.createElement('div'); + this.dom.shadowBottomLeft = document.createElement('div'); + this.dom.shadowTopRight = document.createElement('div'); + this.dom.shadowBottomRight = document.createElement('div'); - this.calculateCameraOrientation(); - }; + this.dom.background.className = 'vispanel background'; + this.dom.backgroundVertical.className = 'vispanel background vertical'; + this.dom.backgroundHorizontal.className = 'vispanel background horizontal'; + this.dom.centerContainer.className = 'vispanel center'; + this.dom.leftContainer.className = 'vispanel left'; + this.dom.rightContainer.className = 'vispanel right'; + this.dom.top.className = 'vispanel top'; + this.dom.bottom.className = 'vispanel bottom'; + this.dom.left.className = 'content'; + this.dom.center.className = 'content'; + this.dom.right.className = 'content'; + this.dom.shadowTop.className = 'shadow top'; + this.dom.shadowBottom.className = 'shadow bottom'; + this.dom.shadowTopLeft.className = 'shadow top'; + this.dom.shadowBottomLeft.className = 'shadow bottom'; + this.dom.shadowTopRight.className = 'shadow top'; + this.dom.shadowBottomRight.className = 'shadow bottom'; - /** - * Set the location (origin) of the arm - * @param {Number} x Normalized value of x - * @param {Number} y Normalized value of y - * @param {Number} z Normalized value of z - */ - Graph3d.Camera.prototype.setArmLocation = function(x, y, z) { - this.armLocation.x = x; - this.armLocation.y = y; - this.armLocation.z = z; + this.dom.root.appendChild(this.dom.background); + this.dom.root.appendChild(this.dom.backgroundVertical); + this.dom.root.appendChild(this.dom.backgroundHorizontal); + this.dom.root.appendChild(this.dom.centerContainer); + this.dom.root.appendChild(this.dom.leftContainer); + this.dom.root.appendChild(this.dom.rightContainer); + this.dom.root.appendChild(this.dom.top); + this.dom.root.appendChild(this.dom.bottom); - this.calculateCameraOrientation(); - }; + this.dom.centerContainer.appendChild(this.dom.center); + this.dom.leftContainer.appendChild(this.dom.left); + this.dom.rightContainer.appendChild(this.dom.right); - /** - * Set the rotation of the camera arm - * @param {Number} horizontal The horizontal rotation, between 0 and 2*PI. - * Optional, can be left undefined. - * @param {Number} vertical The vertical rotation, between 0 and 0.5*PI - * if vertical=0.5*PI, the graph is shown from the - * top. Optional, can be left undefined. - */ - Graph3d.Camera.prototype.setArmRotation = function(horizontal, vertical) { - if (horizontal !== undefined) { - this.armRotation.horizontal = horizontal; - } + this.dom.centerContainer.appendChild(this.dom.shadowTop); + this.dom.centerContainer.appendChild(this.dom.shadowBottom); + this.dom.leftContainer.appendChild(this.dom.shadowTopLeft); + this.dom.leftContainer.appendChild(this.dom.shadowBottomLeft); + this.dom.rightContainer.appendChild(this.dom.shadowTopRight); + this.dom.rightContainer.appendChild(this.dom.shadowBottomRight); - if (vertical !== undefined) { - this.armRotation.vertical = vertical; - if (this.armRotation.vertical < 0) this.armRotation.vertical = 0; - if (this.armRotation.vertical > 0.5*Math.PI) this.armRotation.vertical = 0.5*Math.PI; - } + this.on('rangechange', this.redraw.bind(this)); + this.on('change', this.redraw.bind(this)); + this.on('touch', this._onTouch.bind(this)); + this.on('pinch', this._onPinch.bind(this)); + this.on('dragstart', this._onDragStart.bind(this)); + this.on('drag', this._onDrag.bind(this)); - if (horizontal !== undefined || vertical !== undefined) { - this.calculateCameraOrientation(); - } - }; + // create event listeners for all interesting events, these events will be + // emitted via emitter + this.hammer = Hammer(this.dom.root, { + prevent_default: true + }); + this.listeners = {}; - /** - * Retrieve the current arm rotation - * @return {object} An object with parameters horizontal and vertical - */ - Graph3d.Camera.prototype.getArmRotation = function() { - var rot = {}; - rot.horizontal = this.armRotation.horizontal; - rot.vertical = this.armRotation.vertical; + var me = this; + var events = [ + 'touch', 'pinch', + 'tap', 'doubletap', 'hold', + 'dragstart', 'drag', 'dragend', + 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed for Firefox + ]; + events.forEach(function (event) { + var listener = function () { + var args = [event].concat(Array.prototype.slice.call(arguments, 0)); + me.emit.apply(me, args); + }; + me.hammer.on(event, listener); + me.listeners[event] = listener; + }); - return rot; + // size properties of each of the panels + this.props = { + root: {}, + background: {}, + centerContainer: {}, + leftContainer: {}, + rightContainer: {}, + center: {}, + left: {}, + right: {}, + top: {}, + bottom: {}, + border: {}, + scrollTop: 0, + scrollTopMin: 0 + }; + this.touch = {}; // store state information needed for touch events + + // attach the root panel to the provided container + if (!container) throw new Error('No container provided'); + container.appendChild(this.dom.root); }; /** - * Set the (normalized) length of the camera arm. - * @param {Number} length A length between 0.71 and 5.0 + * Destroy the Timeline, clean up all DOM elements and event listeners. */ - Graph3d.Camera.prototype.setArmLength = function(length) { - if (length === undefined) - return; + Timeline.prototype.destroy = function () { + // unbind datasets + this.clear(); - this.armLength = length; + // remove all event listeners + this.off(); - // Radius must be larger than the corner of the graph, - // which has a distance of sqrt(0.5^2+0.5^2) = 0.71 from the center of the - // graph - if (this.armLength < 0.71) this.armLength = 0.71; - if (this.armLength > 5.0) this.armLength = 5.0; + // stop checking for changed size + this._stopAutoResize(); - this.calculateCameraOrientation(); - }; + // remove from DOM + if (this.dom.root.parentNode) { + this.dom.root.parentNode.removeChild(this.dom.root); + } + this.dom = null; - /** - * Retrieve the arm length - * @return {Number} length - */ - Graph3d.Camera.prototype.getArmLength = function() { - return this.armLength; + // cleanup hammer touch events + for (var event in this.listeners) { + if (this.listeners.hasOwnProperty(event)) { + delete this.listeners[event]; + } + } + this.listeners = null; + this.hammer = null; + + // give all components the opportunity to cleanup + this.components.forEach(function (component) { + component.destroy(); + }); + + this.body = null; }; /** - * Retrieve the camera location - * @return {Point3d} cameraLocation + * Set options. Options will be passed to all components loaded in the Timeline. + * @param {Object} [options] + * {String} orientation + * Vertical orientation for the Timeline, + * can be 'bottom' (default) or 'top'. + * {String | Number} width + * Width for the timeline, a number in pixels or + * a css string like '1000px' or '75%'. '100%' by default. + * {String | Number} height + * Fixed height for the Timeline, a number in pixels or + * a css string like '400px' or '75%'. If undefined, + * The Timeline will automatically size such that + * its contents fit. + * {String | Number} minHeight + * Minimum height for the Timeline, a number in pixels or + * a css string like '400px' or '75%'. + * {String | Number} maxHeight + * Maximum height for the Timeline, a number in pixels or + * a css string like '400px' or '75%'. + * {Number | Date | String} start + * Start date for the visible window + * {Number | Date | String} end + * End date for the visible window */ - Graph3d.Camera.prototype.getCameraLocation = function() { - return this.cameraLocation; + Timeline.prototype.setOptions = function (options) { + if (options) { + // copy the known options + var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'orientation']; + util.selectiveExtend(fields, this.options, options); + + // enable/disable autoResize + this._initAutoResize(); + } + + // propagate options to all components + this.components.forEach(function (component) { + component.setOptions(options); + }); + + // TODO: remove deprecation error one day (deprecated since version 0.8.0) + if (options && options.order) { + throw new Error('Option order is deprecated. There is no replacement for this feature.'); + } + + // redraw everything + this.redraw(); }; /** - * Retrieve the camera rotation - * @return {Point3d} cameraRotation + * Set a custom time bar + * @param {Date} time */ - Graph3d.Camera.prototype.getCameraRotation = function() { - return this.cameraRotation; + Timeline.prototype.setCustomTime = function (time) { + if (!this.customTime) { + throw new Error('Cannot get custom time: Custom time bar is not enabled'); + } + + this.customTime.setCustomTime(time); }; /** - * Calculate the location and rotation of the camera based on the - * position and orientation of the camera arm + * Retrieve the current custom time. + * @return {Date} customTime */ - Graph3d.Camera.prototype.calculateCameraOrientation = function() { - // calculate location of the camera - this.cameraLocation.x = this.armLocation.x - this.armLength * Math.sin(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical); - this.cameraLocation.y = this.armLocation.y - this.armLength * Math.cos(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical); - this.cameraLocation.z = this.armLocation.z + this.armLength * Math.sin(this.armRotation.vertical); + Timeline.prototype.getCustomTime = function() { + if (!this.customTime) { + throw new Error('Cannot get custom time: Custom time bar is not enabled'); + } - // calculate rotation of the camera - this.cameraRotation.x = Math.PI/2 - this.armRotation.vertical; - this.cameraRotation.y = 0; - this.cameraRotation.z = -this.armRotation.horizontal; + return this.customTime.getCustomTime(); }; /** - * Calculate the scaling values, dependent on the range in x, y, and z direction + * Set items + * @param {vis.DataSet | Array | google.visualization.DataTable | null} items */ - Graph3d.prototype._setScale = function() { - this.scale = new Point3d(1 / (this.xMax - this.xMin), - 1 / (this.yMax - this.yMin), - 1 / (this.zMax - this.zMin)); + Timeline.prototype.setItems = function(items) { + var initialLoad = (this.itemsData == null); - // keep aspect ration between x and y scale if desired - if (this.keepAspectRatio) { - if (this.scale.x < this.scale.y) { - //noinspection JSSuspiciousNameCombination - this.scale.y = this.scale.x; - } - else { - //noinspection JSSuspiciousNameCombination - this.scale.x = this.scale.y; - } + // convert to type DataSet when needed + var newDataSet; + if (!items) { + newDataSet = null; + } + else if (items instanceof DataSet || items instanceof DataView) { + newDataSet = items; + } + else { + // turn an array into a dataset + newDataSet = new DataSet(items, { + type: { + start: 'Date', + end: 'Date' + } + }); } - // scale the vertical axis - this.scale.z *= this.verticalRatio; - // TODO: can this be automated? verticalRatio? - - // determine scale for (optional) value - this.scale.value = 1 / (this.valueMax - this.valueMin); + // set items + this.itemsData = newDataSet; + this.itemSet && this.itemSet.setItems(newDataSet); - // position the camera arm - var xCenter = (this.xMax + this.xMin) / 2 * this.scale.x; - var yCenter = (this.yMax + this.yMin) / 2 * this.scale.y; - var zCenter = (this.zMax + this.zMin) / 2 * this.scale.z; - this.camera.setArmLocation(xCenter, yCenter, zCenter); - }; + if (initialLoad && ('start' in this.options || 'end' in this.options)) { + this.fit(); + var start = ('start' in this.options) ? util.convert(this.options.start, 'Date') : null; + var end = ('end' in this.options) ? util.convert(this.options.end, 'Date') : null; - /** - * Convert a 3D location to a 2D location on screen - * http://en.wikipedia.org/wiki/3D_projection - * @param {Point3d} point3d A 3D point with parameters x, y, z - * @return {Point2d} point2d A 2D point with parameters x, y - */ - Graph3d.prototype._convert3Dto2D = function(point3d) { - var translation = this._convertPointToTranslation(point3d); - return this._convertTranslationToScreen(translation); + this.setWindow(start, end); + } }; /** - * Convert a 3D location its translation seen from the camera - * http://en.wikipedia.org/wiki/3D_projection - * @param {Point3d} point3d A 3D point with parameters x, y, z - * @return {Point3d} translation A 3D point with parameters x, y, z This is - * the translation of the point, seen from the - * camera + * Set groups + * @param {vis.DataSet | Array | google.visualization.DataTable} groups */ - Graph3d.prototype._convertPointToTranslation = function(point3d) { - var ax = point3d.x * this.scale.x, - ay = point3d.y * this.scale.y, - az = point3d.z * this.scale.z, + Timeline.prototype.setGroups = function(groups) { + // convert to type DataSet when needed + var newDataSet; + if (!groups) { + newDataSet = null; + } + else if (groups instanceof DataSet || groups instanceof DataView) { + newDataSet = groups; + } + else { + // turn an array into a dataset + newDataSet = new DataSet(groups); + } - cx = this.camera.getCameraLocation().x, - cy = this.camera.getCameraLocation().y, - cz = this.camera.getCameraLocation().z, + this.groupsData = newDataSet; + this.itemSet.setGroups(newDataSet); + }; - // calculate angles - sinTx = Math.sin(this.camera.getCameraRotation().x), - cosTx = Math.cos(this.camera.getCameraRotation().x), - sinTy = Math.sin(this.camera.getCameraRotation().y), - cosTy = Math.cos(this.camera.getCameraRotation().y), - sinTz = Math.sin(this.camera.getCameraRotation().z), - cosTz = Math.cos(this.camera.getCameraRotation().z), + /** + * Clear the Timeline. By Default, items, groups and options are cleared. + * Example usage: + * + * timeline.clear(); // clear items, groups, and options + * timeline.clear({options: true}); // clear options only + * + * @param {Object} [what] Optionally specify what to clear. By default: + * {items: true, groups: true, options: true} + */ + Timeline.prototype.clear = function(what) { + // clear items + if (!what || what.items) { + this.setItems(null); + } - // calculate translation - dx = cosTy * (sinTz * (ay - cy) + cosTz * (ax - cx)) - sinTy * (az - cz), - dy = sinTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) + cosTx * (cosTz * (ay - cy) - sinTz * (ax-cx)), - dz = cosTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) - sinTx * (cosTz * (ay - cy) - sinTz * (ax-cx)); + // clear groups + if (!what || what.groups) { + this.setGroups(null); + } - return new Point3d(dx, dy, dz); + // clear options of timeline and of each of the components + if (!what || what.options) { + this.components.forEach(function (component) { + component.setOptions(component.defaultOptions); + }); + + this.setOptions(this.defaultOptions); // this will also do a redraw + } }; /** - * Convert a translation point to a point on the screen - * @param {Point3d} translation A 3D point with parameters x, y, z This is - * the translation of the point, seen from the - * camera - * @return {Point2d} point2d A 2D point with parameters x, y + * Set Timeline window such that it fits all items */ - Graph3d.prototype._convertTranslationToScreen = function(translation) { - var ex = this.eye.x, - ey = this.eye.y, - ez = this.eye.z, - dx = translation.x, - dy = translation.y, - dz = translation.z; + Timeline.prototype.fit = function() { + // apply the data range as range + var dataRange = this.getItemRange(); - // calculate position on screen from translation - var bx; - var by; - if (this.showPerspective) { - bx = (dx - ex) * (ez / dz); - by = (dy - ey) * (ez / dz); + // add 5% space on both sides + var start = dataRange.min; + var end = dataRange.max; + if (start != null && end != null) { + var interval = (end.valueOf() - start.valueOf()); + if (interval <= 0) { + // prevent an empty interval + interval = 24 * 60 * 60 * 1000; // 1 day + } + start = new Date(start.valueOf() - interval * 0.05); + end = new Date(end.valueOf() + interval * 0.05); } - else { - bx = dx * -(ez / this.camera.getArmLength()); - by = dy * -(ez / this.camera.getArmLength()); + + // skip range set if there is no start and end date + if (start === null && end === null) { + return; } - // shift and scale the point to the center of the screen - // use the width of the graph to scale both horizontally and vertically. - return new Point2d( - this.xcenter + bx * this.frame.canvas.clientWidth, - this.ycenter - by * this.frame.canvas.clientWidth); + this.range.setRange(start, end); }; /** - * Set the background styling for the graph - * @param {string | {fill: string, stroke: string, strokeWidth: string}} backgroundColor + * Get the data range of the item set. + * @returns {{min: Date, max: Date}} range A range with a start and end Date. + * When no minimum is found, min==null + * When no maximum is found, max==null */ - Graph3d.prototype._setBackgroundColor = function(backgroundColor) { - var fill = 'white'; - var stroke = 'gray'; - var strokeWidth = 1; + Timeline.prototype.getItemRange = function() { + // calculate min from start filed + var dataset = this.itemsData.getDataSet(), + min = null, + max = null; - if (typeof(backgroundColor) === 'string') { - fill = backgroundColor; - stroke = 'none'; - strokeWidth = 0; - } - else if (typeof(backgroundColor) === 'object') { - if (backgroundColor.fill !== undefined) fill = backgroundColor.fill; - if (backgroundColor.stroke !== undefined) stroke = backgroundColor.stroke; - if (backgroundColor.strokeWidth !== undefined) strokeWidth = backgroundColor.strokeWidth; - } - else if (backgroundColor === undefined) { - // use use defaults - } - else { - throw 'Unsupported type of backgroundColor'; + if (dataset) { + // calculate the minimum value of the field 'start' + var minItem = dataset.min('start'); + min = minItem ? util.convert(minItem.start, 'Date').valueOf() : null; + // Note: we convert first to Date and then to number because else + // a conversion from ISODate to Number will fail + + // calculate maximum value of fields 'start' and 'end' + var maxStartItem = dataset.max('start'); + if (maxStartItem) { + max = util.convert(maxStartItem.start, 'Date').valueOf(); + } + var maxEndItem = dataset.max('end'); + if (maxEndItem) { + if (max == null) { + max = util.convert(maxEndItem.end, 'Date').valueOf(); + } + else { + max = Math.max(max, util.convert(maxEndItem.end, 'Date').valueOf()); + } + } } - this.frame.style.backgroundColor = fill; - this.frame.style.borderColor = stroke; - this.frame.style.borderWidth = strokeWidth + 'px'; - this.frame.style.borderStyle = 'solid'; + return { + min: (min != null) ? new Date(min) : null, + max: (max != null) ? new Date(max) : null + }; }; + /** + * Set selected items by their id. Replaces the current selection + * Unknown id's are silently ignored. + * @param {Array} [ids] An array with zero or more id's of the items to be + * selected. If ids is an empty array, all items will be + * unselected. + */ + Timeline.prototype.setSelection = function(ids) { + this.itemSet && this.itemSet.setSelection(ids); + }; - /// enumerate the available styles - Graph3d.STYLE = { - BAR: 0, - BARCOLOR: 1, - BARSIZE: 2, - DOT : 3, - DOTLINE : 4, - DOTCOLOR: 5, - DOTSIZE: 6, - GRID : 7, - LINE: 8, - SURFACE : 9 + /** + * Get the selected items by their id + * @return {Array} ids The ids of the selected items + */ + Timeline.prototype.getSelection = function() { + return this.itemSet && this.itemSet.getSelection() || []; }; /** - * Retrieve the style index from given styleName - * @param {string} styleName Style name such as 'dot', 'grid', 'dot-line' - * @return {Number} styleNumber Enumeration value representing the style, or -1 - * when not found + * Set the visible window. Both parameters are optional, you can change only + * start or only end. Syntax: + * + * TimeLine.setWindow(start, end) + * TimeLine.setWindow(range) + * + * Where start and end can be a Date, number, or string, and range is an + * object with properties start and end. + * + * @param {Date | Number | String | Object} [start] Start date of visible window + * @param {Date | Number | String} [end] End date of visible window */ - Graph3d.prototype._getStyleNumber = function(styleName) { - switch (styleName) { - case 'dot': return Graph3d.STYLE.DOT; - case 'dot-line': return Graph3d.STYLE.DOTLINE; - case 'dot-color': return Graph3d.STYLE.DOTCOLOR; - case 'dot-size': return Graph3d.STYLE.DOTSIZE; - case 'line': return Graph3d.STYLE.LINE; - case 'grid': return Graph3d.STYLE.GRID; - case 'surface': return Graph3d.STYLE.SURFACE; - case 'bar': return Graph3d.STYLE.BAR; - case 'bar-color': return Graph3d.STYLE.BARCOLOR; - case 'bar-size': return Graph3d.STYLE.BARSIZE; + Timeline.prototype.setWindow = function(start, end) { + if (arguments.length == 1) { + var range = arguments[0]; + this.range.setRange(range.start, range.end); + } + else { + this.range.setRange(start, end); } + }; - return -1; + /** + * Get the visible window + * @return {{start: Date, end: Date}} Visible range + */ + Timeline.prototype.getWindow = function() { + var range = this.range.getRange(); + return { + start: new Date(range.start), + end: new Date(range.end) + }; }; /** - * Determine the indexes of the data columns, based on the given style and data - * @param {DataSet} data - * @param {Number} style + * Force a redraw of the Timeline. Can be useful to manually redraw when + * option autoResize=false */ - Graph3d.prototype._determineColumnIndexes = function(data, style) { - if (this.style === Graph3d.STYLE.DOT || - this.style === Graph3d.STYLE.DOTLINE || - this.style === Graph3d.STYLE.LINE || - this.style === Graph3d.STYLE.GRID || - this.style === Graph3d.STYLE.SURFACE || - this.style === Graph3d.STYLE.BAR) { - // 3 columns expected, and optionally a 4th with filter values - this.colX = 0; - this.colY = 1; - this.colZ = 2; - this.colValue = undefined; + Timeline.prototype.redraw = function() { + var resized = false, + options = this.options, + props = this.props, + dom = this.dom; - if (data.getNumberOfColumns() > 3) { - this.colFilter = 3; - } - } - else if (this.style === Graph3d.STYLE.DOTCOLOR || - this.style === Graph3d.STYLE.DOTSIZE || - this.style === Graph3d.STYLE.BARCOLOR || - this.style === Graph3d.STYLE.BARSIZE) { - // 4 columns expected, and optionally a 5th with filter values - this.colX = 0; - this.colY = 1; - this.colZ = 2; - this.colValue = 3; + if (!dom) return; // when destroyed - if (data.getNumberOfColumns() > 4) { - this.colFilter = 4; - } - } - else { - throw 'Unknown style "' + this.style + '"'; - } - }; + // update class names + dom.root.className = 'vis timeline root ' + options.orientation; - Graph3d.prototype.getNumberOfRows = function(data) { - return data.length; - } + // update root width and height options + dom.root.style.maxHeight = util.option.asSize(options.maxHeight, ''); + dom.root.style.minHeight = util.option.asSize(options.minHeight, ''); + dom.root.style.width = util.option.asSize(options.width, ''); + // calculate border widths + props.border.left = (dom.centerContainer.offsetWidth - dom.centerContainer.clientWidth) / 2; + props.border.right = props.border.left; + props.border.top = (dom.centerContainer.offsetHeight - dom.centerContainer.clientHeight) / 2; + props.border.bottom = props.border.top; + var borderRootHeight= dom.root.offsetHeight - dom.root.clientHeight; + var borderRootWidth = dom.root.offsetWidth - dom.root.clientWidth; - Graph3d.prototype.getNumberOfColumns = function(data) { - var counter = 0; - for (var column in data[0]) { - if (data[0].hasOwnProperty(column)) { - counter++; - } - } - return counter; - } + // calculate the heights. If any of the side panels is empty, we set the height to + // minus the border width, such that the border will be invisible + props.center.height = dom.center.offsetHeight; + props.left.height = dom.left.offsetHeight; + props.right.height = dom.right.offsetHeight; + props.top.height = dom.top.clientHeight || -props.border.top; + props.bottom.height = dom.bottom.clientHeight || -props.border.bottom; + // TODO: compensate borders when any of the panels is empty. - Graph3d.prototype.getDistinctValues = function(data, column) { - var distinctValues = []; - for (var i = 0; i < data.length; i++) { - if (distinctValues.indexOf(data[i][column]) == -1) { - distinctValues.push(data[i][column]); - } - } - return distinctValues; - } + // apply auto height + // TODO: only calculate autoHeight when needed (else we cause an extra reflow/repaint of the DOM) + var contentHeight = Math.max(props.left.height, props.center.height, props.right.height); + var autoHeight = props.top.height + contentHeight + props.bottom.height + + borderRootHeight + props.border.top + props.border.bottom; + dom.root.style.height = util.option.asSize(options.height, autoHeight + 'px'); + // calculate heights of the content panels + props.root.height = dom.root.offsetHeight; + props.background.height = props.root.height - borderRootHeight; + var containerHeight = props.root.height - props.top.height - props.bottom.height - + borderRootHeight; + props.centerContainer.height = containerHeight; + props.leftContainer.height = containerHeight; + props.rightContainer.height = props.leftContainer.height; - Graph3d.prototype.getColumnRange = function(data,column) { - var minMax = {min:data[0][column],max:data[0][column]}; - for (var i = 0; i < data.length; i++) { - if (minMax.min > data[i][column]) { minMax.min = data[i][column]; } - if (minMax.max < data[i][column]) { minMax.max = data[i][column]; } - } - return minMax; - }; + // calculate the widths of the panels + props.root.width = dom.root.offsetWidth; + props.background.width = props.root.width - borderRootWidth; + props.left.width = dom.leftContainer.clientWidth || -props.border.left; + props.leftContainer.width = props.left.width; + props.right.width = dom.rightContainer.clientWidth || -props.border.right; + props.rightContainer.width = props.right.width; + var centerWidth = props.root.width - props.left.width - props.right.width - borderRootWidth; + props.center.width = centerWidth; + props.centerContainer.width = centerWidth; + props.top.width = centerWidth; + props.bottom.width = centerWidth; - /** - * Initialize the data from the data table. Calculate minimum and maximum values - * and column index values - * @param {Array | DataSet | DataView} rawData The data containing the items for the Graph. - * @param {Number} style Style Number - */ - Graph3d.prototype._dataInitialize = function (rawData, style) { - var me = this; + // resize the panels + dom.background.style.height = props.background.height + 'px'; + dom.backgroundVertical.style.height = props.background.height + 'px'; + dom.backgroundHorizontal.style.height = props.centerContainer.height + 'px'; + dom.centerContainer.style.height = props.centerContainer.height + 'px'; + dom.leftContainer.style.height = props.leftContainer.height + 'px'; + dom.rightContainer.style.height = props.rightContainer.height + 'px'; - // unsubscribe from the dataTable - if (this.dataSet) { - this.dataSet.off('*', this._onChange); - } + dom.background.style.width = props.background.width + 'px'; + dom.backgroundVertical.style.width = props.centerContainer.width + 'px'; + dom.backgroundHorizontal.style.width = props.background.width + 'px'; + dom.centerContainer.style.width = props.center.width + 'px'; + dom.top.style.width = props.top.width + 'px'; + dom.bottom.style.width = props.bottom.width + 'px'; - if (rawData === undefined) - return; + // reposition the panels + dom.background.style.left = '0'; + dom.background.style.top = '0'; + dom.backgroundVertical.style.left = props.left.width + 'px'; + dom.backgroundVertical.style.top = '0'; + dom.backgroundHorizontal.style.left = '0'; + dom.backgroundHorizontal.style.top = props.top.height + 'px'; + dom.centerContainer.style.left = props.left.width + 'px'; + dom.centerContainer.style.top = props.top.height + 'px'; + dom.leftContainer.style.left = '0'; + dom.leftContainer.style.top = props.top.height + 'px'; + dom.rightContainer.style.left = (props.left.width + props.center.width) + 'px'; + dom.rightContainer.style.top = props.top.height + 'px'; + dom.top.style.left = props.left.width + 'px'; + dom.top.style.top = '0'; + dom.bottom.style.left = props.left.width + 'px'; + dom.bottom.style.top = (props.top.height + props.centerContainer.height) + 'px'; - if (Array.isArray(rawData)) { - rawData = new DataSet(rawData); - } + // update the scrollTop, feasible range for the offset can be changed + // when the height of the Timeline or of the contents of the center changed + this._updateScrollTop(); - var data; - if (rawData instanceof DataSet || rawData instanceof DataView) { - data = rawData.get(); - } - else { - throw new Error('Array, DataSet, or DataView expected'); + // reposition the scrollable contents + var offset = this.props.scrollTop; + if (options.orientation == 'bottom') { + offset += Math.max(this.props.centerContainer.height - this.props.center.height - + this.props.border.top - this.props.border.bottom, 0); } + dom.center.style.left = '0'; + dom.center.style.top = offset + 'px'; + dom.left.style.left = '0'; + dom.left.style.top = offset + 'px'; + dom.right.style.left = '0'; + dom.right.style.top = offset + 'px'; - if (data.length == 0) - return; + // show shadows when vertical scrolling is available + var visibilityTop = this.props.scrollTop == 0 ? 'hidden' : ''; + var visibilityBottom = this.props.scrollTop == this.props.scrollTopMin ? 'hidden' : ''; + dom.shadowTop.style.visibility = visibilityTop; + dom.shadowBottom.style.visibility = visibilityBottom; + dom.shadowTopLeft.style.visibility = visibilityTop; + dom.shadowBottomLeft.style.visibility = visibilityBottom; + dom.shadowTopRight.style.visibility = visibilityTop; + dom.shadowBottomRight.style.visibility = visibilityBottom; - this.dataSet = rawData; - this.dataTable = data; + // redraw all components + this.components.forEach(function (component) { + resized = component.redraw() || resized; + }); + if (resized) { + // keep repainting until all sizes are settled + this.redraw(); + } + }; - // subscribe to changes in the dataset - this._onChange = function () { - me.setData(me.dataSet); - }; - this.dataSet.on('*', this._onChange); + // TODO: deprecated since version 1.1.0, remove some day + Timeline.prototype.repaint = function () { + throw new Error('Function repaint is deprecated. Use redraw instead.'); + }; - // _determineColumnIndexes - // getNumberOfRows (points) - // getNumberOfColumns (x,y,z,v,t,t1,t2...) - // getDistinctValues (unique values?) - // getColumnRange + /** + * Convert a position on screen (pixels) to a datetime + * @param {int} x Position on the screen in pixels + * @return {Date} time The datetime the corresponds with given position x + * @private + */ + // TODO: move this function to Range + Timeline.prototype._toTime = function(x) { + var conversion = this.range.conversion(this.props.center.width); + return new Date(x / conversion.scale + conversion.offset); + }; - // determine the location of x,y,z,value,filter columns - this.colX = 'x'; - this.colY = 'y'; - this.colZ = 'z'; - this.colValue = 'style'; - this.colFilter = 'filter'; + /** + * Convert a position on the global screen (pixels) to a datetime + * @param {int} x Position on the screen in pixels + * @return {Date} time The datetime the corresponds with given position x + * @private + */ + // TODO: move this function to Range + Timeline.prototype._toGlobalTime = function(x) { + var conversion = this.range.conversion(this.props.root.width); + return new Date(x / conversion.scale + conversion.offset); + }; + /** + * Convert a datetime (Date object) into a position on the screen + * @param {Date} time A date + * @return {int} x The position on the screen in pixels which corresponds + * with the given date. + * @private + */ + // TODO: move this function to Range + Timeline.prototype._toScreen = function(time) { + var conversion = this.range.conversion(this.props.center.width); + return (time.valueOf() - conversion.offset) * conversion.scale; + }; - // check if a filter column is provided - if (data[0].hasOwnProperty('filter')) { - if (this.dataFilter === undefined) { - this.dataFilter = new Filter(rawData, this.colFilter, this); - this.dataFilter.setOnLoadCallback(function() {me.redraw();}); - } - } - - - var withBars = this.style == Graph3d.STYLE.BAR || - this.style == Graph3d.STYLE.BARCOLOR || - this.style == Graph3d.STYLE.BARSIZE; - - // determine barWidth from data - if (withBars) { - if (this.defaultXBarWidth !== undefined) { - this.xBarWidth = this.defaultXBarWidth; - } - else { - var dataX = this.getDistinctValues(data,this.colX); - this.xBarWidth = (dataX[1] - dataX[0]) || 1; - } - if (this.defaultYBarWidth !== undefined) { - this.yBarWidth = this.defaultYBarWidth; - } - else { - var dataY = this.getDistinctValues(data,this.colY); - this.yBarWidth = (dataY[1] - dataY[0]) || 1; - } - } + /** + * Convert a datetime (Date object) into a position on the root + * This is used to get the pixel density estimate for the screen, not the center panel + * @param {Date} time A date + * @return {int} x The position on root in pixels which corresponds + * with the given date. + * @private + */ + // TODO: move this function to Range + Timeline.prototype._toGlobalScreen = function(time) { + var conversion = this.range.conversion(this.props.root.width); + return (time.valueOf() - conversion.offset) * conversion.scale; + }; - // calculate minimums and maximums - var xRange = this.getColumnRange(data,this.colX); - if (withBars) { - xRange.min -= this.xBarWidth / 2; - xRange.max += this.xBarWidth / 2; - } - this.xMin = (this.defaultXMin !== undefined) ? this.defaultXMin : xRange.min; - this.xMax = (this.defaultXMax !== undefined) ? this.defaultXMax : xRange.max; - if (this.xMax <= this.xMin) this.xMax = this.xMin + 1; - this.xStep = (this.defaultXStep !== undefined) ? this.defaultXStep : (this.xMax-this.xMin)/5; - var yRange = this.getColumnRange(data,this.colY); - if (withBars) { - yRange.min -= this.yBarWidth / 2; - yRange.max += this.yBarWidth / 2; + /** + * Initialize watching when option autoResize is true + * @private + */ + Timeline.prototype._initAutoResize = function () { + if (this.options.autoResize == true) { + this._startAutoResize(); } - this.yMin = (this.defaultYMin !== undefined) ? this.defaultYMin : yRange.min; - this.yMax = (this.defaultYMax !== undefined) ? this.defaultYMax : yRange.max; - if (this.yMax <= this.yMin) this.yMax = this.yMin + 1; - this.yStep = (this.defaultYStep !== undefined) ? this.defaultYStep : (this.yMax-this.yMin)/5; - - var zRange = this.getColumnRange(data,this.colZ); - this.zMin = (this.defaultZMin !== undefined) ? this.defaultZMin : zRange.min; - this.zMax = (this.defaultZMax !== undefined) ? this.defaultZMax : zRange.max; - if (this.zMax <= this.zMin) this.zMax = this.zMin + 1; - this.zStep = (this.defaultZStep !== undefined) ? this.defaultZStep : (this.zMax-this.zMin)/5; - - if (this.colValue !== undefined) { - var valueRange = this.getColumnRange(data,this.colValue); - this.valueMin = (this.defaultValueMin !== undefined) ? this.defaultValueMin : valueRange.min; - this.valueMax = (this.defaultValueMax !== undefined) ? this.defaultValueMax : valueRange.max; - if (this.valueMax <= this.valueMin) this.valueMax = this.valueMin + 1; + else { + this._stopAutoResize(); } - - // set the scale dependent on the ranges. - this._setScale(); }; - - /** - * Filter the data based on the current filter - * @param {Array} data - * @return {Array} dataPoints Array with point objects which can be drawn on screen + * Watch for changes in the size of the container. On resize, the Panel will + * automatically redraw itself. + * @private */ - Graph3d.prototype._getDataPoints = function (data) { - // TODO: store the created matrix dataPoints in the filters instead of reloading each time - var x, y, i, z, obj, point; + Timeline.prototype._startAutoResize = function () { + var me = this; - var dataPoints = []; + this._stopAutoResize(); - if (this.style === Graph3d.STYLE.GRID || - this.style === Graph3d.STYLE.SURFACE) { - // copy all values from the google data table to a matrix - // the provided values are supposed to form a grid of (x,y) positions + this._onResize = function() { + if (me.options.autoResize != true) { + // stop watching when the option autoResize is changed to false + me._stopAutoResize(); + return; + } - // create two lists with all present x and y values - var dataX = []; - var dataY = []; - for (i = 0; i < this.getNumberOfRows(data); i++) { - x = data[i][this.colX] || 0; - y = data[i][this.colY] || 0; + if (me.dom.root) { + // check whether the frame is resized + if ((me.dom.root.clientWidth != me.props.lastWidth) || + (me.dom.root.clientHeight != me.props.lastHeight)) { + me.props.lastWidth = me.dom.root.clientWidth; + me.props.lastHeight = me.dom.root.clientHeight; - if (dataX.indexOf(x) === -1) { - dataX.push(x); - } - if (dataY.indexOf(y) === -1) { - dataY.push(y); + me.emit('change'); } } + }; - function sortNumber(a, b) { - return a - b; - } - dataX.sort(sortNumber); - dataY.sort(sortNumber); - - // create a grid, a 2d matrix, with all values. - var dataMatrix = []; // temporary data matrix - for (i = 0; i < data.length; i++) { - x = data[i][this.colX] || 0; - y = data[i][this.colY] || 0; - z = data[i][this.colZ] || 0; + // add event listener to window resize + util.addEventListener(window, 'resize', this._onResize); - var xIndex = dataX.indexOf(x); // TODO: implement Array().indexOf() for Internet Explorer - var yIndex = dataY.indexOf(y); + this.watchTimer = setInterval(this._onResize, 1000); + }; - if (dataMatrix[xIndex] === undefined) { - dataMatrix[xIndex] = []; - } + /** + * Stop watching for a resize of the frame. + * @private + */ + Timeline.prototype._stopAutoResize = function () { + if (this.watchTimer) { + clearInterval(this.watchTimer); + this.watchTimer = undefined; + } - var point3d = new Point3d(); - point3d.x = x; - point3d.y = y; - point3d.z = z; + // remove event listener on window.resize + util.removeEventListener(window, 'resize', this._onResize); + this._onResize = null; + }; - obj = {}; - obj.point = point3d; - obj.trans = undefined; - obj.screen = undefined; - obj.bottom = new Point3d(x, y, this.zMin); + /** + * Start moving the timeline vertically + * @param {Event} event + * @private + */ + Timeline.prototype._onTouch = function (event) { + this.touch.allowDragging = true; + }; - dataMatrix[xIndex][yIndex] = obj; + /** + * Start moving the timeline vertically + * @param {Event} event + * @private + */ + Timeline.prototype._onPinch = function (event) { + this.touch.allowDragging = false; + }; - dataPoints.push(obj); - } + /** + * Start moving the timeline vertically + * @param {Event} event + * @private + */ + Timeline.prototype._onDragStart = function (event) { + this.touch.initialScrollTop = this.props.scrollTop; + }; - // fill in the pointers to the neighbors. - for (x = 0; x < dataMatrix.length; x++) { - for (y = 0; y < dataMatrix[x].length; y++) { - if (dataMatrix[x][y]) { - dataMatrix[x][y].pointRight = (x < dataMatrix.length-1) ? dataMatrix[x+1][y] : undefined; - dataMatrix[x][y].pointTop = (y < dataMatrix[x].length-1) ? dataMatrix[x][y+1] : undefined; - dataMatrix[x][y].pointCross = - (x < dataMatrix.length-1 && y < dataMatrix[x].length-1) ? - dataMatrix[x+1][y+1] : - undefined; - } - } - } - } - else { // 'dot', 'dot-line', etc. - // copy all values from the google data table to a list with Point3d objects - for (i = 0; i < data.length; i++) { - point = new Point3d(); - point.x = data[i][this.colX] || 0; - point.y = data[i][this.colY] || 0; - point.z = data[i][this.colZ] || 0; + /** + * Move the timeline vertically + * @param {Event} event + * @private + */ + Timeline.prototype._onDrag = function (event) { + // refuse to drag when we where pinching to prevent the timeline make a jump + // when releasing the fingers in opposite order from the touch screen + if (!this.touch.allowDragging) return; - if (this.colValue !== undefined) { - point.value = data[i][this.colValue] || 0; - } + var delta = event.gesture.deltaY; - obj = {}; - obj.point = point; - obj.bottom = new Point3d(point.x, point.y, this.zMin); - obj.trans = undefined; - obj.screen = undefined; + var oldScrollTop = this._getScrollTop(); + var newScrollTop = this._setScrollTop(this.touch.initialScrollTop + delta); - dataPoints.push(obj); - } + if (newScrollTop != oldScrollTop) { + this.redraw(); // TODO: this causes two redraws when dragging, the other is triggered by rangechange already } - - return dataPoints; }; /** - * Create the main frame for the Graph3d. - * This function is executed once when a Graph3d object is created. The frame - * contains a canvas, and this canvas contains all objects like the axis and - * nodes. + * Apply a scrollTop + * @param {Number} scrollTop + * @returns {Number} scrollTop Returns the applied scrollTop + * @private */ - Graph3d.prototype.create = function () { - // remove all elements from the container element. - while (this.containerElement.hasChildNodes()) { - this.containerElement.removeChild(this.containerElement.firstChild); - } - - this.frame = document.createElement('div'); - this.frame.style.position = 'relative'; - this.frame.style.overflow = 'hidden'; + Timeline.prototype._setScrollTop = function (scrollTop) { + this.props.scrollTop = scrollTop; + this._updateScrollTop(); + return this.props.scrollTop; + }; - // create the graph canvas (HTML canvas element) - this.frame.canvas = document.createElement( 'canvas' ); - this.frame.canvas.style.position = 'relative'; - this.frame.appendChild(this.frame.canvas); - //if (!this.frame.canvas.getContext) { - { - var noCanvas = document.createElement( 'DIV' ); - noCanvas.style.color = 'red'; - noCanvas.style.fontWeight = 'bold' ; - noCanvas.style.padding = '10px'; - noCanvas.innerHTML = 'Error: your browser does not support HTML canvas'; - this.frame.canvas.appendChild(noCanvas); + /** + * Update the current scrollTop when the height of the containers has been changed + * @returns {Number} scrollTop Returns the applied scrollTop + * @private + */ + Timeline.prototype._updateScrollTop = function () { + // recalculate the scrollTopMin + var scrollTopMin = Math.min(this.props.centerContainer.height - this.props.center.height, 0); // is negative or zero + if (scrollTopMin != this.props.scrollTopMin) { + // in case of bottom orientation, change the scrollTop such that the contents + // do not move relative to the time axis at the bottom + if (this.options.orientation == 'bottom') { + this.props.scrollTop += (scrollTopMin - this.props.scrollTopMin); + } + this.props.scrollTopMin = scrollTopMin; } - this.frame.filter = document.createElement( 'div' ); - this.frame.filter.style.position = 'absolute'; - this.frame.filter.style.bottom = '0px'; - this.frame.filter.style.left = '0px'; - this.frame.filter.style.width = '100%'; - this.frame.appendChild(this.frame.filter); - - // add event listeners to handle moving and zooming the contents - var me = this; - var onmousedown = function (event) {me._onMouseDown(event);}; - var ontouchstart = function (event) {me._onTouchStart(event);}; - var onmousewheel = function (event) {me._onWheel(event);}; - var ontooltip = function (event) {me._onTooltip(event);}; - // TODO: these events are never cleaned up... can give a 'memory leakage' - - G3DaddEventListener(this.frame.canvas, 'keydown', onkeydown); - G3DaddEventListener(this.frame.canvas, 'mousedown', onmousedown); - G3DaddEventListener(this.frame.canvas, 'touchstart', ontouchstart); - G3DaddEventListener(this.frame.canvas, 'mousewheel', onmousewheel); - G3DaddEventListener(this.frame.canvas, 'mousemove', ontooltip); + // limit the scrollTop to the feasible scroll range + if (this.props.scrollTop > 0) this.props.scrollTop = 0; + if (this.props.scrollTop < scrollTopMin) this.props.scrollTop = scrollTopMin; - // add the new graph to the container element - this.containerElement.appendChild(this.frame); + return this.props.scrollTop; }; - /** - * Set a new size for the graph - * @param {string} width Width in pixels or percentage (for example '800px' - * or '50%') - * @param {string} height Height in pixels or percentage (for example '400px' - * or '30%') + * Get the current scrollTop + * @returns {number} scrollTop + * @private */ - Graph3d.prototype.setSize = function(width, height) { - this.frame.style.width = width; - this.frame.style.height = height; - - this._resizeCanvas(); + Timeline.prototype._getScrollTop = function () { + return this.props.scrollTop; }; - /** - * Resize the canvas to the current size of the frame - */ - Graph3d.prototype._resizeCanvas = function() { - this.frame.canvas.style.width = '100%'; - this.frame.canvas.style.height = '100%'; + module.exports = Timeline; - this.frame.canvas.width = this.frame.canvas.clientWidth; - this.frame.canvas.height = this.frame.canvas.clientHeight; - // adjust with for margin - this.frame.filter.style.width = (this.frame.canvas.clientWidth - 2 * 10) + 'px'; - }; +/***/ }, +/* 6 */ +/***/ function(module, exports, __webpack_require__) { /** - * Start animation + * @constructor DataStep + * The class DataStep is an iterator for data for the lineGraph. You provide a start data point and an + * end data point. The class itself determines the best scale (step size) based on the + * provided start Date, end Date, and minimumStep. + * + * If minimumStep is provided, the step size is chosen as close as possible + * to the minimumStep but larger than minimumStep. If minimumStep is not + * provided, the scale is set to 1 DAY. + * The minimumStep should correspond with the onscreen size of about 6 characters + * + * Alternatively, you can set a scale by hand. + * After creation, you can initialize the class by executing first(). Then you + * can iterate from the start date to the end date via next(). You can check if + * the end date is reached with the function hasNext(). After each step, you can + * retrieve the current date via getCurrent(). + * The DataStep has scales ranging from milliseconds, seconds, minutes, hours, + * days, to years. + * + * Version: 1.2 + * + * @param {Date} [start] The start date, for example new Date(2010, 9, 21) + * or new Date(2010, 9, 21, 23, 45, 00) + * @param {Date} [end] The end date + * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds */ - Graph3d.prototype.animationStart = function() { - if (!this.frame.filter || !this.frame.filter.slider) - throw 'No animation available'; + function DataStep(start, end, minimumStep, containerHeight, forcedStepSize) { + // variables + this.current = 0; - this.frame.filter.slider.play(); - }; + this.autoScale = true; + this.stepIndex = 0; + this.step = 1; + this.scale = 1; + this.marginStart; + this.marginEnd; - /** - * Stop animation - */ - Graph3d.prototype.animationStop = function() { - if (!this.frame.filter || !this.frame.filter.slider) return; + this.majorSteps = [1, 2, 5, 10]; + this.minorSteps = [0.25, 0.5, 1, 2]; + + this.setRange(start, end, minimumStep, containerHeight, forcedStepSize); + } - this.frame.filter.slider.stop(); - }; /** - * Resize the center position based on the current values in this.defaultXCenter - * and this.defaultYCenter (which are strings with a percentage or a value - * in pixels). The center positions are the variables this.xCenter - * and this.yCenter + * Set a new range + * If minimumStep is provided, the step size is chosen as close as possible + * to the minimumStep but larger than minimumStep. If minimumStep is not + * provided, the scale is set to 1 DAY. + * The minimumStep should correspond with the onscreen size of about 6 characters + * @param {Number} [start] The start date and time. + * @param {Number} [end] The end date and time. + * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds */ - Graph3d.prototype._resizeCenter = function() { - // calculate the horizontal center position - if (this.defaultXCenter.charAt(this.defaultXCenter.length-1) === '%') { - this.xcenter = - parseFloat(this.defaultXCenter) / 100 * - this.frame.canvas.clientWidth; - } - else { - this.xcenter = parseFloat(this.defaultXCenter); // supposed to be in px - } + DataStep.prototype.setRange = function(start, end, minimumStep, containerHeight, forcedStepSize) { + this._start = start; + this._end = end; - // calculate the vertical center position - if (this.defaultYCenter.charAt(this.defaultYCenter.length-1) === '%') { - this.ycenter = - parseFloat(this.defaultYCenter) / 100 * - (this.frame.canvas.clientHeight - this.frame.filter.clientHeight); - } - else { - this.ycenter = parseFloat(this.defaultYCenter); // supposed to be in px + if (this.autoScale) { + this.setMinimumStep(minimumStep, containerHeight, forcedStepSize); } + this.setFirst(); }; /** - * Set the rotation and distance of the camera - * @param {Object} pos An object with the camera position. The object - * contains three parameters: - * - horizontal {Number} - * The horizontal rotation, between 0 and 2*PI. - * Optional, can be left undefined. - * - vertical {Number} - * The vertical rotation, between 0 and 0.5*PI - * if vertical=0.5*PI, the graph is shown from the - * top. Optional, can be left undefined. - * - distance {Number} - * The (normalized) distance of the camera to the - * center of the graph, a value between 0.71 and 5.0. - * Optional, can be left undefined. + * Automatically determine the scale that bests fits the provided minimum step + * @param {Number} [minimumStep] The minimum step size in milliseconds */ - Graph3d.prototype.setCameraPosition = function(pos) { - if (pos === undefined) { - return; - } + DataStep.prototype.setMinimumStep = function(minimumStep, containerHeight) { + // round to floor + var size = this._end - this._start; + var safeSize = size * 1.1; + var minimumStepValue = minimumStep * (safeSize / containerHeight); + var orderOfMagnitude = Math.round(Math.log(safeSize)/Math.LN10); - if (pos.horizontal !== undefined && pos.vertical !== undefined) { - this.camera.setArmRotation(pos.horizontal, pos.vertical); - } + var minorStepIdx = -1; + var magnitudefactor = Math.pow(10,orderOfMagnitude); - if (pos.distance !== undefined) { - this.camera.setArmLength(pos.distance); + var start = 0; + if (orderOfMagnitude < 0) { + start = orderOfMagnitude; } - this.redraw(); + var solutionFound = false; + for (var i = start; Math.abs(i) <= Math.abs(orderOfMagnitude); i++) { + magnitudefactor = Math.pow(10,i); + for (var j = 0; j < this.minorSteps.length; j++) { + var stepSize = magnitudefactor * this.minorSteps[j]; + if (stepSize >= minimumStepValue) { + solutionFound = true; + minorStepIdx = j; + break; + } + } + if (solutionFound == true) { + break; + } + } + this.stepIndex = minorStepIdx; + this.scale = magnitudefactor; + this.step = magnitudefactor * this.minorSteps[minorStepIdx]; }; /** - * Retrieve the current camera rotation - * @return {object} An object with parameters horizontal, vertical, and - * distance + * Set the range iterator to the start date. */ - Graph3d.prototype.getCameraPosition = function() { - var pos = this.camera.getArmRotation(); - pos.distance = this.camera.getArmLength(); - return pos; + DataStep.prototype.first = function() { + this.setFirst(); }; /** - * Load data into the 3D Graph + * Round the current date to the first minor date value + * This must be executed once when the current date is set to start Date */ - Graph3d.prototype._readData = function(data) { - // read the data - this._dataInitialize(data, this.style); + DataStep.prototype.setFirst = function() { + var niceStart = this._start - (this.scale * this.minorSteps[this.stepIndex]); + var niceEnd = this._end + (this.scale * this.minorSteps[this.stepIndex]); + this.marginEnd = this.roundToMinor(niceEnd); + this.marginStart = this.roundToMinor(niceStart); + this.marginRange = this.marginEnd - this.marginStart; - if (this.dataFilter) { - // apply filtering - this.dataPoints = this.dataFilter._getDataPoints(); + this.current = this.marginEnd; + + }; + + DataStep.prototype.roundToMinor = function(value) { + var rounded = value - (value % (this.scale * this.minorSteps[this.stepIndex])); + if (value % (this.scale * this.minorSteps[this.stepIndex]) > 0.5 * (this.scale * this.minorSteps[this.stepIndex])) { + return rounded + (this.scale * this.minorSteps[this.stepIndex]); } else { - // no filtering. load all data - this.dataPoints = this._getDataPoints(this.dataTable); + return rounded; } + } - // draw the filter - this._redrawFilter(); + + /** + * Check if the there is a next step + * @return {boolean} true if the current date has not passed the end date + */ + DataStep.prototype.hasNext = function () { + return (this.current >= this.marginStart); }; /** - * Replace the dataset of the Graph3d - * @param {Array | DataSet | DataView} data + * Do the next step */ - Graph3d.prototype.setData = function (data) { - this._readData(data); - this.redraw(); + DataStep.prototype.next = function() { + var prev = this.current; + this.current -= this.step; - // start animation when option is true - if (this.animationAutoStart && this.dataFilter) { - this.animationStart(); + // safety mechanism: if current time is still unchanged, move to the end + if (this.current == prev) { + this.current = this._end; } }; /** - * Update the options. Options will be merged with current options - * @param {Object} options + * Do the next step */ - Graph3d.prototype.setOptions = function (options) { - var cameraPosition = undefined; - - this.animationStop(); - - if (options !== undefined) { - // retrieve parameter values - if (options.width !== undefined) this.width = options.width; - if (options.height !== undefined) this.height = options.height; + DataStep.prototype.previous = function() { + this.current += this.step; + this.marginEnd += this.step; + this.marginRange = this.marginEnd - this.marginStart; + }; - if (options.xCenter !== undefined) this.defaultXCenter = options.xCenter; - if (options.yCenter !== undefined) this.defaultYCenter = options.yCenter; - if (options.filterLabel !== undefined) this.filterLabel = options.filterLabel; - if (options.legendLabel !== undefined) this.legendLabel = options.legendLabel; - if (options.xLabel !== undefined) this.xLabel = options.xLabel; - if (options.yLabel !== undefined) this.yLabel = options.yLabel; - if (options.zLabel !== undefined) this.zLabel = options.zLabel; - if (options.style !== undefined) { - var styleNumber = this._getStyleNumber(options.style); - if (styleNumber !== -1) { - this.style = styleNumber; - } + /** + * Get the current datetime + * @return {String} current The current date + */ + DataStep.prototype.getCurrent = function() { + var toPrecision = '' + Number(this.current).toPrecision(5); + for (var i = toPrecision.length-1; i > 0; i--) { + if (toPrecision[i] == "0") { + toPrecision = toPrecision.slice(0,i); } - if (options.showGrid !== undefined) this.showGrid = options.showGrid; - if (options.showPerspective !== undefined) this.showPerspective = options.showPerspective; - if (options.showShadow !== undefined) this.showShadow = options.showShadow; - if (options.tooltip !== undefined) this.showTooltip = options.tooltip; - if (options.showAnimationControls !== undefined) this.showAnimationControls = options.showAnimationControls; - if (options.keepAspectRatio !== undefined) this.keepAspectRatio = options.keepAspectRatio; - if (options.verticalRatio !== undefined) this.verticalRatio = options.verticalRatio; - - if (options.animationInterval !== undefined) this.animationInterval = options.animationInterval; - if (options.animationPreload !== undefined) this.animationPreload = options.animationPreload; - if (options.animationAutoStart !== undefined)this.animationAutoStart = options.animationAutoStart; - - if (options.xBarWidth !== undefined) this.defaultXBarWidth = options.xBarWidth; - if (options.yBarWidth !== undefined) this.defaultYBarWidth = options.yBarWidth; - - if (options.xMin !== undefined) this.defaultXMin = options.xMin; - if (options.xStep !== undefined) this.defaultXStep = options.xStep; - if (options.xMax !== undefined) this.defaultXMax = options.xMax; - if (options.yMin !== undefined) this.defaultYMin = options.yMin; - if (options.yStep !== undefined) this.defaultYStep = options.yStep; - if (options.yMax !== undefined) this.defaultYMax = options.yMax; - if (options.zMin !== undefined) this.defaultZMin = options.zMin; - if (options.zStep !== undefined) this.defaultZStep = options.zStep; - if (options.zMax !== undefined) this.defaultZMax = options.zMax; - if (options.valueMin !== undefined) this.defaultValueMin = options.valueMin; - if (options.valueMax !== undefined) this.defaultValueMax = options.valueMax; - - if (options.cameraPosition !== undefined) cameraPosition = options.cameraPosition; - - if (cameraPosition !== undefined) { - this.camera.setArmRotation(cameraPosition.horizontal, cameraPosition.vertical); - this.camera.setArmLength(cameraPosition.distance); + else if (toPrecision[i] == "." || toPrecision[i] == ",") { + toPrecision = toPrecision.slice(0,i); + break; } - else { - this.camera.setArmRotation(1.0, 0.5); - this.camera.setArmLength(1.7); + else{ + break; } } - this._setBackgroundColor(options && options.backgroundColor); - - this.setSize(this.width, this.height); + return toPrecision; + }; - // re-load the data - if (this.dataTable) { - this.setData(this.dataTable); - } - // start animation when option is true - if (this.animationAutoStart && this.dataFilter) { - this.animationStart(); - } - }; /** - * Redraw the Graph. + * Snap a date to a rounded value. + * The snap intervals are dependent on the current scale and step. + * @param {Date} date the date to be snapped. + * @return {Date} snappedDate */ - Graph3d.prototype.redraw = function() { - if (this.dataPoints === undefined) { - throw 'Error: graph data not initialized'; - } - - this._resizeCanvas(); - this._resizeCenter(); - this._redrawSlider(); - this._redrawClear(); - this._redrawAxis(); - - if (this.style === Graph3d.STYLE.GRID || - this.style === Graph3d.STYLE.SURFACE) { - this._redrawDataGrid(); - } - else if (this.style === Graph3d.STYLE.LINE) { - this._redrawDataLine(); - } - else if (this.style === Graph3d.STYLE.BAR || - this.style === Graph3d.STYLE.BARCOLOR || - this.style === Graph3d.STYLE.BARSIZE) { - this._redrawDataBar(); - } - else { - // style is DOT, DOTLINE, DOTCOLOR, DOTSIZE - this._redrawDataDot(); - } + DataStep.prototype.snap = function(date) { - this._redrawInfo(); - this._redrawLegend(); }; /** - * Clear the canvas before redrawing + * Check if the current value is a major value (for example when the step + * is DAY, a major value is each first day of the MONTH) + * @return {boolean} true if current date is major, else false. */ - Graph3d.prototype._redrawClear = function() { - var canvas = this.frame.canvas; - var ctx = canvas.getContext('2d'); - - ctx.clearRect(0, 0, canvas.width, canvas.height); + DataStep.prototype.isMajor = function() { + return (this.current % (this.scale * this.majorSteps[this.stepIndex]) == 0); }; + module.exports = DataStep; - /** - * Redraw the legend showing the colors - */ - Graph3d.prototype._redrawLegend = function() { - var y; - if (this.style === Graph3d.STYLE.DOTCOLOR || - this.style === Graph3d.STYLE.DOTSIZE) { +/***/ }, +/* 7 */ +/***/ function(module, exports, __webpack_require__) { - var dotSize = this.frame.clientWidth * 0.02; + var Emitter = __webpack_require__(41); + var Hammer = __webpack_require__(50); + var util = __webpack_require__(1); + var DataSet = __webpack_require__(3); + var DataView = __webpack_require__(4); + var Range = __webpack_require__(8); + var TimeAxis = __webpack_require__(21); + var CurrentTime = __webpack_require__(13); + var CustomTime = __webpack_require__(14); + var LineGraph = __webpack_require__(20); - var widthMin, widthMax; - if (this.style === Graph3d.STYLE.DOTSIZE) { - widthMin = dotSize / 2; // px - widthMax = dotSize / 2 + dotSize * 2; // Todo: put this in one function - } - else { - widthMin = 20; // px - widthMax = 20; // px - } + /** + * Create a timeline visualization + * @param {HTMLElement} container + * @param {vis.DataSet | Array | google.visualization.DataTable} [items] + * @param {Object} [options] See Graph2d.setOptions for the available options. + * @constructor + */ + function Graph2d (container, items, options, groups) { + var me = this; + this.defaultOptions = { + start: null, + end: null, - var height = Math.max(this.frame.clientHeight * 0.25, 100); - var top = this.margin; - var right = this.frame.clientWidth - this.margin; - var left = right - widthMax; - var bottom = top + height; - } + autoResize: true, - var canvas = this.frame.canvas; - var ctx = canvas.getContext('2d'); - ctx.lineWidth = 1; - ctx.font = '14px arial'; // TODO: put in options + orientation: 'bottom', + width: null, + height: null, + maxHeight: null, + minHeight: null + }; + this.options = util.deepExtend({}, this.defaultOptions); - if (this.style === Graph3d.STYLE.DOTCOLOR) { - // draw the color bar - var ymin = 0; - var ymax = height; // Todo: make height customizable - for (y = ymin; y < ymax; y++) { - var f = (y - ymin) / (ymax - ymin); + // Create the DOM, props, and emitter + this._create(container); - //var width = (dotSize / 2 + (1-f) * dotSize * 2); // Todo: put this in one function - var hue = f * 240; - var color = this._hsv2rgb(hue, 1, 1); + // all components listed here will be repainted automatically + this.components = []; - ctx.strokeStyle = color; - ctx.beginPath(); - ctx.moveTo(left, top + y); - ctx.lineTo(right, top + y); - ctx.stroke(); + this.body = { + dom: this.dom, + domProps: this.props, + emitter: { + on: this.on.bind(this), + off: this.off.bind(this), + emit: this.emit.bind(this) + }, + util: { + snap: null, // will be specified after TimeAxis is created + toScreen: me._toScreen.bind(me), + toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width + toTime: me._toTime.bind(me), + toGlobalTime : me._toGlobalTime.bind(me) } + }; - ctx.strokeStyle = this.colorAxis; - ctx.strokeRect(left, top, widthMax, height); - } + // range + this.range = new Range(this.body); + this.components.push(this.range); + this.body.range = this.range; - if (this.style === Graph3d.STYLE.DOTSIZE) { - // draw border around color bar - ctx.strokeStyle = this.colorAxis; - ctx.fillStyle = this.colorDot; - ctx.beginPath(); - ctx.moveTo(left, top); - ctx.lineTo(right, top); - ctx.lineTo(right - widthMax + widthMin, bottom); - ctx.lineTo(left, bottom); - ctx.closePath(); - ctx.fill(); - ctx.stroke(); - } + // time axis + this.timeAxis = new TimeAxis(this.body); + this.components.push(this.timeAxis); + this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis); - if (this.style === Graph3d.STYLE.DOTCOLOR || - this.style === Graph3d.STYLE.DOTSIZE) { - // print values along the color bar - var gridLineLen = 5; // px - var step = new StepNumber(this.valueMin, this.valueMax, (this.valueMax-this.valueMin)/5, true); - step.start(); - if (step.getCurrent() < this.valueMin) { - step.next(); - } - while (!step.end()) { - y = bottom - (step.getCurrent() - this.valueMin) / (this.valueMax - this.valueMin) * height; + // current time bar + this.currentTime = new CurrentTime(this.body); + this.components.push(this.currentTime); - ctx.beginPath(); - ctx.moveTo(left - gridLineLen, y); - ctx.lineTo(left, y); - ctx.stroke(); + // custom time bar + // Note: time bar will be attached in this.setOptions when selected + this.customTime = new CustomTime(this.body); + this.components.push(this.customTime); - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - ctx.fillStyle = this.colorAxis; - ctx.fillText(step.getCurrent(), left - 2 * gridLineLen, y); + // item set + this.linegraph = new LineGraph(this.body); + this.components.push(this.linegraph); - step.next(); - } + this.itemsData = null; // DataSet + this.groupsData = null; // DataSet - ctx.textAlign = 'right'; - ctx.textBaseline = 'top'; - var label = this.legendLabel; - ctx.fillText(label, right, bottom + this.margin); + // apply options + if (options) { + this.setOptions(options); } - }; - - /** - * Redraw the filter - */ - Graph3d.prototype._redrawFilter = function() { - this.frame.filter.innerHTML = ''; - - if (this.dataFilter) { - var options = { - 'visible': this.showAnimationControls - }; - var slider = new Slider(this.frame.filter, options); - this.frame.filter.slider = slider; - - // TODO: css here is not nice here... - this.frame.filter.style.padding = '10px'; - //this.frame.filter.style.backgroundColor = '#EFEFEF'; - - slider.setValues(this.dataFilter.values); - slider.setPlayInterval(this.animationInterval); - - // create an event handler - var me = this; - var onchange = function () { - var index = slider.getIndex(); - me.dataFilter.selectValue(index); - me.dataPoints = me.dataFilter._getDataPoints(); + // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS! + if (groups) { + this.setGroups(groups); + } - me.redraw(); - }; - slider.setOnChangeCallback(onchange); + // create itemset + if (items) { + this.setItems(items); } else { - this.frame.filter.slider = undefined; - } - }; - - /** - * Redraw the slider - */ - Graph3d.prototype._redrawSlider = function() { - if ( this.frame.filter.slider !== undefined) { - this.frame.filter.slider.redraw(); + this.redraw(); } - }; + } + // turn Graph2d into an event emitter + Emitter(Graph2d.prototype); /** - * Redraw common information + * Create the main DOM for the Graph2d: a root panel containing left, right, + * top, bottom, content, and background panel. + * @param {Element} container The container element where the Graph2d will + * be attached. + * @private */ - Graph3d.prototype._redrawInfo = function() { - if (this.dataFilter) { - var canvas = this.frame.canvas; - var ctx = canvas.getContext('2d'); + Graph2d.prototype._create = function (container) { + this.dom = {}; - ctx.font = '14px arial'; // TODO: put in options - ctx.lineStyle = 'gray'; - ctx.fillStyle = 'gray'; - ctx.textAlign = 'left'; - ctx.textBaseline = 'top'; + this.dom.root = document.createElement('div'); + this.dom.background = document.createElement('div'); + this.dom.backgroundVertical = document.createElement('div'); + this.dom.backgroundHorizontalContainer = document.createElement('div'); + this.dom.centerContainer = document.createElement('div'); + this.dom.leftContainer = document.createElement('div'); + this.dom.rightContainer = document.createElement('div'); + this.dom.backgroundHorizontal = document.createElement('div'); + this.dom.center = document.createElement('div'); + this.dom.left = document.createElement('div'); + this.dom.right = document.createElement('div'); + this.dom.top = document.createElement('div'); + this.dom.bottom = document.createElement('div'); + this.dom.shadowTop = document.createElement('div'); + this.dom.shadowBottom = document.createElement('div'); + this.dom.shadowTopLeft = document.createElement('div'); + this.dom.shadowBottomLeft = document.createElement('div'); + this.dom.shadowTopRight = document.createElement('div'); + this.dom.shadowBottomRight = document.createElement('div'); - var x = this.margin; - var y = this.margin; - ctx.fillText(this.dataFilter.getLabel() + ': ' + this.dataFilter.getSelectedValue(), x, y); - } - }; + this.dom.background.className = 'vispanel background'; + this.dom.backgroundVertical.className = 'vispanel background vertical'; + this.dom.backgroundHorizontalContainer.className = 'vispanel background horizontal'; + this.dom.backgroundHorizontal.className = 'vispanel background horizontal'; + this.dom.centerContainer.className = 'vispanel center'; + this.dom.leftContainer.className = 'vispanel left'; + this.dom.rightContainer.className = 'vispanel right'; + this.dom.top.className = 'vispanel top'; + this.dom.bottom.className = 'vispanel bottom'; + this.dom.left.className = 'content'; + this.dom.center.className = 'content'; + this.dom.right.className = 'content'; + this.dom.shadowTop.className = 'shadow top'; + this.dom.shadowBottom.className = 'shadow bottom'; + this.dom.shadowTopLeft.className = 'shadow top'; + this.dom.shadowBottomLeft.className = 'shadow bottom'; + this.dom.shadowTopRight.className = 'shadow top'; + this.dom.shadowBottomRight.className = 'shadow bottom'; + this.dom.root.appendChild(this.dom.background); + this.dom.root.appendChild(this.dom.backgroundVertical); + this.dom.root.appendChild(this.dom.backgroundHorizontalContainer); + this.dom.root.appendChild(this.dom.centerContainer); + this.dom.root.appendChild(this.dom.leftContainer); + this.dom.root.appendChild(this.dom.rightContainer); + this.dom.root.appendChild(this.dom.top); + this.dom.root.appendChild(this.dom.bottom); - /** - * Redraw the axis - */ - Graph3d.prototype._redrawAxis = function() { - var canvas = this.frame.canvas, - ctx = canvas.getContext('2d'), - from, to, step, prettyStep, - text, xText, yText, zText, - offset, xOffset, yOffset, - xMin2d, xMax2d; + this.dom.backgroundHorizontalContainer.appendChild(this.dom.backgroundHorizontal); + this.dom.centerContainer.appendChild(this.dom.center); + this.dom.leftContainer.appendChild(this.dom.left); + this.dom.rightContainer.appendChild(this.dom.right); - // TODO: get the actual rendered style of the containerElement - //ctx.font = this.containerElement.style.font; - ctx.font = 24 / this.camera.getArmLength() + 'px arial'; + this.dom.centerContainer.appendChild(this.dom.shadowTop); + this.dom.centerContainer.appendChild(this.dom.shadowBottom); + this.dom.leftContainer.appendChild(this.dom.shadowTopLeft); + this.dom.leftContainer.appendChild(this.dom.shadowBottomLeft); + this.dom.rightContainer.appendChild(this.dom.shadowTopRight); + this.dom.rightContainer.appendChild(this.dom.shadowBottomRight); - // calculate the length for the short grid lines - var gridLenX = 0.025 / this.scale.x; - var gridLenY = 0.025 / this.scale.y; - var textMargin = 5 / this.camera.getArmLength(); // px - var armAngle = this.camera.getArmRotation().horizontal; + this.on('rangechange', this.redraw.bind(this)); + this.on('change', this.redraw.bind(this)); + this.on('touch', this._onTouch.bind(this)); + this.on('pinch', this._onPinch.bind(this)); + this.on('dragstart', this._onDragStart.bind(this)); + this.on('drag', this._onDrag.bind(this)); - // draw x-grid lines - ctx.lineWidth = 1; - prettyStep = (this.defaultXStep === undefined); - step = new StepNumber(this.xMin, this.xMax, this.xStep, prettyStep); - step.start(); - if (step.getCurrent() < this.xMin) { - step.next(); - } - while (!step.end()) { - var x = step.getCurrent(); + // create event listeners for all interesting events, these events will be + // emitted via emitter + this.hammer = Hammer(this.dom.root, { + prevent_default: true + }); + this.listeners = {}; - if (this.showGrid) { - from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin)); - to = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin)); - ctx.strokeStyle = this.colorGrid; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); - } - else { - from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin)); - to = this._convert3Dto2D(new Point3d(x, this.yMin+gridLenX, this.zMin)); - ctx.strokeStyle = this.colorAxis; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); + var me = this; + var events = [ + 'touch', 'pinch', + 'tap', 'doubletap', 'hold', + 'dragstart', 'drag', 'dragend', + 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed for Firefox + ]; + events.forEach(function (event) { + var listener = function () { + var args = [event].concat(Array.prototype.slice.call(arguments, 0)); + me.emit.apply(me, args); + }; + me.hammer.on(event, listener); + me.listeners[event] = listener; + }); - from = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin)); - to = this._convert3Dto2D(new Point3d(x, this.yMax-gridLenX, this.zMin)); - ctx.strokeStyle = this.colorAxis; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); - } + // size properties of each of the panels + this.props = { + root: {}, + background: {}, + centerContainer: {}, + leftContainer: {}, + rightContainer: {}, + center: {}, + left: {}, + right: {}, + top: {}, + bottom: {}, + border: {}, + scrollTop: 0, + scrollTopMin: 0 + }; + this.touch = {}; // store state information needed for touch events - yText = (Math.cos(armAngle) > 0) ? this.yMin : this.yMax; - text = this._convert3Dto2D(new Point3d(x, yText, this.zMin)); - if (Math.cos(armAngle * 2) > 0) { - ctx.textAlign = 'center'; - ctx.textBaseline = 'top'; - text.y += textMargin; - } - else if (Math.sin(armAngle * 2) < 0){ - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - } - else { - ctx.textAlign = 'left'; - ctx.textBaseline = 'middle'; - } - ctx.fillStyle = this.colorAxis; - ctx.fillText(' ' + step.getCurrent() + ' ', text.x, text.y); + // attach the root panel to the provided container + if (!container) throw new Error('No container provided'); + container.appendChild(this.dom.root); + }; - step.next(); - } + /** + * Destroy the Graph2d, clean up all DOM elements and event listeners. + */ + Graph2d.prototype.destroy = function () { + // unbind datasets + this.clear(); - // draw y-grid lines - ctx.lineWidth = 1; - prettyStep = (this.defaultYStep === undefined); - step = new StepNumber(this.yMin, this.yMax, this.yStep, prettyStep); - step.start(); - if (step.getCurrent() < this.yMin) { - step.next(); + // remove all event listeners + this.off(); + + // stop checking for changed size + this._stopAutoResize(); + + // remove from DOM + if (this.dom.root.parentNode) { + this.dom.root.parentNode.removeChild(this.dom.root); } - while (!step.end()) { - if (this.showGrid) { - from = this._convert3Dto2D(new Point3d(this.xMin, step.getCurrent(), this.zMin)); - to = this._convert3Dto2D(new Point3d(this.xMax, step.getCurrent(), this.zMin)); - ctx.strokeStyle = this.colorGrid; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); - } - else { - from = this._convert3Dto2D(new Point3d(this.xMin, step.getCurrent(), this.zMin)); - to = this._convert3Dto2D(new Point3d(this.xMin+gridLenY, step.getCurrent(), this.zMin)); - ctx.strokeStyle = this.colorAxis; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); + this.dom = null; - from = this._convert3Dto2D(new Point3d(this.xMax, step.getCurrent(), this.zMin)); - to = this._convert3Dto2D(new Point3d(this.xMax-gridLenY, step.getCurrent(), this.zMin)); - ctx.strokeStyle = this.colorAxis; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); + // cleanup hammer touch events + for (var event in this.listeners) { + if (this.listeners.hasOwnProperty(event)) { + delete this.listeners[event]; } + } + this.listeners = null; + this.hammer = null; - xText = (Math.sin(armAngle ) > 0) ? this.xMin : this.xMax; - text = this._convert3Dto2D(new Point3d(xText, step.getCurrent(), this.zMin)); - if (Math.cos(armAngle * 2) < 0) { - ctx.textAlign = 'center'; - ctx.textBaseline = 'top'; - text.y += textMargin; - } - else if (Math.sin(armAngle * 2) > 0){ - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - } - else { - ctx.textAlign = 'left'; - ctx.textBaseline = 'middle'; - } - ctx.fillStyle = this.colorAxis; - ctx.fillText(' ' + step.getCurrent() + ' ', text.x, text.y); + // give all components the opportunity to cleanup + this.components.forEach(function (component) { + component.destroy(); + }); - step.next(); + this.body = null; + }; + + /** + * Set options. Options will be passed to all components loaded in the Graph2d. + * @param {Object} [options] + * {String} orientation + * Vertical orientation for the Graph2d, + * can be 'bottom' (default) or 'top'. + * {String | Number} width + * Width for the timeline, a number in pixels or + * a css string like '1000px' or '75%'. '100%' by default. + * {String | Number} height + * Fixed height for the Graph2d, a number in pixels or + * a css string like '400px' or '75%'. If undefined, + * The Graph2d will automatically size such that + * its contents fit. + * {String | Number} minHeight + * Minimum height for the Graph2d, a number in pixels or + * a css string like '400px' or '75%'. + * {String | Number} maxHeight + * Maximum height for the Graph2d, a number in pixels or + * a css string like '400px' or '75%'. + * {Number | Date | String} start + * Start date for the visible window + * {Number | Date | String} end + * End date for the visible window + */ + Graph2d.prototype.setOptions = function (options) { + if (options) { + // copy the known options + var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'orientation']; + util.selectiveExtend(fields, this.options, options); + + // enable/disable autoResize + this._initAutoResize(); } - // draw z-grid lines and axis - ctx.lineWidth = 1; - prettyStep = (this.defaultZStep === undefined); - step = new StepNumber(this.zMin, this.zMax, this.zStep, prettyStep); - step.start(); - if (step.getCurrent() < this.zMin) { - step.next(); + // propagate options to all components + this.components.forEach(function (component) { + component.setOptions(options); + }); + + // TODO: remove deprecation error one day (deprecated since version 0.8.0) + if (options && options.order) { + throw new Error('Option order is deprecated. There is no replacement for this feature.'); } - xText = (Math.cos(armAngle ) > 0) ? this.xMin : this.xMax; - yText = (Math.sin(armAngle ) < 0) ? this.yMin : this.yMax; - while (!step.end()) { - // TODO: make z-grid lines really 3d? - from = this._convert3Dto2D(new Point3d(xText, yText, step.getCurrent())); - ctx.strokeStyle = this.colorAxis; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(from.x - textMargin, from.y); - ctx.stroke(); - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - ctx.fillStyle = this.colorAxis; - ctx.fillText(step.getCurrent() + ' ', from.x - 5, from.y); + // redraw everything + this.redraw(); + }; - step.next(); + /** + * Set a custom time bar + * @param {Date} time + */ + Graph2d.prototype.setCustomTime = function (time) { + if (!this.customTime) { + throw new Error('Cannot get custom time: Custom time bar is not enabled'); } - ctx.lineWidth = 1; - from = this._convert3Dto2D(new Point3d(xText, yText, this.zMin)); - to = this._convert3Dto2D(new Point3d(xText, yText, this.zMax)); - ctx.strokeStyle = this.colorAxis; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); - // draw x-axis - ctx.lineWidth = 1; - // line at yMin - xMin2d = this._convert3Dto2D(new Point3d(this.xMin, this.yMin, this.zMin)); - xMax2d = this._convert3Dto2D(new Point3d(this.xMax, this.yMin, this.zMin)); - ctx.strokeStyle = this.colorAxis; - ctx.beginPath(); - ctx.moveTo(xMin2d.x, xMin2d.y); - ctx.lineTo(xMax2d.x, xMax2d.y); - ctx.stroke(); - // line at ymax - xMin2d = this._convert3Dto2D(new Point3d(this.xMin, this.yMax, this.zMin)); - xMax2d = this._convert3Dto2D(new Point3d(this.xMax, this.yMax, this.zMin)); - ctx.strokeStyle = this.colorAxis; - ctx.beginPath(); - ctx.moveTo(xMin2d.x, xMin2d.y); - ctx.lineTo(xMax2d.x, xMax2d.y); - ctx.stroke(); + this.customTime.setCustomTime(time); + }; - // draw y-axis - ctx.lineWidth = 1; - // line at xMin - from = this._convert3Dto2D(new Point3d(this.xMin, this.yMin, this.zMin)); - to = this._convert3Dto2D(new Point3d(this.xMin, this.yMax, this.zMin)); - ctx.strokeStyle = this.colorAxis; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); - // line at xMax - from = this._convert3Dto2D(new Point3d(this.xMax, this.yMin, this.zMin)); - to = this._convert3Dto2D(new Point3d(this.xMax, this.yMax, this.zMin)); - ctx.strokeStyle = this.colorAxis; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); - - // draw x-label - var xLabel = this.xLabel; - if (xLabel.length > 0) { - yOffset = 0.1 / this.scale.y; - xText = (this.xMin + this.xMax) / 2; - yText = (Math.cos(armAngle) > 0) ? this.yMin - yOffset: this.yMax + yOffset; - text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin)); - if (Math.cos(armAngle * 2) > 0) { - ctx.textAlign = 'center'; - ctx.textBaseline = 'top'; - } - else if (Math.sin(armAngle * 2) < 0){ - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - } - else { - ctx.textAlign = 'left'; - ctx.textBaseline = 'middle'; - } - ctx.fillStyle = this.colorAxis; - ctx.fillText(xLabel, text.x, text.y); - } - - // draw y-label - var yLabel = this.yLabel; - if (yLabel.length > 0) { - xOffset = 0.1 / this.scale.x; - xText = (Math.sin(armAngle ) > 0) ? this.xMin - xOffset : this.xMax + xOffset; - yText = (this.yMin + this.yMax) / 2; - text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin)); - if (Math.cos(armAngle * 2) < 0) { - ctx.textAlign = 'center'; - ctx.textBaseline = 'top'; - } - else if (Math.sin(armAngle * 2) > 0){ - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - } - else { - ctx.textAlign = 'left'; - ctx.textBaseline = 'middle'; - } - ctx.fillStyle = this.colorAxis; - ctx.fillText(yLabel, text.x, text.y); + /** + * Retrieve the current custom time. + * @return {Date} customTime + */ + Graph2d.prototype.getCustomTime = function() { + if (!this.customTime) { + throw new Error('Cannot get custom time: Custom time bar is not enabled'); } - // draw z-label - var zLabel = this.zLabel; - if (zLabel.length > 0) { - offset = 30; // pixels. // TODO: relate to the max width of the values on the z axis? - xText = (Math.cos(armAngle ) > 0) ? this.xMin : this.xMax; - yText = (Math.sin(armAngle ) < 0) ? this.yMin : this.yMax; - zText = (this.zMin + this.zMax) / 2; - text = this._convert3Dto2D(new Point3d(xText, yText, zText)); - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - ctx.fillStyle = this.colorAxis; - ctx.fillText(zLabel, text.x - offset, text.y); - } + return this.customTime.getCustomTime(); }; /** - * Calculate the color based on the given value. - * @param {Number} H Hue, a value be between 0 and 360 - * @param {Number} S Saturation, a value between 0 and 1 - * @param {Number} V Value, a value between 0 and 1 + * Set items + * @param {vis.DataSet | Array | google.visualization.DataTable | null} items */ - Graph3d.prototype._hsv2rgb = function(H, S, V) { - var R, G, B, C, Hi, X; + Graph2d.prototype.setItems = function(items) { + var initialLoad = (this.itemsData == null); - C = V * S; - Hi = Math.floor(H/60); // hi = 0,1,2,3,4,5 - X = C * (1 - Math.abs(((H/60) % 2) - 1)); + // convert to type DataSet when needed + var newDataSet; + if (!items) { + newDataSet = null; + } + else if (items instanceof DataSet || items instanceof DataView) { + newDataSet = items; + } + else { + // turn an array into a dataset + newDataSet = new DataSet(items, { + type: { + start: 'Date', + end: 'Date' + } + }); + } - switch (Hi) { - case 0: R = C; G = X; B = 0; break; - case 1: R = X; G = C; B = 0; break; - case 2: R = 0; G = C; B = X; break; - case 3: R = 0; G = X; B = C; break; - case 4: R = X; G = 0; B = C; break; - case 5: R = C; G = 0; B = X; break; + // set items + this.itemsData = newDataSet; + this.linegraph && this.linegraph.setItems(newDataSet); - default: R = 0; G = 0; B = 0; break; - } + if (initialLoad && ('start' in this.options || 'end' in this.options)) { + this.fit(); - return 'RGB(' + parseInt(R*255) + ',' + parseInt(G*255) + ',' + parseInt(B*255) + ')'; - }; + var start = ('start' in this.options) ? util.convert(this.options.start, 'Date') : null; + var end = ('end' in this.options) ? util.convert(this.options.end, 'Date') : null; + this.setWindow(start, end); + } + }; /** - * Draw all datapoints as a grid - * This function can be used when the style is 'grid' + * Set groups + * @param {vis.DataSet | Array | google.visualization.DataTable} groups */ - Graph3d.prototype._redrawDataGrid = function() { - var canvas = this.frame.canvas, - ctx = canvas.getContext('2d'), - point, right, top, cross, - i, - topSideVisible, fillStyle, strokeStyle, lineWidth, - h, s, v, zAvg; - - - if (this.dataPoints === undefined || this.dataPoints.length <= 0) - return; // TODO: throw exception? + Graph2d.prototype.setGroups = function(groups) { + // convert to type DataSet when needed + var newDataSet; + if (!groups) { + newDataSet = null; + } + else if (groups instanceof DataSet || groups instanceof DataView) { + newDataSet = groups; + } + else { + // turn an array into a dataset + newDataSet = new DataSet(groups); + } - // calculate the translations and screen position of all points - for (i = 0; i < this.dataPoints.length; i++) { - var trans = this._convertPointToTranslation(this.dataPoints[i].point); - var screen = this._convertTranslationToScreen(trans); + this.groupsData = newDataSet; + this.linegraph.setGroups(newDataSet); + }; - this.dataPoints[i].trans = trans; - this.dataPoints[i].screen = screen; + /** + * Clear the Graph2d. By Default, items, groups and options are cleared. + * Example usage: + * + * timeline.clear(); // clear items, groups, and options + * timeline.clear({options: true}); // clear options only + * + * @param {Object} [what] Optionally specify what to clear. By default: + * {items: true, groups: true, options: true} + */ + Graph2d.prototype.clear = function(what) { + // clear items + if (!what || what.items) { + this.setItems(null); + } - // calculate the translation of the point at the bottom (needed for sorting) - var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom); - this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z; + // clear groups + if (!what || what.groups) { + this.setGroups(null); } - // sort the points on depth of their (x,y) position (not on z) - var sortDepth = function (a, b) { - return b.dist - a.dist; - }; - this.dataPoints.sort(sortDepth); + // clear options of timeline and of each of the components + if (!what || what.options) { + this.components.forEach(function (component) { + component.setOptions(component.defaultOptions); + }); - if (this.style === Graph3d.STYLE.SURFACE) { - for (i = 0; i < this.dataPoints.length; i++) { - point = this.dataPoints[i]; - right = this.dataPoints[i].pointRight; - top = this.dataPoints[i].pointTop; - cross = this.dataPoints[i].pointCross; + this.setOptions(this.defaultOptions); // this will also do a redraw + } + }; - if (point !== undefined && right !== undefined && top !== undefined && cross !== undefined) { + /** + * Set Graph2d window such that it fits all items + */ + Graph2d.prototype.fit = function() { + // apply the data range as range + var dataRange = this.getItemRange(); - if (this.showGrayBottom || this.showShadow) { - // calculate the cross product of the two vectors from center - // to left and right, in order to know whether we are looking at the - // bottom or at the top side. We can also use the cross product - // for calculating light intensity - var aDiff = Point3d.subtract(cross.trans, point.trans); - var bDiff = Point3d.subtract(top.trans, right.trans); - var crossproduct = Point3d.crossProduct(aDiff, bDiff); - var len = crossproduct.length(); - // FIXME: there is a bug with determining the surface side (shadow or colored) + // add 5% space on both sides + var start = dataRange.min; + var end = dataRange.max; + if (start != null && end != null) { + var interval = (end.valueOf() - start.valueOf()); + if (interval <= 0) { + // prevent an empty interval + interval = 24 * 60 * 60 * 1000; // 1 day + } + start = new Date(start.valueOf() - interval * 0.05); + end = new Date(end.valueOf() + interval * 0.05); + } - topSideVisible = (crossproduct.z > 0); - } - else { - topSideVisible = true; - } + // skip range set if there is no start and end date + if (start === null && end === null) { + return; + } - if (topSideVisible) { - // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 - zAvg = (point.point.z + right.point.z + top.point.z + cross.point.z) / 4; - h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; - s = 1; // saturation + this.range.setRange(start, end); + }; - if (this.showShadow) { - v = Math.min(1 + (crossproduct.x / len) / 2, 1); // value. TODO: scale - fillStyle = this._hsv2rgb(h, s, v); - strokeStyle = fillStyle; - } - else { - v = 1; - fillStyle = this._hsv2rgb(h, s, v); - strokeStyle = this.colorAxis; - } - } - else { - fillStyle = 'gray'; - strokeStyle = this.colorAxis; - } - lineWidth = 0.5; - - ctx.lineWidth = lineWidth; - ctx.fillStyle = fillStyle; - ctx.strokeStyle = strokeStyle; - ctx.beginPath(); - ctx.moveTo(point.screen.x, point.screen.y); - ctx.lineTo(right.screen.x, right.screen.y); - ctx.lineTo(cross.screen.x, cross.screen.y); - ctx.lineTo(top.screen.x, top.screen.y); - ctx.closePath(); - ctx.fill(); - ctx.stroke(); - } - } - } - else { // grid style - for (i = 0; i < this.dataPoints.length; i++) { - point = this.dataPoints[i]; - right = this.dataPoints[i].pointRight; - top = this.dataPoints[i].pointTop; - - if (point !== undefined) { - if (this.showPerspective) { - lineWidth = 2 / -point.trans.z; - } - else { - lineWidth = 2 * -(this.eye.z / this.camera.getArmLength()); - } - } + /** + * Get the data range of the item set. + * @returns {{min: Date, max: Date}} range A range with a start and end Date. + * When no minimum is found, min==null + * When no maximum is found, max==null + */ + Graph2d.prototype.getItemRange = function() { + // calculate min from start filed + var itemsData = this.itemsData, + min = null, + max = null; - if (point !== undefined && right !== undefined) { - // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 - zAvg = (point.point.z + right.point.z) / 2; - h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; + if (itemsData) { + // calculate the minimum value of the field 'start' + var minItem = itemsData.min('start'); + min = minItem ? util.convert(minItem.start, 'Date').valueOf() : null; + // Note: we convert first to Date and then to number because else + // a conversion from ISODate to Number will fail - ctx.lineWidth = lineWidth; - ctx.strokeStyle = this._hsv2rgb(h, 1, 1); - ctx.beginPath(); - ctx.moveTo(point.screen.x, point.screen.y); - ctx.lineTo(right.screen.x, right.screen.y); - ctx.stroke(); + // calculate maximum value of fields 'start' and 'end' + var maxStartItem = itemsData.max('start'); + if (maxStartItem) { + max = util.convert(maxStartItem.start, 'Date').valueOf(); + } + var maxEndItem = itemsData.max('end'); + if (maxEndItem) { + if (max == null) { + max = util.convert(maxEndItem.end, 'Date').valueOf(); } - - if (point !== undefined && top !== undefined) { - // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 - zAvg = (point.point.z + top.point.z) / 2; - h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; - - ctx.lineWidth = lineWidth; - ctx.strokeStyle = this._hsv2rgb(h, 1, 1); - ctx.beginPath(); - ctx.moveTo(point.screen.x, point.screen.y); - ctx.lineTo(top.screen.x, top.screen.y); - ctx.stroke(); + else { + max = Math.max(max, util.convert(maxEndItem.end, 'Date').valueOf()); } } } - }; + return { + min: (min != null) ? new Date(min) : null, + max: (max != null) ? new Date(max) : null + }; + }; /** - * Draw all datapoints as dots. - * This function can be used when the style is 'dot' or 'dot-line' + * Set the visible window. Both parameters are optional, you can change only + * start or only end. Syntax: + * + * TimeLine.setWindow(start, end) + * TimeLine.setWindow(range) + * + * Where start and end can be a Date, number, or string, and range is an + * object with properties start and end. + * + * @param {Date | Number | String | Object} [start] Start date of visible window + * @param {Date | Number | String} [end] End date of visible window */ - Graph3d.prototype._redrawDataDot = function() { - var canvas = this.frame.canvas; - var ctx = canvas.getContext('2d'); - var i; - - if (this.dataPoints === undefined || this.dataPoints.length <= 0) - return; // TODO: throw exception? - - // calculate the translations of all points - for (i = 0; i < this.dataPoints.length; i++) { - var trans = this._convertPointToTranslation(this.dataPoints[i].point); - var screen = this._convertTranslationToScreen(trans); - this.dataPoints[i].trans = trans; - this.dataPoints[i].screen = screen; - - // calculate the distance from the point at the bottom to the camera - var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom); - this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z; + Graph2d.prototype.setWindow = function(start, end) { + if (arguments.length == 1) { + var range = arguments[0]; + this.range.setRange(range.start, range.end); } - - // order the translated points by depth - var sortDepth = function (a, b) { - return b.dist - a.dist; - }; - this.dataPoints.sort(sortDepth); - - // draw the datapoints as colored circles - var dotSize = this.frame.clientWidth * 0.02; // px - for (i = 0; i < this.dataPoints.length; i++) { - var point = this.dataPoints[i]; - - if (this.style === Graph3d.STYLE.DOTLINE) { - // draw a vertical line from the bottom to the graph value - //var from = this._convert3Dto2D(new Point3d(point.point.x, point.point.y, this.zMin)); - var from = this._convert3Dto2D(point.bottom); - ctx.lineWidth = 1; - ctx.strokeStyle = this.colorGrid; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(point.screen.x, point.screen.y); - ctx.stroke(); - } - - // calculate radius for the circle - var size; - if (this.style === Graph3d.STYLE.DOTSIZE) { - size = dotSize/2 + 2*dotSize * (point.point.value - this.valueMin) / (this.valueMax - this.valueMin); - } - else { - size = dotSize; - } - - var radius; - if (this.showPerspective) { - radius = size / -point.trans.z; - } - else { - radius = size * -(this.eye.z / this.camera.getArmLength()); - } - if (radius < 0) { - radius = 0; - } - - var hue, color, borderColor; - if (this.style === Graph3d.STYLE.DOTCOLOR ) { - // calculate the color based on the value - hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240; - color = this._hsv2rgb(hue, 1, 1); - borderColor = this._hsv2rgb(hue, 1, 0.8); - } - else if (this.style === Graph3d.STYLE.DOTSIZE) { - color = this.colorDot; - borderColor = this.colorDotBorder; - } - else { - // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 - hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240; - color = this._hsv2rgb(hue, 1, 1); - borderColor = this._hsv2rgb(hue, 1, 0.8); - } - - // draw the circle - ctx.lineWidth = 1.0; - ctx.strokeStyle = borderColor; - ctx.fillStyle = color; - ctx.beginPath(); - ctx.arc(point.screen.x, point.screen.y, radius, 0, Math.PI*2, true); - ctx.fill(); - ctx.stroke(); + else { + this.range.setRange(start, end); } }; /** - * Draw all datapoints as bars. - * This function can be used when the style is 'bar', 'bar-color', or 'bar-size' + * Get the visible window + * @return {{start: Date, end: Date}} Visible range */ - Graph3d.prototype._redrawDataBar = function() { - var canvas = this.frame.canvas; - var ctx = canvas.getContext('2d'); - var i, j, surface, corners; + Graph2d.prototype.getWindow = function() { + var range = this.range.getRange(); + return { + start: new Date(range.start), + end: new Date(range.end) + }; + }; - if (this.dataPoints === undefined || this.dataPoints.length <= 0) - return; // TODO: throw exception? + /** + * Force a redraw of the Graph2d. Can be useful to manually redraw when + * option autoResize=false + */ + Graph2d.prototype.redraw = function() { + var resized = false, + options = this.options, + props = this.props, + dom = this.dom; - // calculate the translations of all points - for (i = 0; i < this.dataPoints.length; i++) { - var trans = this._convertPointToTranslation(this.dataPoints[i].point); - var screen = this._convertTranslationToScreen(trans); - this.dataPoints[i].trans = trans; - this.dataPoints[i].screen = screen; + if (!dom) return; // when destroyed - // calculate the distance from the point at the bottom to the camera - var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom); - this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z; - } + // update class names + dom.root.className = 'vis timeline root ' + options.orientation; - // order the translated points by depth - var sortDepth = function (a, b) { - return b.dist - a.dist; - }; - this.dataPoints.sort(sortDepth); + // update root width and height options + dom.root.style.maxHeight = util.option.asSize(options.maxHeight, ''); + dom.root.style.minHeight = util.option.asSize(options.minHeight, ''); + dom.root.style.width = util.option.asSize(options.width, ''); - // draw the datapoints as bars - var xWidth = this.xBarWidth / 2; - var yWidth = this.yBarWidth / 2; - for (i = 0; i < this.dataPoints.length; i++) { - var point = this.dataPoints[i]; - - // determine color - var hue, color, borderColor; - if (this.style === Graph3d.STYLE.BARCOLOR ) { - // calculate the color based on the value - hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240; - color = this._hsv2rgb(hue, 1, 1); - borderColor = this._hsv2rgb(hue, 1, 0.8); - } - else if (this.style === Graph3d.STYLE.BARSIZE) { - color = this.colorDot; - borderColor = this.colorDotBorder; - } - else { - // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 - hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240; - color = this._hsv2rgb(hue, 1, 1); - borderColor = this._hsv2rgb(hue, 1, 0.8); - } - - // calculate size for the bar - if (this.style === Graph3d.STYLE.BARSIZE) { - xWidth = (this.xBarWidth / 2) * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2); - yWidth = (this.yBarWidth / 2) * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2); - } - - // calculate all corner points - var me = this; - var point3d = point.point; - var top = [ - {point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, point3d.z)}, - {point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, point3d.z)}, - {point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, point3d.z)}, - {point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, point3d.z)} - ]; - var bottom = [ - {point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, this.zMin)}, - {point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, this.zMin)}, - {point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, this.zMin)}, - {point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, this.zMin)} - ]; - - // calculate screen location of the points - top.forEach(function (obj) { - obj.screen = me._convert3Dto2D(obj.point); - }); - bottom.forEach(function (obj) { - obj.screen = me._convert3Dto2D(obj.point); - }); - - // create five sides, calculate both corner points and center points - var surfaces = [ - {corners: top, center: Point3d.avg(bottom[0].point, bottom[2].point)}, - {corners: [top[0], top[1], bottom[1], bottom[0]], center: Point3d.avg(bottom[1].point, bottom[0].point)}, - {corners: [top[1], top[2], bottom[2], bottom[1]], center: Point3d.avg(bottom[2].point, bottom[1].point)}, - {corners: [top[2], top[3], bottom[3], bottom[2]], center: Point3d.avg(bottom[3].point, bottom[2].point)}, - {corners: [top[3], top[0], bottom[0], bottom[3]], center: Point3d.avg(bottom[0].point, bottom[3].point)} - ]; - point.surfaces = surfaces; - - // calculate the distance of each of the surface centers to the camera - for (j = 0; j < surfaces.length; j++) { - surface = surfaces[j]; - var transCenter = this._convertPointToTranslation(surface.center); - surface.dist = this.showPerspective ? transCenter.length() : -transCenter.z; - // TODO: this dept calculation doesn't work 100% of the cases due to perspective, - // but the current solution is fast/simple and works in 99.9% of all cases - // the issue is visible in example 14, with graph.setCameraPosition({horizontal: 2.97, vertical: 0.5, distance: 0.9}) - } - - // order the surfaces by their (translated) depth - surfaces.sort(function (a, b) { - var diff = b.dist - a.dist; - if (diff) return diff; + // calculate border widths + props.border.left = (dom.centerContainer.offsetWidth - dom.centerContainer.clientWidth) / 2; + props.border.right = props.border.left; + props.border.top = (dom.centerContainer.offsetHeight - dom.centerContainer.clientHeight) / 2; + props.border.bottom = props.border.top; + var borderRootHeight= dom.root.offsetHeight - dom.root.clientHeight; + var borderRootWidth = dom.root.offsetWidth - dom.root.clientWidth; - // if equal depth, sort the top surface last - if (a.corners === top) return 1; - if (b.corners === top) return -1; + // calculate the heights. If any of the side panels is empty, we set the height to + // minus the border width, such that the border will be invisible + props.center.height = dom.center.offsetHeight; + props.left.height = dom.left.offsetHeight; + props.right.height = dom.right.offsetHeight; + props.top.height = dom.top.clientHeight || -props.border.top; + props.bottom.height = dom.bottom.clientHeight || -props.border.bottom; - // both are equal - return 0; - }); + // TODO: compensate borders when any of the panels is empty. - // draw the ordered surfaces - ctx.lineWidth = 1; - ctx.strokeStyle = borderColor; - ctx.fillStyle = color; - // NOTE: we start at j=2 instead of j=0 as we don't need to draw the two surfaces at the backside - for (j = 2; j < surfaces.length; j++) { - surface = surfaces[j]; - corners = surface.corners; - ctx.beginPath(); - ctx.moveTo(corners[3].screen.x, corners[3].screen.y); - ctx.lineTo(corners[0].screen.x, corners[0].screen.y); - ctx.lineTo(corners[1].screen.x, corners[1].screen.y); - ctx.lineTo(corners[2].screen.x, corners[2].screen.y); - ctx.lineTo(corners[3].screen.x, corners[3].screen.y); - ctx.fill(); - ctx.stroke(); - } - } - }; + // apply auto height + // TODO: only calculate autoHeight when needed (else we cause an extra reflow/repaint of the DOM) + var contentHeight = Math.max(props.left.height, props.center.height, props.right.height); + var autoHeight = props.top.height + contentHeight + props.bottom.height + + borderRootHeight + props.border.top + props.border.bottom; + dom.root.style.height = util.option.asSize(options.height, autoHeight + 'px'); + // calculate heights of the content panels + props.root.height = dom.root.offsetHeight; + props.background.height = props.root.height - borderRootHeight; + var containerHeight = props.root.height - props.top.height - props.bottom.height - + borderRootHeight; + props.centerContainer.height = containerHeight; + props.leftContainer.height = containerHeight; + props.rightContainer.height = props.leftContainer.height; - /** - * Draw a line through all datapoints. - * This function can be used when the style is 'line' - */ - Graph3d.prototype._redrawDataLine = function() { - var canvas = this.frame.canvas, - ctx = canvas.getContext('2d'), - point, i; + // calculate the widths of the panels + props.root.width = dom.root.offsetWidth; + props.background.width = props.root.width - borderRootWidth; + props.left.width = dom.leftContainer.clientWidth || -props.border.left; + props.leftContainer.width = props.left.width; + props.right.width = dom.rightContainer.clientWidth || -props.border.right; + props.rightContainer.width = props.right.width; + var centerWidth = props.root.width - props.left.width - props.right.width - borderRootWidth; + props.center.width = centerWidth; + props.centerContainer.width = centerWidth; + props.top.width = centerWidth; + props.bottom.width = centerWidth; - if (this.dataPoints === undefined || this.dataPoints.length <= 0) - return; // TODO: throw exception? + // resize the panels + dom.background.style.height = props.background.height + 'px'; + dom.backgroundVertical.style.height = props.background.height + 'px'; + dom.backgroundHorizontalContainer.style.height = props.centerContainer.height + 'px'; + dom.centerContainer.style.height = props.centerContainer.height + 'px'; + dom.leftContainer.style.height = props.leftContainer.height + 'px'; + dom.rightContainer.style.height = props.rightContainer.height + 'px'; - // calculate the translations of all points - for (i = 0; i < this.dataPoints.length; i++) { - var trans = this._convertPointToTranslation(this.dataPoints[i].point); - var screen = this._convertTranslationToScreen(trans); + dom.background.style.width = props.background.width + 'px'; + dom.backgroundVertical.style.width = props.centerContainer.width + 'px'; + dom.backgroundHorizontalContainer.style.width = props.background.width + 'px'; + dom.backgroundHorizontal.style.width = props.background.width + 'px'; + dom.centerContainer.style.width = props.center.width + 'px'; + dom.top.style.width = props.top.width + 'px'; + dom.bottom.style.width = props.bottom.width + 'px'; - this.dataPoints[i].trans = trans; - this.dataPoints[i].screen = screen; - } + // reposition the panels + dom.background.style.left = '0'; + dom.background.style.top = '0'; + dom.backgroundVertical.style.left = props.left.width + 'px'; + dom.backgroundVertical.style.top = '0'; + dom.backgroundHorizontalContainer.style.left = '0'; + dom.backgroundHorizontalContainer.style.top = props.top.height + 'px'; + dom.centerContainer.style.left = props.left.width + 'px'; + dom.centerContainer.style.top = props.top.height + 'px'; + dom.leftContainer.style.left = '0'; + dom.leftContainer.style.top = props.top.height + 'px'; + dom.rightContainer.style.left = (props.left.width + props.center.width) + 'px'; + dom.rightContainer.style.top = props.top.height + 'px'; + dom.top.style.left = props.left.width + 'px'; + dom.top.style.top = '0'; + dom.bottom.style.left = props.left.width + 'px'; + dom.bottom.style.top = (props.top.height + props.centerContainer.height) + 'px'; - // start the line - if (this.dataPoints.length > 0) { - point = this.dataPoints[0]; + // update the scrollTop, feasible range for the offset can be changed + // when the height of the Graph2d or of the contents of the center changed + this._updateScrollTop(); - ctx.lineWidth = 1; // TODO: make customizable - ctx.strokeStyle = 'blue'; // TODO: make customizable - ctx.beginPath(); - ctx.moveTo(point.screen.x, point.screen.y); + // reposition the scrollable contents + var offset = this.props.scrollTop; + if (options.orientation == 'bottom') { + offset += Math.max(this.props.centerContainer.height - this.props.center.height - + this.props.border.top - this.props.border.bottom, 0); } + dom.center.style.left = '0'; + dom.center.style.top = offset + 'px'; + dom.backgroundHorizontal.style.left = '0'; + dom.backgroundHorizontal.style.top = offset + 'px'; + dom.left.style.left = '0'; + dom.left.style.top = offset + 'px'; + dom.right.style.left = '0'; + dom.right.style.top = offset + 'px'; - // draw the datapoints as colored circles - for (i = 1; i < this.dataPoints.length; i++) { - point = this.dataPoints[i]; - ctx.lineTo(point.screen.x, point.screen.y); - } + // show shadows when vertical scrolling is available + var visibilityTop = this.props.scrollTop == 0 ? 'hidden' : ''; + var visibilityBottom = this.props.scrollTop == this.props.scrollTopMin ? 'hidden' : ''; + dom.shadowTop.style.visibility = visibilityTop; + dom.shadowBottom.style.visibility = visibilityBottom; + dom.shadowTopLeft.style.visibility = visibilityTop; + dom.shadowBottomLeft.style.visibility = visibilityBottom; + dom.shadowTopRight.style.visibility = visibilityTop; + dom.shadowBottomRight.style.visibility = visibilityBottom; - // finish the line - if (this.dataPoints.length > 0) { - ctx.stroke(); + // redraw all components + this.components.forEach(function (component) { + resized = component.redraw() || resized; + }); + if (resized) { + // keep redrawing until all sizes are settled + this.redraw(); } }; /** - * Start a moving operation inside the provided parent element - * @param {Event} event The event that occurred (required for - * retrieving the mouse position) + * Convert a position on screen (pixels) to a datetime + * @param {int} x Position on the screen in pixels + * @return {Date} time The datetime the corresponds with given position x + * @private */ - Graph3d.prototype._onMouseDown = function(event) { - event = event || window.event; + // TODO: move this function to Range + Graph2d.prototype._toTime = function(x) { + var conversion = this.range.conversion(this.props.center.width); + return new Date(x / conversion.scale + conversion.offset); + }; - // check if mouse is still down (may be up when focus is lost for example - // in an iframe) - if (this.leftButtonDown) { - this._onMouseUp(event); - } - - // only react on left mouse button down - this.leftButtonDown = event.which ? (event.which === 1) : (event.button === 1); - if (!this.leftButtonDown && !this.touchDown) return; - - // get mouse position (different code for IE and all other browsers) - this.startMouseX = getMouseX(event); - this.startMouseY = getMouseY(event); - - this.startStart = new Date(this.start); - this.startEnd = new Date(this.end); - this.startArmRotation = this.camera.getArmRotation(); - - this.frame.style.cursor = 'move'; - - // add event listeners to handle moving the contents - // we store the function onmousemove and onmouseup in the graph, so we can - // remove the eventlisteners lateron in the function mouseUp() - var me = this; - this.onmousemove = function (event) {me._onMouseMove(event);}; - this.onmouseup = function (event) {me._onMouseUp(event);}; - G3DaddEventListener(document, 'mousemove', me.onmousemove); - G3DaddEventListener(document, 'mouseup', me.onmouseup); - G3DpreventDefault(event); + /** + * Convert a datetime (Date object) into a position on the root + * This is used to get the pixel density estimate for the screen, not the center panel + * @param {Date} time A date + * @return {int} x The position on root in pixels which corresponds + * with the given date. + * @private + */ + // TODO: move this function to Range + Graph2d.prototype._toGlobalTime = function(x) { + var conversion = this.range.conversion(this.props.root.width); + return new Date(x / conversion.scale + conversion.offset); }; - /** - * Perform moving operating. - * This function activated from within the funcion Graph.mouseDown(). - * @param {Event} event Well, eehh, the event + * Convert a datetime (Date object) into a position on the screen + * @param {Date} time A date + * @return {int} x The position on the screen in pixels which corresponds + * with the given date. + * @private */ - Graph3d.prototype._onMouseMove = function (event) { - event = event || window.event; - - // calculate change in mouse position - var diffX = parseFloat(getMouseX(event)) - this.startMouseX; - var diffY = parseFloat(getMouseY(event)) - this.startMouseY; - - var horizontalNew = this.startArmRotation.horizontal + diffX / 200; - var verticalNew = this.startArmRotation.vertical + diffY / 200; - - var snapAngle = 4; // degrees - var snapValue = Math.sin(snapAngle / 360 * 2 * Math.PI); - - // snap horizontally to nice angles at 0pi, 0.5pi, 1pi, 1.5pi, etc... - // the -0.001 is to take care that the vertical axis is always drawn at the left front corner - if (Math.abs(Math.sin(horizontalNew)) < snapValue) { - horizontalNew = Math.round((horizontalNew / Math.PI)) * Math.PI - 0.001; - } - if (Math.abs(Math.cos(horizontalNew)) < snapValue) { - horizontalNew = (Math.round((horizontalNew/ Math.PI - 0.5)) + 0.5) * Math.PI - 0.001; - } - - // snap vertically to nice angles - if (Math.abs(Math.sin(verticalNew)) < snapValue) { - verticalNew = Math.round((verticalNew / Math.PI)) * Math.PI; - } - if (Math.abs(Math.cos(verticalNew)) < snapValue) { - verticalNew = (Math.round((verticalNew/ Math.PI - 0.5)) + 0.5) * Math.PI; - } - - this.camera.setArmRotation(horizontalNew, verticalNew); - this.redraw(); - - // fire a cameraPositionChange event - var parameters = this.getCameraPosition(); - this.emit('cameraPositionChange', parameters); - - G3DpreventDefault(event); + // TODO: move this function to Range + Graph2d.prototype._toScreen = function(time) { + var conversion = this.range.conversion(this.props.center.width); + return (time.valueOf() - conversion.offset) * conversion.scale; }; /** - * Stop moving operating. - * This function activated from within the funcion Graph.mouseDown(). - * @param {event} event The event + * Convert a datetime (Date object) into a position on the root + * This is used to get the pixel density estimate for the screen, not the center panel + * @param {Date} time A date + * @return {int} x The position on root in pixels which corresponds + * with the given date. + * @private */ - Graph3d.prototype._onMouseUp = function (event) { - this.frame.style.cursor = 'auto'; - this.leftButtonDown = false; + // TODO: move this function to Range + Graph2d.prototype._toGlobalScreen = function(time) { + var conversion = this.range.conversion(this.props.root.width); + return (time.valueOf() - conversion.offset) * conversion.scale; + }; - // remove event listeners here - G3DremoveEventListener(document, 'mousemove', this.onmousemove); - G3DremoveEventListener(document, 'mouseup', this.onmouseup); - G3DpreventDefault(event); + /** + * Initialize watching when option autoResize is true + * @private + */ + Graph2d.prototype._initAutoResize = function () { + if (this.options.autoResize == true) { + this._startAutoResize(); + } + else { + this._stopAutoResize(); + } }; /** - * After having moved the mouse, a tooltip should pop up when the mouse is resting on a data point - * @param {Event} event A mouse move event + * Watch for changes in the size of the container. On resize, the Panel will + * automatically redraw itself. + * @private */ - Graph3d.prototype._onTooltip = function (event) { - var delay = 300; // ms - var mouseX = getMouseX(event) - getAbsoluteLeft(this.frame); - var mouseY = getMouseY(event) - getAbsoluteTop(this.frame); + Graph2d.prototype._startAutoResize = function () { + var me = this; - if (!this.showTooltip) { - return; - } + this._stopAutoResize(); - if (this.tooltipTimeout) { - clearTimeout(this.tooltipTimeout); - } + this._onResize = function() { + if (me.options.autoResize != true) { + // stop watching when the option autoResize is changed to false + me._stopAutoResize(); + return; + } - // (delayed) display of a tooltip only if no mouse button is down - if (this.leftButtonDown) { - this._hideTooltip(); - return; - } + if (me.dom.root) { + // check whether the frame is resized + if ((me.dom.root.clientWidth != me.props.lastWidth) || + (me.dom.root.clientHeight != me.props.lastHeight)) { + me.props.lastWidth = me.dom.root.clientWidth; + me.props.lastHeight = me.dom.root.clientHeight; - if (this.tooltip && this.tooltip.dataPoint) { - // tooltip is currently visible - var dataPoint = this._dataPointFromXY(mouseX, mouseY); - if (dataPoint !== this.tooltip.dataPoint) { - // datapoint changed - if (dataPoint) { - this._showTooltip(dataPoint); - } - else { - this._hideTooltip(); + me.emit('change'); } } - } - else { - // tooltip is currently not visible - var me = this; - this.tooltipTimeout = setTimeout(function () { - me.tooltipTimeout = null; + }; - // show a tooltip if we have a data point - var dataPoint = me._dataPointFromXY(mouseX, mouseY); - if (dataPoint) { - me._showTooltip(dataPoint); - } - }, delay); - } + // add event listener to window resize + util.addEventListener(window, 'resize', this._onResize); + + this.watchTimer = setInterval(this._onResize, 1000); }; /** - * Event handler for touchstart event on mobile devices + * Stop watching for a resize of the frame. + * @private */ - Graph3d.prototype._onTouchStart = function(event) { - this.touchDown = true; - - var me = this; - this.ontouchmove = function (event) {me._onTouchMove(event);}; - this.ontouchend = function (event) {me._onTouchEnd(event);}; - G3DaddEventListener(document, 'touchmove', me.ontouchmove); - G3DaddEventListener(document, 'touchend', me.ontouchend); + Graph2d.prototype._stopAutoResize = function () { + if (this.watchTimer) { + clearInterval(this.watchTimer); + this.watchTimer = undefined; + } - this._onMouseDown(event); + // remove event listener on window.resize + util.removeEventListener(window, 'resize', this._onResize); + this._onResize = null; }; /** - * Event handler for touchmove event on mobile devices + * Start moving the timeline vertically + * @param {Event} event + * @private */ - Graph3d.prototype._onTouchMove = function(event) { - this._onMouseMove(event); + Graph2d.prototype._onTouch = function (event) { + this.touch.allowDragging = true; }; /** - * Event handler for touchend event on mobile devices + * Start moving the timeline vertically + * @param {Event} event + * @private */ - Graph3d.prototype._onTouchEnd = function(event) { - this.touchDown = false; - - G3DremoveEventListener(document, 'touchmove', this.ontouchmove); - G3DremoveEventListener(document, 'touchend', this.ontouchend); - - this._onMouseUp(event); + Graph2d.prototype._onPinch = function (event) { + this.touch.allowDragging = false; }; + /** + * Start moving the timeline vertically + * @param {Event} event + * @private + */ + Graph2d.prototype._onDragStart = function (event) { + this.touch.initialScrollTop = this.props.scrollTop; + }; /** - * Event handler for mouse wheel event, used to zoom the graph - * Code from http://adomas.org/javascript-mouse-wheel/ - * @param {event} event The event + * Move the timeline vertically + * @param {Event} event + * @private */ - Graph3d.prototype._onWheel = function(event) { - if (!event) /* For IE. */ - event = window.event; + Graph2d.prototype._onDrag = function (event) { + // refuse to drag when we where pinching to prevent the timeline make a jump + // when releasing the fingers in opposite order from the touch screen + if (!this.touch.allowDragging) return; - // retrieve delta - var delta = 0; - if (event.wheelDelta) { /* IE/Opera. */ - delta = event.wheelDelta/120; - } else if (event.detail) { /* Mozilla case. */ - // In Mozilla, sign of delta is different than in IE. - // Also, delta is multiple of 3. - delta = -event.detail/3; - } - - // If delta is nonzero, handle it. - // Basically, delta is now positive if wheel was scrolled up, - // and negative, if wheel was scrolled down. - if (delta) { - var oldLength = this.camera.getArmLength(); - var newLength = oldLength * (1 - delta / 10); + var delta = event.gesture.deltaY; - this.camera.setArmLength(newLength); - this.redraw(); + var oldScrollTop = this._getScrollTop(); + var newScrollTop = this._setScrollTop(this.touch.initialScrollTop + delta); - this._hideTooltip(); + if (newScrollTop != oldScrollTop) { + this.redraw(); // TODO: this causes two redraws when dragging, the other is triggered by rangechange already } - - // fire a cameraPositionChange event - var parameters = this.getCameraPosition(); - this.emit('cameraPositionChange', parameters); - - // Prevent default actions caused by mouse wheel. - // That might be ugly, but we handle scrolls somehow - // anyway, so don't bother here.. - G3DpreventDefault(event); }; /** - * Test whether a point lies inside given 2D triangle - * @param {Point2d} point - * @param {Point2d[]} triangle - * @return {boolean} Returns true if given point lies inside or on the edge of the triangle + * Apply a scrollTop + * @param {Number} scrollTop + * @returns {Number} scrollTop Returns the applied scrollTop * @private */ - Graph3d.prototype._insideTriangle = function (point, triangle) { - var a = triangle[0], - b = triangle[1], - c = triangle[2]; + Graph2d.prototype._setScrollTop = function (scrollTop) { + this.props.scrollTop = scrollTop; + this._updateScrollTop(); + return this.props.scrollTop; + }; - function sign (x) { - return x > 0 ? 1 : x < 0 ? -1 : 0; + /** + * Update the current scrollTop when the height of the containers has been changed + * @returns {Number} scrollTop Returns the applied scrollTop + * @private + */ + Graph2d.prototype._updateScrollTop = function () { + // recalculate the scrollTopMin + var scrollTopMin = Math.min(this.props.centerContainer.height - this.props.center.height, 0); // is negative or zero + if (scrollTopMin != this.props.scrollTopMin) { + // in case of bottom orientation, change the scrollTop such that the contents + // do not move relative to the time axis at the bottom + if (this.options.orientation == 'bottom') { + this.props.scrollTop += (scrollTopMin - this.props.scrollTopMin); + } + this.props.scrollTopMin = scrollTopMin; } - var as = sign((b.x - a.x) * (point.y - a.y) - (b.y - a.y) * (point.x - a.x)); - var bs = sign((c.x - b.x) * (point.y - b.y) - (c.y - b.y) * (point.x - b.x)); - var cs = sign((a.x - c.x) * (point.y - c.y) - (a.y - c.y) * (point.x - c.x)); + // limit the scrollTop to the feasible scroll range + if (this.props.scrollTop > 0) this.props.scrollTop = 0; + if (this.props.scrollTop < scrollTopMin) this.props.scrollTop = scrollTopMin; - // each of the three signs must be either equal to each other or zero - return (as == 0 || bs == 0 || as == bs) && - (bs == 0 || cs == 0 || bs == cs) && - (as == 0 || cs == 0 || as == cs); + return this.props.scrollTop; }; /** - * Find a data point close to given screen position (x, y) - * @param {Number} x - * @param {Number} y - * @return {Object | null} The closest data point or null if not close to any data point + * Get the current scrollTop + * @returns {number} scrollTop * @private */ - Graph3d.prototype._dataPointFromXY = function (x, y) { - var i, - distMax = 100, // px - dataPoint = null, - closestDataPoint = null, - closestDist = null, - center = new Point2d(x, y); + Graph2d.prototype._getScrollTop = function () { + return this.props.scrollTop; + }; - if (this.style === Graph3d.STYLE.BAR || - this.style === Graph3d.STYLE.BARCOLOR || - this.style === Graph3d.STYLE.BARSIZE) { - // the data points are ordered from far away to closest - for (i = this.dataPoints.length - 1; i >= 0; i--) { - dataPoint = this.dataPoints[i]; - var surfaces = dataPoint.surfaces; - if (surfaces) { - for (var s = surfaces.length - 1; s >= 0; s--) { - // split each surface in two triangles, and see if the center point is inside one of these - var surface = surfaces[s]; - var corners = surface.corners; - var triangle1 = [corners[0].screen, corners[1].screen, corners[2].screen]; - var triangle2 = [corners[2].screen, corners[3].screen, corners[0].screen]; - if (this._insideTriangle(center, triangle1) || - this._insideTriangle(center, triangle2)) { - // return immediately at the first hit - return dataPoint; - } - } - } - } - } - else { - // find the closest data point, using distance to the center of the point on 2d screen - for (i = 0; i < this.dataPoints.length; i++) { - dataPoint = this.dataPoints[i]; - var point = dataPoint.screen; - if (point) { - var distX = Math.abs(x - point.x); - var distY = Math.abs(y - point.y); - var dist = Math.sqrt(distX * distX + distY * distY); + module.exports = Graph2d; - if ((closestDist === null || dist < closestDist) && dist < distMax) { - closestDist = dist; - closestDataPoint = dataPoint; - } - } - } - } +/***/ }, +/* 8 */ +/***/ function(module, exports, __webpack_require__) { - return closestDataPoint; - }; + var util = __webpack_require__(1); + var moment = __webpack_require__(39); + var Component = __webpack_require__(12); /** - * Display a tooltip for given data point - * @param {Object} dataPoint - * @private + * @constructor Range + * A Range controls a numeric range with a start and end value. + * The Range adjusts the range based on mouse events or programmatic changes, + * and triggers events when the range is changing or has been changed. + * @param {{dom: Object, domProps: Object, emitter: Emitter}} body + * @param {Object} [options] See description at Range.setOptions */ - Graph3d.prototype._showTooltip = function (dataPoint) { - var content, line, dot; - - if (!this.tooltip) { - content = document.createElement('div'); - content.style.position = 'absolute'; - content.style.padding = '10px'; - content.style.border = '1px solid #4d4d4d'; - content.style.color = '#1a1a1a'; - content.style.background = 'rgba(255,255,255,0.7)'; - content.style.borderRadius = '2px'; - content.style.boxShadow = '5px 5px 10px rgba(128,128,128,0.5)'; + function Range(body, options) { + var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0); + this.start = now.clone().add('days', -3).valueOf(); // Number + this.end = now.clone().add('days', 4).valueOf(); // Number - line = document.createElement('div'); - line.style.position = 'absolute'; - line.style.height = '40px'; - line.style.width = '0'; - line.style.borderLeft = '1px solid #4d4d4d'; + this.body = body; - dot = document.createElement('div'); - dot.style.position = 'absolute'; - dot.style.height = '0'; - dot.style.width = '0'; - dot.style.border = '5px solid #4d4d4d'; - dot.style.borderRadius = '5px'; + // default options + this.defaultOptions = { + start: null, + end: null, + direction: 'horizontal', // 'horizontal' or 'vertical' + moveable: true, + zoomable: true, + min: null, + max: null, + zoomMin: 10, // milliseconds + zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000 // milliseconds + }; + this.options = util.extend({}, this.defaultOptions); - this.tooltip = { - dataPoint: null, - dom: { - content: content, - line: line, - dot: dot - } - }; - } - else { - content = this.tooltip.dom.content; - line = this.tooltip.dom.line; - dot = this.tooltip.dom.dot; - } + this.props = { + touch: {} + }; - this._hideTooltip(); + // drag listeners for dragging + this.body.emitter.on('dragstart', this._onDragStart.bind(this)); + this.body.emitter.on('drag', this._onDrag.bind(this)); + this.body.emitter.on('dragend', this._onDragEnd.bind(this)); - this.tooltip.dataPoint = dataPoint; - if (typeof this.showTooltip === 'function') { - content.innerHTML = this.showTooltip(dataPoint.point); - } - else { - content.innerHTML = '' + - '' + - '' + - '' + - '
x:' + dataPoint.point.x + '
y:' + dataPoint.point.y + '
z:' + dataPoint.point.z + '
'; - } + // ignore dragging when holding + this.body.emitter.on('hold', this._onHold.bind(this)); - content.style.left = '0'; - content.style.top = '0'; - this.frame.appendChild(content); - this.frame.appendChild(line); - this.frame.appendChild(dot); + // mouse wheel for zooming + this.body.emitter.on('mousewheel', this._onMouseWheel.bind(this)); + this.body.emitter.on('DOMMouseScroll', this._onMouseWheel.bind(this)); // For FF - // calculate sizes - var contentWidth = content.offsetWidth; - var contentHeight = content.offsetHeight; - var lineHeight = line.offsetHeight; - var dotWidth = dot.offsetWidth; - var dotHeight = dot.offsetHeight; + // pinch to zoom + this.body.emitter.on('touch', this._onTouch.bind(this)); + this.body.emitter.on('pinch', this._onPinch.bind(this)); - var left = dataPoint.screen.x - contentWidth / 2; - left = Math.min(Math.max(left, 10), this.frame.clientWidth - 10 - contentWidth); + this.setOptions(options); + } - line.style.left = dataPoint.screen.x + 'px'; - line.style.top = (dataPoint.screen.y - lineHeight) + 'px'; - content.style.left = left + 'px'; - content.style.top = (dataPoint.screen.y - lineHeight - contentHeight) + 'px'; - dot.style.left = (dataPoint.screen.x - dotWidth / 2) + 'px'; - dot.style.top = (dataPoint.screen.y - dotHeight / 2) + 'px'; - }; + Range.prototype = new Component(); /** - * Hide the tooltip when displayed - * @private - */ - Graph3d.prototype._hideTooltip = function () { - if (this.tooltip) { - this.tooltip.dataPoint = null; - - for (var prop in this.tooltip.dom) { - if (this.tooltip.dom.hasOwnProperty(prop)) { - var elem = this.tooltip.dom[prop]; - if (elem && elem.parentNode) { - elem.parentNode.removeChild(elem); - } - } + * Set options for the range controller + * @param {Object} options Available options: + * {Number | Date | String} start Start date for the range + * {Number | Date | String} end End date for the range + * {Number} min Minimum value for start + * {Number} max Maximum value for end + * {Number} zoomMin Set a minimum value for + * (end - start). + * {Number} zoomMax Set a maximum value for + * (end - start). + * {Boolean} moveable Enable moving of the range + * by dragging. True by default + * {Boolean} zoomable Enable zooming of the range + * by pinching/scrolling. True by default + */ + Range.prototype.setOptions = function (options) { + if (options) { + // copy the options that we know + var fields = ['direction', 'min', 'max', 'zoomMin', 'zoomMax', 'moveable', 'zoomable']; + util.selectiveExtend(fields, this.options, options); + + if ('start' in options || 'end' in options) { + // apply a new range. both start and end are optional + this.setRange(options.start, options.end); } } }; - /** - * Add and event listener. Works for all browsers - * @param {Element} element An html element - * @param {string} action The action, for example 'click', - * without the prefix 'on' - * @param {function} listener The callback function to be executed - * @param {boolean} useCapture + * Test whether direction has a valid value + * @param {String} direction 'horizontal' or 'vertical' */ - G3DaddEventListener = function(element, action, listener, useCapture) { - if (element.addEventListener) { - if (useCapture === undefined) - useCapture = false; - - if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) { - action = 'DOMMouseScroll'; // For Firefox - } + function validateDirection (direction) { + if (direction != 'horizontal' && direction != 'vertical') { + throw new TypeError('Unknown direction "' + direction + '". ' + + 'Choose "horizontal" or "vertical".'); + } + } - element.addEventListener(action, listener, useCapture); - } else { - element.attachEvent('on' + action, listener); // IE browsers + /** + * Set a new start and end range + * @param {Number} [start] + * @param {Number} [end] + */ + Range.prototype.setRange = function(start, end) { + var changed = this._applyRange(start, end); + if (changed) { + var params = { + start: new Date(this.start), + end: new Date(this.end) + }; + this.body.emitter.emit('rangechange', params); + this.body.emitter.emit('rangechanged', params); } }; /** - * Remove an event listener from an element - * @param {Element} element An html dom element - * @param {string} action The name of the event, for example 'mousedown' - * @param {function} listener The listener function - * @param {boolean} useCapture + * Set a new start and end range. This method is the same as setRange, but + * does not trigger a range change and range changed event, and it returns + * true when the range is changed + * @param {Number} [start] + * @param {Number} [end] + * @return {Boolean} changed + * @private */ - G3DremoveEventListener = function(element, action, listener, useCapture) { - if (element.removeEventListener) { - // non-IE browsers - if (useCapture === undefined) - useCapture = false; + Range.prototype._applyRange = function(start, end) { + var newStart = (start != null) ? util.convert(start, 'Date').valueOf() : this.start, + newEnd = (end != null) ? util.convert(end, 'Date').valueOf() : this.end, + max = (this.options.max != null) ? util.convert(this.options.max, 'Date').valueOf() : null, + min = (this.options.min != null) ? util.convert(this.options.min, 'Date').valueOf() : null, + diff; - if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) { - action = 'DOMMouseScroll'; // For Firefox + // check for valid number + if (isNaN(newStart) || newStart === null) { + throw new Error('Invalid start "' + start + '"'); + } + if (isNaN(newEnd) || newEnd === null) { + throw new Error('Invalid end "' + end + '"'); + } + + // prevent start < end + if (newEnd < newStart) { + newEnd = newStart; + } + + // prevent start < min + if (min !== null) { + if (newStart < min) { + diff = (min - newStart); + newStart += diff; + newEnd += diff; + + // prevent end > max + if (max != null) { + if (newEnd > max) { + newEnd = max; + } + } } + } - element.removeEventListener(action, listener, useCapture); - } else { - // IE browsers - element.detachEvent('on' + action, listener); + // prevent end > max + if (max !== null) { + if (newEnd > max) { + diff = (newEnd - max); + newStart -= diff; + newEnd -= diff; + + // prevent start < min + if (min != null) { + if (newStart < min) { + newStart = min; + } + } + } + } + + // prevent (end-start) < zoomMin + if (this.options.zoomMin !== null) { + var zoomMin = parseFloat(this.options.zoomMin); + if (zoomMin < 0) { + zoomMin = 0; + } + if ((newEnd - newStart) < zoomMin) { + if ((this.end - this.start) === zoomMin) { + // ignore this action, we are already zoomed to the minimum + newStart = this.start; + newEnd = this.end; + } + else { + // zoom to the minimum + diff = (zoomMin - (newEnd - newStart)); + newStart -= diff / 2; + newEnd += diff / 2; + } + } } + + // prevent (end-start) > zoomMax + if (this.options.zoomMax !== null) { + var zoomMax = parseFloat(this.options.zoomMax); + if (zoomMax < 0) { + zoomMax = 0; + } + if ((newEnd - newStart) > zoomMax) { + if ((this.end - this.start) === zoomMax) { + // ignore this action, we are already zoomed to the maximum + newStart = this.start; + newEnd = this.end; + } + else { + // zoom to the maximum + diff = ((newEnd - newStart) - zoomMax); + newStart += diff / 2; + newEnd -= diff / 2; + } + } + } + + var changed = (this.start != newStart || this.end != newEnd); + + this.start = newStart; + this.end = newEnd; + + return changed; }; /** - * Stop event propagation + * Retrieve the current range. + * @return {Object} An object with start and end properties */ - G3DstopPropagation = function(event) { - if (!event) - event = window.event; + Range.prototype.getRange = function() { + return { + start: this.start, + end: this.end + }; + }; - if (event.stopPropagation) { - event.stopPropagation(); // non-IE browsers + /** + * Calculate the conversion offset and scale for current range, based on + * the provided width + * @param {Number} width + * @returns {{offset: number, scale: number}} conversion + */ + Range.prototype.conversion = function (width) { + return Range.conversion(this.start, this.end, width); + }; + + /** + * Static method to calculate the conversion offset and scale for a range, + * based on the provided start, end, and width + * @param {Number} start + * @param {Number} end + * @param {Number} width + * @returns {{offset: number, scale: number}} conversion + */ + Range.conversion = function (start, end, width) { + if (width != 0 && (end - start != 0)) { + return { + offset: start, + scale: width / (end - start) + } } else { - event.cancelBubble = true; // IE browsers + return { + offset: 0, + scale: 1 + }; } }; - /** - * Cancels the event if it is cancelable, without stopping further propagation of the event. + * Start dragging horizontally or vertically + * @param {Event} event + * @private */ - G3DpreventDefault = function (event) { - if (!event) - event = window.event; + Range.prototype._onDragStart = function(event) { + // only allow dragging when configured as movable + if (!this.options.moveable) return; - if (event.preventDefault) { - event.preventDefault(); // non-IE browsers - } - else { - event.returnValue = false; // IE browsers + // refuse to drag when we where pinching to prevent the timeline make a jump + // when releasing the fingers in opposite order from the touch screen + if (!this.props.touch.allowDragging) return; + + this.props.touch.start = this.start; + this.props.touch.end = this.end; + + if (this.body.dom.root) { + this.body.dom.root.style.cursor = 'move'; } }; /** - * @constructor Slider - * - * An html slider control with start/stop/prev/next buttons - * @param {Element} container The element where the slider will be created - * @param {Object} options Available options: - * {boolean} visible If true (default) the - * slider is visible. + * Perform dragging operation + * @param {Event} event + * @private */ - function Slider(container, options) { - if (container === undefined) { - throw 'Error: No container element defined'; - } - this.container = container; - this.visible = (options && options.visible != undefined) ? options.visible : true; - - if (this.visible) { - this.frame = document.createElement('DIV'); - //this.frame.style.backgroundColor = '#E5E5E5'; - this.frame.style.width = '100%'; - this.frame.style.position = 'relative'; - this.container.appendChild(this.frame); + Range.prototype._onDrag = function (event) { + // only allow dragging when configured as movable + if (!this.options.moveable) return; + var direction = this.options.direction; + validateDirection(direction); + // refuse to drag when we where pinching to prevent the timeline make a jump + // when releasing the fingers in opposite order from the touch screen + if (!this.props.touch.allowDragging) return; + var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY, + interval = (this.props.touch.end - this.props.touch.start), + width = (direction == 'horizontal') ? this.body.domProps.center.width : this.body.domProps.center.height, + diffRange = -delta / width * interval; + this._applyRange(this.props.touch.start + diffRange, this.props.touch.end + diffRange); + this.body.emitter.emit('rangechange', { + start: new Date(this.start), + end: new Date(this.end) + }); + }; - this.frame.prev = document.createElement('INPUT'); - this.frame.prev.type = 'BUTTON'; - this.frame.prev.value = 'Prev'; - this.frame.appendChild(this.frame.prev); + /** + * Stop dragging operation + * @param {event} event + * @private + */ + Range.prototype._onDragEnd = function (event) { + // only allow dragging when configured as movable + if (!this.options.moveable) return; - this.frame.play = document.createElement('INPUT'); - this.frame.play.type = 'BUTTON'; - this.frame.play.value = 'Play'; - this.frame.appendChild(this.frame.play); + // refuse to drag when we where pinching to prevent the timeline make a jump + // when releasing the fingers in opposite order from the touch screen + if (!this.props.touch.allowDragging) return; - this.frame.next = document.createElement('INPUT'); - this.frame.next.type = 'BUTTON'; - this.frame.next.value = 'Next'; - this.frame.appendChild(this.frame.next); + if (this.body.dom.root) { + this.body.dom.root.style.cursor = 'auto'; + } - this.frame.bar = document.createElement('INPUT'); - this.frame.bar.type = 'BUTTON'; - this.frame.bar.style.position = 'absolute'; - this.frame.bar.style.border = '1px solid red'; - this.frame.bar.style.width = '100px'; - this.frame.bar.style.height = '6px'; - this.frame.bar.style.borderRadius = '2px'; - this.frame.bar.style.MozBorderRadius = '2px'; - this.frame.bar.style.border = '1px solid #7F7F7F'; - this.frame.bar.style.backgroundColor = '#E5E5E5'; - this.frame.appendChild(this.frame.bar); + // fire a rangechanged event + this.body.emitter.emit('rangechanged', { + start: new Date(this.start), + end: new Date(this.end) + }); + }; - this.frame.slide = document.createElement('INPUT'); - this.frame.slide.type = 'BUTTON'; - this.frame.slide.style.margin = '0px'; - this.frame.slide.value = ' '; - this.frame.slide.style.position = 'relative'; - this.frame.slide.style.left = '-100px'; - this.frame.appendChild(this.frame.slide); + /** + * Event handler for mouse wheel event, used to zoom + * Code from http://adomas.org/javascript-mouse-wheel/ + * @param {Event} event + * @private + */ + Range.prototype._onMouseWheel = function(event) { + // only allow zooming when configured as zoomable and moveable + if (!(this.options.zoomable && this.options.moveable)) return; - // create events - var me = this; - this.frame.slide.onmousedown = function (event) {me._onMouseDown(event);}; - this.frame.prev.onclick = function (event) {me.prev(event);}; - this.frame.play.onclick = function (event) {me.togglePlay(event);}; - this.frame.next.onclick = function (event) {me.next(event);}; + // retrieve delta + var delta = 0; + if (event.wheelDelta) { /* IE/Opera. */ + delta = event.wheelDelta / 120; + } else if (event.detail) { /* Mozilla case. */ + // In Mozilla, sign of delta is different than in IE. + // Also, delta is multiple of 3. + delta = -event.detail / 3; } - this.onChangeCallback = undefined; + // If delta is nonzero, handle it. + // Basically, delta is now positive if wheel was scrolled up, + // and negative, if wheel was scrolled down. + if (delta) { + // perform the zoom action. Delta is normally 1 or -1 - this.values = []; - this.index = undefined; + // adjust a negative delta such that zooming in with delta 0.1 + // equals zooming out with a delta -0.1 + var scale; + if (delta < 0) { + scale = 1 - (delta / 5); + } + else { + scale = 1 / (1 + (delta / 5)) ; + } - this.playTimeout = undefined; - this.playInterval = 1000; // milliseconds - this.playLoop = true; - } + // calculate center, the date to zoom around + var gesture = util.fakeGesture(this, event), + pointer = getPointer(gesture.center, this.body.dom.center), + pointerDate = this._pointerToDate(pointer); + + this.zoom(scale, pointerDate); + } + + // Prevent default actions caused by mouse wheel + // (else the page and timeline both zoom and scroll) + event.preventDefault(); + }; /** - * Select the previous index + * Start of a touch gesture + * @private */ - Slider.prototype.prev = function() { - var index = this.getIndex(); - if (index > 0) { - index--; - this.setIndex(index); - } + Range.prototype._onTouch = function (event) { + this.props.touch.start = this.start; + this.props.touch.end = this.end; + this.props.touch.allowDragging = true; + this.props.touch.center = null; }; /** - * Select the next index + * On start of a hold gesture + * @private */ - Slider.prototype.next = function() { - var index = this.getIndex(); - if (index < this.values.length - 1) { - index++; - this.setIndex(index); - } + Range.prototype._onHold = function () { + this.props.touch.allowDragging = false; }; /** - * Select the next index + * Handle pinch event + * @param {Event} event + * @private */ - Slider.prototype.playNext = function() { - var start = new Date(); + Range.prototype._onPinch = function (event) { + // only allow zooming when configured as zoomable and moveable + if (!(this.options.zoomable && this.options.moveable)) return; - var index = this.getIndex(); - if (index < this.values.length - 1) { - index++; - this.setIndex(index); - } - else if (this.playLoop) { - // jump to the start - index = 0; - this.setIndex(index); - } + this.props.touch.allowDragging = false; - var end = new Date(); - var diff = (end - start); + if (event.gesture.touches.length > 1) { + if (!this.props.touch.center) { + this.props.touch.center = getPointer(event.gesture.center, this.body.dom.center); + } - // calculate how much time it to to set the index and to execute the callback - // function. - var interval = Math.max(this.playInterval - diff, 0); - // document.title = diff // TODO: cleanup + var scale = 1 / event.gesture.scale, + initDate = this._pointerToDate(this.props.touch.center); - var me = this; - this.playTimeout = setTimeout(function() {me.playNext();}, interval); - }; + // calculate new start and end + var newStart = parseInt(initDate + (this.props.touch.start - initDate) * scale); + var newEnd = parseInt(initDate + (this.props.touch.end - initDate) * scale); - /** - * Toggle start or stop playing - */ - Slider.prototype.togglePlay = function() { - if (this.playTimeout === undefined) { - this.play(); - } else { - this.stop(); + // apply new range + this.setRange(newStart, newEnd); } }; /** - * Start playing + * Helper function to calculate the center date for zooming + * @param {{x: Number, y: Number}} pointer + * @return {number} date + * @private */ - Slider.prototype.play = function() { - // Test whether already playing - if (this.playTimeout) return; + Range.prototype._pointerToDate = function (pointer) { + var conversion; + var direction = this.options.direction; - this.playNext(); + validateDirection(direction); - if (this.frame) { - this.frame.play.value = 'Stop'; + if (direction == 'horizontal') { + var width = this.body.domProps.center.width; + conversion = this.conversion(width); + return pointer.x / conversion.scale + conversion.offset; } - }; - - /** - * Stop playing - */ - Slider.prototype.stop = function() { - clearInterval(this.playTimeout); - this.playTimeout = undefined; - - if (this.frame) { - this.frame.play.value = 'Play'; + else { + var height = this.body.domProps.center.height; + conversion = this.conversion(height); + return pointer.y / conversion.scale + conversion.offset; } }; /** - * Set a callback function which will be triggered when the value of the - * slider bar has changed. + * Get the pointer location relative to the location of the dom element + * @param {{pageX: Number, pageY: Number}} touch + * @param {Element} element HTML DOM element + * @return {{x: Number, y: Number}} pointer + * @private */ - Slider.prototype.setOnChangeCallback = function(callback) { - this.onChangeCallback = callback; - }; + function getPointer (touch, element) { + return { + x: touch.pageX - util.getAbsoluteLeft(element), + y: touch.pageY - util.getAbsoluteTop(element) + }; + } /** - * Set the interval for playing the list - * @param {Number} interval The interval in milliseconds + * Zoom the range the given scale in or out. Start and end date will + * be adjusted, and the timeline will be redrawn. You can optionally give a + * date around which to zoom. + * For example, try scale = 0.9 or 1.1 + * @param {Number} scale Scaling factor. Values above 1 will zoom out, + * values below 1 will zoom in. + * @param {Number} [center] Value representing a date around which will + * be zoomed. */ - Slider.prototype.setPlayInterval = function(interval) { - this.playInterval = interval; + Range.prototype.zoom = function(scale, center) { + // if centerDate is not provided, take it half between start Date and end Date + if (center == null) { + center = (this.start + this.end) / 2; + } + + // calculate new start and end + var newStart = center + (this.start - center) * scale; + var newEnd = center + (this.end - center) * scale; + + this.setRange(newStart, newEnd); }; /** - * Retrieve the current play interval - * @return {Number} interval The interval in milliseconds + * Move the range with a given delta to the left or right. Start and end + * value will be adjusted. For example, try delta = 0.1 or -0.1 + * @param {Number} delta Moving amount. Positive value will move right, + * negative value will move left */ - Slider.prototype.getPlayInterval = function(interval) { - return this.playInterval; + Range.prototype.move = function(delta) { + // zoom start Date and end Date relative to the centerDate + var diff = (this.end - this.start); + + // apply new values + var newStart = this.start + diff * delta; + var newEnd = this.end + diff * delta; + + // TODO: reckon with min and max range + + this.start = newStart; + this.end = newEnd; }; /** - * Set looping on or off - * @pararm {boolean} doLoop If true, the slider will jump to the start when - * the end is passed, and will jump to the end - * when the start is passed. + * Move the range to a new center point + * @param {Number} moveTo New center point of the range */ - Slider.prototype.setPlayLoop = function(doLoop) { - this.playLoop = doLoop; + Range.prototype.moveTo = function(moveTo) { + var center = (this.start + this.end) / 2; + + var diff = center - moveTo; + + // calculate new start and end + var newStart = this.start - diff; + var newEnd = this.end - diff; + + this.setRange(newStart, newEnd); }; + module.exports = Range; + + +/***/ }, +/* 9 */ +/***/ function(module, exports, __webpack_require__) { + + // Utility functions for ordering and stacking of items + var EPSILON = 0.001; // used when checking collisions, to prevent round-off errors /** - * Execute the onchange callback function + * Order items by their start data + * @param {Item[]} items */ - Slider.prototype.onChange = function() { - if (this.onChangeCallback !== undefined) { - this.onChangeCallback(); - } + exports.orderByStart = function(items) { + items.sort(function (a, b) { + return a.data.start - b.data.start; + }); }; /** - * redraw the slider on the correct place + * Order items by their end date. If they have no end date, their start date + * is used. + * @param {Item[]} items */ - Slider.prototype.redraw = function() { - if (this.frame) { - // resize the bar - this.frame.bar.style.top = (this.frame.clientHeight/2 - - this.frame.bar.offsetHeight/2) + 'px'; - this.frame.bar.style.width = (this.frame.clientWidth - - this.frame.prev.clientWidth - - this.frame.play.clientWidth - - this.frame.next.clientWidth - 30) + 'px'; + exports.orderByEnd = function(items) { + items.sort(function (a, b) { + var aTime = ('end' in a.data) ? a.data.end : a.data.start, + bTime = ('end' in b.data) ? b.data.end : b.data.start; - // position the slider button - var left = this.indexToLeft(this.index); - this.frame.slide.style.left = (left) + 'px'; - } + return aTime - bTime; + }); }; - /** - * Set the list with values for the slider - * @param {Array} values A javascript array with values (any type) + * Adjust vertical positions of the items such that they don't overlap each + * other. + * @param {Item[]} items + * All visible items + * @param {{item: number, axis: number}} margin + * Margins between items and between items and the axis. + * @param {boolean} [force=false] + * If true, all items will be repositioned. If false (default), only + * items having a top===null will be re-stacked */ - Slider.prototype.setValues = function(values) { - this.values = values; + exports.stack = function(items, margin, force) { + var i, iMax; - if (this.values.length > 0) - this.setIndex(0); - else - this.index = undefined; + if (force) { + // reset top position of all items + for (i = 0, iMax = items.length; i < iMax; i++) { + items[i].top = null; + } + } + + // calculate new, non-overlapping positions + for (i = 0, iMax = items.length; i < iMax; i++) { + var item = items[i]; + if (item.top === null) { + // initialize top position + item.top = margin.axis; + + do { + // TODO: optimize checking for overlap. when there is a gap without items, + // you only need to check for items from the next item on, not from zero + var collidingItem = null; + for (var j = 0, jj = items.length; j < jj; j++) { + var other = items[j]; + if (other.top !== null && other !== item && exports.collision(item, other, margin.item)) { + collidingItem = other; + break; + } + } + + if (collidingItem != null) { + // There is a collision. Reposition the items above the colliding element + item.top = collidingItem.top + collidingItem.height + margin.item; + } + } while (collidingItem); + } + } }; /** - * Select a value by its index - * @param {Number} index + * Adjust vertical positions of the items without stacking them + * @param {Item[]} items + * All visible items + * @param {{item: number, axis: number}} margin + * Margins between items and between items and the axis. */ - Slider.prototype.setIndex = function(index) { - if (index < this.values.length) { - this.index = index; + exports.nostack = function(items, margin) { + var i, iMax; - this.redraw(); - this.onChange(); - } - else { - throw 'Error: index out of range'; + // reset top position of all items + for (i = 0, iMax = items.length; i < iMax; i++) { + items[i].top = margin.axis; } }; /** - * retrieve the index of the currently selected vaue - * @return {Number} index + * Test if the two provided items collide + * The items must have parameters left, width, top, and height. + * @param {Item} a The first item + * @param {Item} b The second item + * @param {Number} margin A minimum required margin. + * If margin is provided, the two items will be + * marked colliding when they overlap or + * when the margin between the two is smaller than + * the requested margin. + * @return {boolean} true if a and b collide, else false */ - Slider.prototype.getIndex = function() { - return this.index; + exports.collision = function(a, b, margin) { + return ((a.left - margin + EPSILON) < (b.left + b.width) && + (a.left + a.width + margin - EPSILON) > b.left && + (a.top - margin + EPSILON) < (b.top + b.height) && + (a.top + a.height + margin - EPSILON) > b.top); }; +/***/ }, +/* 10 */ +/***/ function(module, exports, __webpack_require__) { + + var moment = __webpack_require__(39); + /** - * retrieve the currently selected value - * @return {*} value + * @constructor TimeStep + * The class TimeStep is an iterator for dates. You provide a start date and an + * end date. The class itself determines the best scale (step size) based on the + * provided start Date, end Date, and minimumStep. + * + * If minimumStep is provided, the step size is chosen as close as possible + * to the minimumStep but larger than minimumStep. If minimumStep is not + * provided, the scale is set to 1 DAY. + * The minimumStep should correspond with the onscreen size of about 6 characters + * + * Alternatively, you can set a scale by hand. + * After creation, you can initialize the class by executing first(). Then you + * can iterate from the start date to the end date via next(). You can check if + * the end date is reached with the function hasNext(). After each step, you can + * retrieve the current date via getCurrent(). + * The TimeStep has scales ranging from milliseconds, seconds, minutes, hours, + * days, to years. + * + * Version: 1.2 + * + * @param {Date} [start] The start date, for example new Date(2010, 9, 21) + * or new Date(2010, 9, 21, 23, 45, 00) + * @param {Date} [end] The end date + * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds */ - Slider.prototype.get = function() { - return this.values[this.index]; - }; + function TimeStep(start, end, minimumStep) { + // variables + this.current = new Date(); + this._start = new Date(); + this._end = new Date(); + this.autoScale = true; + this.scale = TimeStep.SCALE.DAY; + this.step = 1; - Slider.prototype._onMouseDown = function(event) { - // only react on left mouse button down - var leftButtonDown = event.which ? (event.which === 1) : (event.button === 1); - if (!leftButtonDown) return; - - this.startClientX = event.clientX; - this.startSlideX = parseFloat(this.frame.slide.style.left); - - this.frame.style.cursor = 'move'; + // initialize the range + this.setRange(start, end, minimumStep); + } - // add event listeners to handle moving the contents - // we store the function onmousemove and onmouseup in the graph, so we can - // remove the eventlisteners lateron in the function mouseUp() - var me = this; - this.onmousemove = function (event) {me._onMouseMove(event);}; - this.onmouseup = function (event) {me._onMouseUp(event);}; - G3DaddEventListener(document, 'mousemove', this.onmousemove); - G3DaddEventListener(document, 'mouseup', this.onmouseup); - G3DpreventDefault(event); + /// enum scale + TimeStep.SCALE = { + MILLISECOND: 1, + SECOND: 2, + MINUTE: 3, + HOUR: 4, + DAY: 5, + WEEKDAY: 6, + MONTH: 7, + YEAR: 8 }; - Slider.prototype.leftToIndex = function (left) { - var width = parseFloat(this.frame.bar.style.width) - - this.frame.slide.clientWidth - 10; - var x = left - 3; + /** + * Set a new range + * If minimumStep is provided, the step size is chosen as close as possible + * to the minimumStep but larger than minimumStep. If minimumStep is not + * provided, the scale is set to 1 DAY. + * The minimumStep should correspond with the onscreen size of about 6 characters + * @param {Date} [start] The start date and time. + * @param {Date} [end] The end date and time. + * @param {int} [minimumStep] Optional. Minimum step size in milliseconds + */ + TimeStep.prototype.setRange = function(start, end, minimumStep) { + if (!(start instanceof Date) || !(end instanceof Date)) { + throw "No legal start or end date in method setRange"; + } - var index = Math.round(x / width * (this.values.length-1)); - if (index < 0) index = 0; - if (index > this.values.length-1) index = this.values.length-1; + this._start = (start != undefined) ? new Date(start.valueOf()) : new Date(); + this._end = (end != undefined) ? new Date(end.valueOf()) : new Date(); - return index; + if (this.autoScale) { + this.setMinimumStep(minimumStep); + } }; - Slider.prototype.indexToLeft = function (index) { - var width = parseFloat(this.frame.bar.style.width) - - this.frame.slide.clientWidth - 10; - - var x = index / (this.values.length-1) * width; - var left = x + 3; - - return left; + /** + * Set the range iterator to the start date. + */ + TimeStep.prototype.first = function() { + this.current = new Date(this._start.valueOf()); + this.roundToMinor(); }; + /** + * Round the current date to the first minor date value + * This must be executed once when the current date is set to start Date + */ + TimeStep.prototype.roundToMinor = function() { + // round to floor + // IMPORTANT: we have no breaks in this switch! (this is no bug) + //noinspection FallthroughInSwitchStatementJS + switch (this.scale) { + case TimeStep.SCALE.YEAR: + this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step)); + this.current.setMonth(0); + case TimeStep.SCALE.MONTH: this.current.setDate(1); + case TimeStep.SCALE.DAY: // intentional fall through + case TimeStep.SCALE.WEEKDAY: this.current.setHours(0); + case TimeStep.SCALE.HOUR: this.current.setMinutes(0); + case TimeStep.SCALE.MINUTE: this.current.setSeconds(0); + case TimeStep.SCALE.SECOND: this.current.setMilliseconds(0); + //case TimeStep.SCALE.MILLISECOND: // nothing to do for milliseconds + } - - Slider.prototype._onMouseMove = function (event) { - var diff = event.clientX - this.startClientX; - var x = this.startSlideX + diff; - - var index = this.leftToIndex(x); - - this.setIndex(index); - - G3DpreventDefault(); + if (this.step != 1) { + // round down to the first minor value that is a multiple of the current step size + switch (this.scale) { + case TimeStep.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break; + case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break; + case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break; + case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break; + case TimeStep.SCALE.WEEKDAY: // intentional fall through + case TimeStep.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break; + case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break; + case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break; + default: break; + } + } }; - - Slider.prototype._onMouseUp = function (event) { - this.frame.style.cursor = 'auto'; - - // remove event listeners - G3DremoveEventListener(document, 'mousemove', this.onmousemove); - G3DremoveEventListener(document, 'mouseup', this.onmouseup); - - G3DpreventDefault(); + /** + * Check if the there is a next step + * @return {boolean} true if the current date has not passed the end date + */ + TimeStep.prototype.hasNext = function () { + return (this.current.valueOf() <= this._end.valueOf()); }; + /** + * Do the next step + */ + TimeStep.prototype.next = function() { + var prev = this.current.valueOf(); + // Two cases, needed to prevent issues with switching daylight savings + // (end of March and end of October) + if (this.current.getMonth() < 6) { + switch (this.scale) { + case TimeStep.SCALE.MILLISECOND: - /**--------------------------------------------------------------------------**/ - + this.current = new Date(this.current.valueOf() + this.step); break; + case TimeStep.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break; + case TimeStep.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break; + case TimeStep.SCALE.HOUR: + this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60); + // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...) + var h = this.current.getHours(); + this.current.setHours(h - (h % this.step)); + break; + case TimeStep.SCALE.WEEKDAY: // intentional fall through + case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break; + case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break; + case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break; + default: break; + } + } + else { + switch (this.scale) { + case TimeStep.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break; + case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break; + case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break; + case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break; + case TimeStep.SCALE.WEEKDAY: // intentional fall through + case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break; + case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break; + case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break; + default: break; + } + } + if (this.step != 1) { + // round down to the correct major value + switch (this.scale) { + case TimeStep.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break; + case TimeStep.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break; + case TimeStep.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break; + case TimeStep.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break; + case TimeStep.SCALE.WEEKDAY: // intentional fall through + case TimeStep.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break; + case TimeStep.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break; + case TimeStep.SCALE.YEAR: break; // nothing to do for year + default: break; + } + } - /** - * Retrieve the absolute left value of a DOM element - * @param {Element} elem A dom element, for example a div - * @return {Number} left The absolute left position of this element - * in the browser page. - */ - getAbsoluteLeft = function(elem) { - var left = 0; - while( elem !== null ) { - left += elem.offsetLeft; - left -= elem.scrollLeft; - elem = elem.offsetParent; + // safety mechanism: if current time is still unchanged, move to the end + if (this.current.valueOf() == prev) { + this.current = new Date(this._end.valueOf()); } - return left; }; + /** - * Retrieve the absolute top value of a DOM element - * @param {Element} elem A dom element, for example a div - * @return {Number} top The absolute top position of this element - * in the browser page. + * Get the current datetime + * @return {Date} current The current date */ - getAbsoluteTop = function(elem) { - var top = 0; - while( elem !== null ) { - top += elem.offsetTop; - top -= elem.scrollTop; - elem = elem.offsetParent; - } - return top; + TimeStep.prototype.getCurrent = function() { + return this.current; }; /** - * Get the horizontal mouse position from a mouse event - * @param {Event} event - * @return {Number} mouse x + * Set a custom scale. Autoscaling will be disabled. + * For example setScale(SCALE.MINUTES, 5) will result + * in minor steps of 5 minutes, and major steps of an hour. + * + * @param {TimeStep.SCALE} newScale + * A scale. Choose from SCALE.MILLISECOND, + * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR, + * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH, + * SCALE.YEAR. + * @param {Number} newStep A step size, by default 1. Choose for + * example 1, 2, 5, or 10. */ - getMouseX = function(event) { - if ('clientX' in event) return event.clientX; - return event.targetTouches[0] && event.targetTouches[0].clientX || 0; + TimeStep.prototype.setScale = function(newScale, newStep) { + this.scale = newScale; + + if (newStep > 0) { + this.step = newStep; + } + + this.autoScale = false; }; /** - * Get the vertical mouse position from a mouse event - * @param {Event} event - * @return {Number} mouse y + * Enable or disable autoscaling + * @param {boolean} enable If true, autoascaling is set true */ - getMouseY = function(event) { - if ('clientY' in event) return event.clientY; - return event.targetTouches[0] && event.targetTouches[0].clientY || 0; + TimeStep.prototype.setAutoScale = function (enable) { + this.autoScale = enable; }; - module.exports = Graph3d; - - -/***/ }, -/* 6 */ -/***/ function(module, exports, __webpack_require__) { - - var Emitter = __webpack_require__(41); - var Hammer = __webpack_require__(49); - var util = __webpack_require__(1); - var DataSet = __webpack_require__(3); - var DataView = __webpack_require__(4); - var Range = __webpack_require__(9); - var TimeAxis = __webpack_require__(21); - var CurrentTime = __webpack_require__(13); - var CustomTime = __webpack_require__(14); - var ItemSet = __webpack_require__(18); /** - * Create a timeline visualization - * @param {HTMLElement} container - * @param {vis.DataSet | Array | google.visualization.DataTable} [items] - * @param {Object} [options] See Timeline.setOptions for the available options. - * @constructor + * Automatically determine the scale that bests fits the provided minimum step + * @param {Number} [minimumStep] The minimum step size in milliseconds */ - function Timeline (container, items, options) { - if (!(this instanceof Timeline)) { - throw new SyntaxError('Constructor must be called with the new operator'); + TimeStep.prototype.setMinimumStep = function(minimumStep) { + if (minimumStep == undefined) { + return; } - var me = this; - this.defaultOptions = { - start: null, - end: null, - - autoResize: true, - - orientation: 'bottom', - width: null, - height: null, - maxHeight: null, - minHeight: null - }; - this.options = util.deepExtend({}, this.defaultOptions); + var stepYear = (1000 * 60 * 60 * 24 * 30 * 12); + var stepMonth = (1000 * 60 * 60 * 24 * 30); + var stepDay = (1000 * 60 * 60 * 24); + var stepHour = (1000 * 60 * 60); + var stepMinute = (1000 * 60); + var stepSecond = (1000); + var stepMillisecond= (1); - // Create the DOM, props, and emitter - this._create(container); + // find the smallest step that is larger than the provided minimumStep + if (stepYear*1000 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1000;} + if (stepYear*500 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 500;} + if (stepYear*100 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 100;} + if (stepYear*50 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 50;} + if (stepYear*10 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 10;} + if (stepYear*5 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 5;} + if (stepYear > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1;} + if (stepMonth*3 > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 3;} + if (stepMonth > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 1;} + if (stepDay*5 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 5;} + if (stepDay*2 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 2;} + if (stepDay > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 1;} + if (stepDay/2 > minimumStep) {this.scale = TimeStep.SCALE.WEEKDAY; this.step = 1;} + if (stepHour*4 > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 4;} + if (stepHour > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 1;} + if (stepMinute*15 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 15;} + if (stepMinute*10 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 10;} + if (stepMinute*5 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 5;} + if (stepMinute > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 1;} + if (stepSecond*15 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 15;} + if (stepSecond*10 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 10;} + if (stepSecond*5 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 5;} + if (stepSecond > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 1;} + if (stepMillisecond*200 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 200;} + if (stepMillisecond*100 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 100;} + if (stepMillisecond*50 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 50;} + if (stepMillisecond*10 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 10;} + if (stepMillisecond*5 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 5;} + if (stepMillisecond > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 1;} + }; - // all components listed here will be repainted automatically - this.components = []; + /** + * Snap a date to a rounded value. + * The snap intervals are dependent on the current scale and step. + * @param {Date} date the date to be snapped. + * @return {Date} snappedDate + */ + TimeStep.prototype.snap = function(date) { + var clone = new Date(date.valueOf()); - this.body = { - dom: this.dom, - domProps: this.props, - emitter: { - on: this.on.bind(this), - off: this.off.bind(this), - emit: this.emit.bind(this) - }, - util: { - snap: null, // will be specified after TimeAxis is created - toScreen: me._toScreen.bind(me), - toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width - toTime: me._toTime.bind(me), - toGlobalTime : me._toGlobalTime.bind(me) + if (this.scale == TimeStep.SCALE.YEAR) { + var year = clone.getFullYear() + Math.round(clone.getMonth() / 12); + clone.setFullYear(Math.round(year / this.step) * this.step); + clone.setMonth(0); + clone.setDate(0); + clone.setHours(0); + clone.setMinutes(0); + clone.setSeconds(0); + clone.setMilliseconds(0); + } + else if (this.scale == TimeStep.SCALE.MONTH) { + if (clone.getDate() > 15) { + clone.setDate(1); + clone.setMonth(clone.getMonth() + 1); + // important: first set Date to 1, after that change the month. + } + else { + clone.setDate(1); } - }; - - // range - this.range = new Range(this.body); - this.components.push(this.range); - this.body.range = this.range; - - // time axis - this.timeAxis = new TimeAxis(this.body); - this.components.push(this.timeAxis); - this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis); - - // current time bar - this.currentTime = new CurrentTime(this.body); - this.components.push(this.currentTime); - - // custom time bar - // Note: time bar will be attached in this.setOptions when selected - this.customTime = new CustomTime(this.body); - this.components.push(this.customTime); - - // item set - this.itemSet = new ItemSet(this.body); - this.components.push(this.itemSet); - - this.itemsData = null; // DataSet - this.groupsData = null; // DataSet - // apply options - if (options) { - this.setOptions(options); + clone.setHours(0); + clone.setMinutes(0); + clone.setSeconds(0); + clone.setMilliseconds(0); } - - // create itemset - if (items) { - this.setItems(items); + else if (this.scale == TimeStep.SCALE.DAY) { + //noinspection FallthroughInSwitchStatementJS + switch (this.step) { + case 5: + case 2: + clone.setHours(Math.round(clone.getHours() / 24) * 24); break; + default: + clone.setHours(Math.round(clone.getHours() / 12) * 12); break; + } + clone.setMinutes(0); + clone.setSeconds(0); + clone.setMilliseconds(0); } - else { - this.redraw(); + else if (this.scale == TimeStep.SCALE.WEEKDAY) { + //noinspection FallthroughInSwitchStatementJS + switch (this.step) { + case 5: + case 2: + clone.setHours(Math.round(clone.getHours() / 12) * 12); break; + default: + clone.setHours(Math.round(clone.getHours() / 6) * 6); break; + } + clone.setMinutes(0); + clone.setSeconds(0); + clone.setMilliseconds(0); } - } - - // turn Timeline into an event emitter - Emitter(Timeline.prototype); + else if (this.scale == TimeStep.SCALE.HOUR) { + switch (this.step) { + case 4: + clone.setMinutes(Math.round(clone.getMinutes() / 60) * 60); break; + default: + clone.setMinutes(Math.round(clone.getMinutes() / 30) * 30); break; + } + clone.setSeconds(0); + clone.setMilliseconds(0); + } else if (this.scale == TimeStep.SCALE.MINUTE) { + //noinspection FallthroughInSwitchStatementJS + switch (this.step) { + case 15: + case 10: + clone.setMinutes(Math.round(clone.getMinutes() / 5) * 5); + clone.setSeconds(0); + break; + case 5: + clone.setSeconds(Math.round(clone.getSeconds() / 60) * 60); break; + default: + clone.setSeconds(Math.round(clone.getSeconds() / 30) * 30); break; + } + clone.setMilliseconds(0); + } + else if (this.scale == TimeStep.SCALE.SECOND) { + //noinspection FallthroughInSwitchStatementJS + switch (this.step) { + case 15: + case 10: + clone.setSeconds(Math.round(clone.getSeconds() / 5) * 5); + clone.setMilliseconds(0); + break; + case 5: + clone.setMilliseconds(Math.round(clone.getMilliseconds() / 1000) * 1000); break; + default: + clone.setMilliseconds(Math.round(clone.getMilliseconds() / 500) * 500); break; + } + } + else if (this.scale == TimeStep.SCALE.MILLISECOND) { + var step = this.step > 5 ? this.step / 2 : 1; + clone.setMilliseconds(Math.round(clone.getMilliseconds() / step) * step); + } + + return clone; + }; /** - * Create the main DOM for the Timeline: a root panel containing left, right, - * top, bottom, content, and background panel. - * @param {Element} container The container element where the Timeline will - * be attached. - * @private + * Check if the current value is a major value (for example when the step + * is DAY, a major value is each first day of the MONTH) + * @return {boolean} true if current date is major, else false. */ - Timeline.prototype._create = function (container) { - this.dom = {}; - - this.dom.root = document.createElement('div'); - this.dom.background = document.createElement('div'); - this.dom.backgroundVertical = document.createElement('div'); - this.dom.backgroundHorizontal = document.createElement('div'); - this.dom.centerContainer = document.createElement('div'); - this.dom.leftContainer = document.createElement('div'); - this.dom.rightContainer = document.createElement('div'); - this.dom.center = document.createElement('div'); - this.dom.left = document.createElement('div'); - this.dom.right = document.createElement('div'); - this.dom.top = document.createElement('div'); - this.dom.bottom = document.createElement('div'); - this.dom.shadowTop = document.createElement('div'); - this.dom.shadowBottom = document.createElement('div'); - this.dom.shadowTopLeft = document.createElement('div'); - this.dom.shadowBottomLeft = document.createElement('div'); - this.dom.shadowTopRight = document.createElement('div'); - this.dom.shadowBottomRight = document.createElement('div'); - - this.dom.background.className = 'vispanel background'; - this.dom.backgroundVertical.className = 'vispanel background vertical'; - this.dom.backgroundHorizontal.className = 'vispanel background horizontal'; - this.dom.centerContainer.className = 'vispanel center'; - this.dom.leftContainer.className = 'vispanel left'; - this.dom.rightContainer.className = 'vispanel right'; - this.dom.top.className = 'vispanel top'; - this.dom.bottom.className = 'vispanel bottom'; - this.dom.left.className = 'content'; - this.dom.center.className = 'content'; - this.dom.right.className = 'content'; - this.dom.shadowTop.className = 'shadow top'; - this.dom.shadowBottom.className = 'shadow bottom'; - this.dom.shadowTopLeft.className = 'shadow top'; - this.dom.shadowBottomLeft.className = 'shadow bottom'; - this.dom.shadowTopRight.className = 'shadow top'; - this.dom.shadowBottomRight.className = 'shadow bottom'; - - this.dom.root.appendChild(this.dom.background); - this.dom.root.appendChild(this.dom.backgroundVertical); - this.dom.root.appendChild(this.dom.backgroundHorizontal); - this.dom.root.appendChild(this.dom.centerContainer); - this.dom.root.appendChild(this.dom.leftContainer); - this.dom.root.appendChild(this.dom.rightContainer); - this.dom.root.appendChild(this.dom.top); - this.dom.root.appendChild(this.dom.bottom); - - this.dom.centerContainer.appendChild(this.dom.center); - this.dom.leftContainer.appendChild(this.dom.left); - this.dom.rightContainer.appendChild(this.dom.right); - - this.dom.centerContainer.appendChild(this.dom.shadowTop); - this.dom.centerContainer.appendChild(this.dom.shadowBottom); - this.dom.leftContainer.appendChild(this.dom.shadowTopLeft); - this.dom.leftContainer.appendChild(this.dom.shadowBottomLeft); - this.dom.rightContainer.appendChild(this.dom.shadowTopRight); - this.dom.rightContainer.appendChild(this.dom.shadowBottomRight); - - this.on('rangechange', this.redraw.bind(this)); - this.on('change', this.redraw.bind(this)); - this.on('touch', this._onTouch.bind(this)); - this.on('pinch', this._onPinch.bind(this)); - this.on('dragstart', this._onDragStart.bind(this)); - this.on('drag', this._onDrag.bind(this)); - - // create event listeners for all interesting events, these events will be - // emitted via emitter - this.hammer = Hammer(this.dom.root, { - prevent_default: true - }); - this.listeners = {}; - - var me = this; - var events = [ - 'touch', 'pinch', - 'tap', 'doubletap', 'hold', - 'dragstart', 'drag', 'dragend', - 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed for Firefox - ]; - events.forEach(function (event) { - var listener = function () { - var args = [event].concat(Array.prototype.slice.call(arguments, 0)); - me.emit.apply(me, args); - }; - me.hammer.on(event, listener); - me.listeners[event] = listener; - }); - - // size properties of each of the panels - this.props = { - root: {}, - background: {}, - centerContainer: {}, - leftContainer: {}, - rightContainer: {}, - center: {}, - left: {}, - right: {}, - top: {}, - bottom: {}, - border: {}, - scrollTop: 0, - scrollTopMin: 0 - }; - this.touch = {}; // store state information needed for touch events - - // attach the root panel to the provided container - if (!container) throw new Error('No container provided'); - container.appendChild(this.dom.root); + TimeStep.prototype.isMajor = function() { + switch (this.scale) { + case TimeStep.SCALE.MILLISECOND: + return (this.current.getMilliseconds() == 0); + case TimeStep.SCALE.SECOND: + return (this.current.getSeconds() == 0); + case TimeStep.SCALE.MINUTE: + return (this.current.getHours() == 0) && (this.current.getMinutes() == 0); + // Note: this is no bug. Major label is equal for both minute and hour scale + case TimeStep.SCALE.HOUR: + return (this.current.getHours() == 0); + case TimeStep.SCALE.WEEKDAY: // intentional fall through + case TimeStep.SCALE.DAY: + return (this.current.getDate() == 1); + case TimeStep.SCALE.MONTH: + return (this.current.getMonth() == 0); + case TimeStep.SCALE.YEAR: + return false; + default: + return false; + } }; + /** - * Destroy the Timeline, clean up all DOM elements and event listeners. + * Returns formatted text for the minor axislabel, depending on the current + * date and the scale. For example when scale is MINUTE, the current time is + * formatted as "hh:mm". + * @param {Date} [date] custom date. if not provided, current date is taken */ - Timeline.prototype.destroy = function () { - // unbind datasets - this.clear(); - - // remove all event listeners - this.off(); - - // stop checking for changed size - this._stopAutoResize(); - - // remove from DOM - if (this.dom.root.parentNode) { - this.dom.root.parentNode.removeChild(this.dom.root); + TimeStep.prototype.getLabelMinor = function(date) { + if (date == undefined) { + date = this.current; } - this.dom = null; - // cleanup hammer touch events - for (var event in this.listeners) { - if (this.listeners.hasOwnProperty(event)) { - delete this.listeners[event]; - } + switch (this.scale) { + case TimeStep.SCALE.MILLISECOND: return moment(date).format('SSS'); + case TimeStep.SCALE.SECOND: return moment(date).format('s'); + case TimeStep.SCALE.MINUTE: return moment(date).format('HH:mm'); + case TimeStep.SCALE.HOUR: return moment(date).format('HH:mm'); + case TimeStep.SCALE.WEEKDAY: return moment(date).format('ddd D'); + case TimeStep.SCALE.DAY: return moment(date).format('D'); + case TimeStep.SCALE.MONTH: return moment(date).format('MMM'); + case TimeStep.SCALE.YEAR: return moment(date).format('YYYY'); + default: return ''; } - this.listeners = null; - this.hammer = null; - - // give all components the opportunity to cleanup - this.components.forEach(function (component) { - component.destroy(); - }); - - this.body = null; }; + /** - * Set options. Options will be passed to all components loaded in the Timeline. - * @param {Object} [options] - * {String} orientation - * Vertical orientation for the Timeline, - * can be 'bottom' (default) or 'top'. - * {String | Number} width - * Width for the timeline, a number in pixels or - * a css string like '1000px' or '75%'. '100%' by default. - * {String | Number} height - * Fixed height for the Timeline, a number in pixels or - * a css string like '400px' or '75%'. If undefined, - * The Timeline will automatically size such that - * its contents fit. - * {String | Number} minHeight - * Minimum height for the Timeline, a number in pixels or - * a css string like '400px' or '75%'. - * {String | Number} maxHeight - * Maximum height for the Timeline, a number in pixels or - * a css string like '400px' or '75%'. - * {Number | Date | String} start - * Start date for the visible window - * {Number | Date | String} end - * End date for the visible window + * Returns formatted text for the major axis label, depending on the current + * date and the scale. For example when scale is MINUTE, the major scale is + * hours, and the hour will be formatted as "hh". + * @param {Date} [date] custom date. if not provided, current date is taken */ - Timeline.prototype.setOptions = function (options) { - if (options) { - // copy the known options - var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'orientation']; - util.selectiveExtend(fields, this.options, options); - - // enable/disable autoResize - this._initAutoResize(); + TimeStep.prototype.getLabelMajor = function(date) { + if (date == undefined) { + date = this.current; } - // propagate options to all components - this.components.forEach(function (component) { - component.setOptions(options); - }); - - // TODO: remove deprecation error one day (deprecated since version 0.8.0) - if (options && options.order) { - throw new Error('Option order is deprecated. There is no replacement for this feature.'); + //noinspection FallthroughInSwitchStatementJS + switch (this.scale) { + case TimeStep.SCALE.MILLISECOND:return moment(date).format('HH:mm:ss'); + case TimeStep.SCALE.SECOND: return moment(date).format('D MMMM HH:mm'); + case TimeStep.SCALE.MINUTE: + case TimeStep.SCALE.HOUR: return moment(date).format('ddd D MMMM'); + case TimeStep.SCALE.WEEKDAY: + case TimeStep.SCALE.DAY: return moment(date).format('MMMM YYYY'); + case TimeStep.SCALE.MONTH: return moment(date).format('YYYY'); + case TimeStep.SCALE.YEAR: return ''; + default: return ''; } - - // redraw everything - this.redraw(); }; - /** - * Set a custom time bar - * @param {Date} time - */ - Timeline.prototype.setCustomTime = function (time) { - if (!this.customTime) { - throw new Error('Cannot get custom time: Custom time bar is not enabled'); - } + module.exports = TimeStep; - this.customTime.setCustomTime(time); - }; - /** - * Retrieve the current custom time. - * @return {Date} customTime - */ - Timeline.prototype.getCustomTime = function() { - if (!this.customTime) { - throw new Error('Cannot get custom time: Custom time bar is not enabled'); - } +/***/ }, +/* 11 */ +/***/ function(module, exports, __webpack_require__) { - return this.customTime.getCustomTime(); - }; + var Emitter = __webpack_require__(41); + var DataSet = __webpack_require__(3); + var DataView = __webpack_require__(4); + var Point3d = __webpack_require__(33); + var Point2d = __webpack_require__(34); + var Filter = __webpack_require__(35); + var StepNumber = __webpack_require__(36); /** - * Set items - * @param {vis.DataSet | Array | google.visualization.DataTable | null} items + * @constructor Graph3d + * Graph3d displays data in 3d. + * + * Graph3d is developed in javascript as a Google Visualization Chart. + * + * @param {Element} container The DOM element in which the Graph3d will + * be created. Normally a div element. + * @param {DataSet | DataView | Array} [data] + * @param {Object} [options] */ - Timeline.prototype.setItems = function(items) { - var initialLoad = (this.itemsData == null); - - // convert to type DataSet when needed - var newDataSet; - if (!items) { - newDataSet = null; - } - else if (items instanceof DataSet || items instanceof DataView) { - newDataSet = items; - } - else { - // turn an array into a dataset - newDataSet = new DataSet(items, { - type: { - start: 'Date', - end: 'Date' - } - }); + function Graph3d(container, data, options) { + if (!(this instanceof Graph3d)) { + throw new SyntaxError('Constructor must be called with the new operator'); } - // set items - this.itemsData = newDataSet; - this.itemSet && this.itemSet.setItems(newDataSet); + // create variables and set default values + this.containerElement = container; + this.width = '400px'; + this.height = '400px'; + this.margin = 10; // px + this.defaultXCenter = '55%'; + this.defaultYCenter = '50%'; - if (initialLoad && ('start' in this.options || 'end' in this.options)) { - this.fit(); + this.xLabel = 'x'; + this.yLabel = 'y'; + this.zLabel = 'z'; + this.filterLabel = 'time'; + this.legendLabel = 'value'; - var start = ('start' in this.options) ? util.convert(this.options.start, 'Date') : null; - var end = ('end' in this.options) ? util.convert(this.options.end, 'Date') : null; + this.style = Graph3d.STYLE.DOT; + this.showPerspective = true; + this.showGrid = true; + this.keepAspectRatio = true; + this.showShadow = false; + this.showGrayBottom = false; // TODO: this does not work correctly + this.showTooltip = false; + this.verticalRatio = 0.5; // 0.1 to 1.0, where 1.0 results in a 'cube' - this.setWindow(start, end); + this.animationInterval = 1000; // milliseconds + this.animationPreload = false; + + this.camera = new Graph3d.Camera(); + this.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window? + + this.dataTable = null; // The original data table + this.dataPoints = null; // The table with point objects + + // the column indexes + this.colX = undefined; + this.colY = undefined; + this.colZ = undefined; + this.colValue = undefined; + this.colFilter = undefined; + + this.xMin = 0; + this.xStep = undefined; // auto by default + this.xMax = 1; + this.yMin = 0; + this.yStep = undefined; // auto by default + this.yMax = 1; + this.zMin = 0; + this.zStep = undefined; // auto by default + this.zMax = 1; + this.valueMin = 0; + this.valueMax = 1; + this.xBarWidth = 1; + this.yBarWidth = 1; + // TODO: customize axis range + + // constants + this.colorAxis = '#4D4D4D'; + this.colorGrid = '#D3D3D3'; + this.colorDot = '#7DC1FF'; + this.colorDotBorder = '#3267D2'; + + // create a frame and canvas + this.create(); + + // apply options (also when undefined) + this.setOptions(options); + + // apply data + if (data) { + this.setData(data); } + } + + // Extend Graph3d with an Emitter mixin + Emitter(Graph3d.prototype); + + /** + * @class Camera + * The camera is mounted on a (virtual) camera arm. The camera arm can rotate + * The camera is always looking in the direction of the origin of the arm. + * This way, the camera always rotates around one fixed point, the location + * of the camera arm. + * + * Documentation: + * http://en.wikipedia.org/wiki/3D_projection + */ + Graph3d.Camera = function () { + this.armLocation = new Point3d(); + this.armRotation = {}; + this.armRotation.horizontal = 0; + this.armRotation.vertical = 0; + this.armLength = 1.7; + + this.cameraLocation = new Point3d(); + this.cameraRotation = new Point3d(0.5*Math.PI, 0, 0); + + this.calculateCameraOrientation(); }; /** - * Set groups - * @param {vis.DataSet | Array | google.visualization.DataTable} groups + * Set the location (origin) of the arm + * @param {Number} x Normalized value of x + * @param {Number} y Normalized value of y + * @param {Number} z Normalized value of z */ - Timeline.prototype.setGroups = function(groups) { - // convert to type DataSet when needed - var newDataSet; - if (!groups) { - newDataSet = null; - } - else if (groups instanceof DataSet || groups instanceof DataView) { - newDataSet = groups; - } - else { - // turn an array into a dataset - newDataSet = new DataSet(groups); - } + Graph3d.Camera.prototype.setArmLocation = function(x, y, z) { + this.armLocation.x = x; + this.armLocation.y = y; + this.armLocation.z = z; - this.groupsData = newDataSet; - this.itemSet.setGroups(newDataSet); + this.calculateCameraOrientation(); }; /** - * Clear the Timeline. By Default, items, groups and options are cleared. - * Example usage: - * - * timeline.clear(); // clear items, groups, and options - * timeline.clear({options: true}); // clear options only - * - * @param {Object} [what] Optionally specify what to clear. By default: - * {items: true, groups: true, options: true} + * Set the rotation of the camera arm + * @param {Number} horizontal The horizontal rotation, between 0 and 2*PI. + * Optional, can be left undefined. + * @param {Number} vertical The vertical rotation, between 0 and 0.5*PI + * if vertical=0.5*PI, the graph is shown from the + * top. Optional, can be left undefined. */ - Timeline.prototype.clear = function(what) { - // clear items - if (!what || what.items) { - this.setItems(null); + Graph3d.Camera.prototype.setArmRotation = function(horizontal, vertical) { + if (horizontal !== undefined) { + this.armRotation.horizontal = horizontal; } - // clear groups - if (!what || what.groups) { - this.setGroups(null); + if (vertical !== undefined) { + this.armRotation.vertical = vertical; + if (this.armRotation.vertical < 0) this.armRotation.vertical = 0; + if (this.armRotation.vertical > 0.5*Math.PI) this.armRotation.vertical = 0.5*Math.PI; } - // clear options of timeline and of each of the components - if (!what || what.options) { - this.components.forEach(function (component) { - component.setOptions(component.defaultOptions); - }); - - this.setOptions(this.defaultOptions); // this will also do a redraw + if (horizontal !== undefined || vertical !== undefined) { + this.calculateCameraOrientation(); } }; /** - * Set Timeline window such that it fits all items + * Retrieve the current arm rotation + * @return {object} An object with parameters horizontal and vertical */ - Timeline.prototype.fit = function() { - // apply the data range as range - var dataRange = this.getItemRange(); + Graph3d.Camera.prototype.getArmRotation = function() { + var rot = {}; + rot.horizontal = this.armRotation.horizontal; + rot.vertical = this.armRotation.vertical; - // add 5% space on both sides - var start = dataRange.min; - var end = dataRange.max; - if (start != null && end != null) { - var interval = (end.valueOf() - start.valueOf()); - if (interval <= 0) { - // prevent an empty interval - interval = 24 * 60 * 60 * 1000; // 1 day - } - start = new Date(start.valueOf() - interval * 0.05); - end = new Date(end.valueOf() + interval * 0.05); - } + return rot; + }; - // skip range set if there is no start and end date - if (start === null && end === null) { + /** + * Set the (normalized) length of the camera arm. + * @param {Number} length A length between 0.71 and 5.0 + */ + Graph3d.Camera.prototype.setArmLength = function(length) { + if (length === undefined) return; - } - this.range.setRange(start, end); + this.armLength = length; + + // Radius must be larger than the corner of the graph, + // which has a distance of sqrt(0.5^2+0.5^2) = 0.71 from the center of the + // graph + if (this.armLength < 0.71) this.armLength = 0.71; + if (this.armLength > 5.0) this.armLength = 5.0; + + this.calculateCameraOrientation(); }; /** - * Get the data range of the item set. - * @returns {{min: Date, max: Date}} range A range with a start and end Date. - * When no minimum is found, min==null - * When no maximum is found, max==null + * Retrieve the arm length + * @return {Number} length */ - Timeline.prototype.getItemRange = function() { - // calculate min from start filed - var dataset = this.itemsData.getDataSet(), - min = null, - max = null; + Graph3d.Camera.prototype.getArmLength = function() { + return this.armLength; + }; - if (dataset) { - // calculate the minimum value of the field 'start' - var minItem = dataset.min('start'); - min = minItem ? util.convert(minItem.start, 'Date').valueOf() : null; - // Note: we convert first to Date and then to number because else - // a conversion from ISODate to Number will fail - - // calculate maximum value of fields 'start' and 'end' - var maxStartItem = dataset.max('start'); - if (maxStartItem) { - max = util.convert(maxStartItem.start, 'Date').valueOf(); - } - var maxEndItem = dataset.max('end'); - if (maxEndItem) { - if (max == null) { - max = util.convert(maxEndItem.end, 'Date').valueOf(); - } - else { - max = Math.max(max, util.convert(maxEndItem.end, 'Date').valueOf()); - } - } - } - - return { - min: (min != null) ? new Date(min) : null, - max: (max != null) ? new Date(max) : null - }; + /** + * Retrieve the camera location + * @return {Point3d} cameraLocation + */ + Graph3d.Camera.prototype.getCameraLocation = function() { + return this.cameraLocation; }; /** - * Set selected items by their id. Replaces the current selection - * Unknown id's are silently ignored. - * @param {Array} [ids] An array with zero or more id's of the items to be - * selected. If ids is an empty array, all items will be - * unselected. + * Retrieve the camera rotation + * @return {Point3d} cameraRotation */ - Timeline.prototype.setSelection = function(ids) { - this.itemSet && this.itemSet.setSelection(ids); + Graph3d.Camera.prototype.getCameraRotation = function() { + return this.cameraRotation; }; /** - * Get the selected items by their id - * @return {Array} ids The ids of the selected items + * Calculate the location and rotation of the camera based on the + * position and orientation of the camera arm */ - Timeline.prototype.getSelection = function() { - return this.itemSet && this.itemSet.getSelection() || []; + Graph3d.Camera.prototype.calculateCameraOrientation = function() { + // calculate location of the camera + this.cameraLocation.x = this.armLocation.x - this.armLength * Math.sin(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical); + this.cameraLocation.y = this.armLocation.y - this.armLength * Math.cos(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical); + this.cameraLocation.z = this.armLocation.z + this.armLength * Math.sin(this.armRotation.vertical); + + // calculate rotation of the camera + this.cameraRotation.x = Math.PI/2 - this.armRotation.vertical; + this.cameraRotation.y = 0; + this.cameraRotation.z = -this.armRotation.horizontal; }; /** - * Set the visible window. Both parameters are optional, you can change only - * start or only end. Syntax: - * - * TimeLine.setWindow(start, end) - * TimeLine.setWindow(range) - * - * Where start and end can be a Date, number, or string, and range is an - * object with properties start and end. - * - * @param {Date | Number | String | Object} [start] Start date of visible window - * @param {Date | Number | String} [end] End date of visible window + * Calculate the scaling values, dependent on the range in x, y, and z direction */ - Timeline.prototype.setWindow = function(start, end) { - if (arguments.length == 1) { - var range = arguments[0]; - this.range.setRange(range.start, range.end); - } - else { - this.range.setRange(start, end); + Graph3d.prototype._setScale = function() { + this.scale = new Point3d(1 / (this.xMax - this.xMin), + 1 / (this.yMax - this.yMin), + 1 / (this.zMax - this.zMin)); + + // keep aspect ration between x and y scale if desired + if (this.keepAspectRatio) { + if (this.scale.x < this.scale.y) { + //noinspection JSSuspiciousNameCombination + this.scale.y = this.scale.x; + } + else { + //noinspection JSSuspiciousNameCombination + this.scale.x = this.scale.y; + } } + + // scale the vertical axis + this.scale.z *= this.verticalRatio; + // TODO: can this be automated? verticalRatio? + + // determine scale for (optional) value + this.scale.value = 1 / (this.valueMax - this.valueMin); + + // position the camera arm + var xCenter = (this.xMax + this.xMin) / 2 * this.scale.x; + var yCenter = (this.yMax + this.yMin) / 2 * this.scale.y; + var zCenter = (this.zMax + this.zMin) / 2 * this.scale.z; + this.camera.setArmLocation(xCenter, yCenter, zCenter); }; + /** - * Get the visible window - * @return {{start: Date, end: Date}} Visible range + * Convert a 3D location to a 2D location on screen + * http://en.wikipedia.org/wiki/3D_projection + * @param {Point3d} point3d A 3D point with parameters x, y, z + * @return {Point2d} point2d A 2D point with parameters x, y */ - Timeline.prototype.getWindow = function() { - var range = this.range.getRange(); - return { - start: new Date(range.start), - end: new Date(range.end) - }; + Graph3d.prototype._convert3Dto2D = function(point3d) { + var translation = this._convertPointToTranslation(point3d); + return this._convertTranslationToScreen(translation); }; /** - * Force a redraw of the Timeline. Can be useful to manually redraw when - * option autoResize=false + * Convert a 3D location its translation seen from the camera + * http://en.wikipedia.org/wiki/3D_projection + * @param {Point3d} point3d A 3D point with parameters x, y, z + * @return {Point3d} translation A 3D point with parameters x, y, z This is + * the translation of the point, seen from the + * camera */ - Timeline.prototype.redraw = function() { - var resized = false, - options = this.options, - props = this.props, - dom = this.dom; + Graph3d.prototype._convertPointToTranslation = function(point3d) { + var ax = point3d.x * this.scale.x, + ay = point3d.y * this.scale.y, + az = point3d.z * this.scale.z, - if (!dom) return; // when destroyed + cx = this.camera.getCameraLocation().x, + cy = this.camera.getCameraLocation().y, + cz = this.camera.getCameraLocation().z, - // update class names - dom.root.className = 'vis timeline root ' + options.orientation; + // calculate angles + sinTx = Math.sin(this.camera.getCameraRotation().x), + cosTx = Math.cos(this.camera.getCameraRotation().x), + sinTy = Math.sin(this.camera.getCameraRotation().y), + cosTy = Math.cos(this.camera.getCameraRotation().y), + sinTz = Math.sin(this.camera.getCameraRotation().z), + cosTz = Math.cos(this.camera.getCameraRotation().z), - // update root width and height options - dom.root.style.maxHeight = util.option.asSize(options.maxHeight, ''); - dom.root.style.minHeight = util.option.asSize(options.minHeight, ''); - dom.root.style.width = util.option.asSize(options.width, ''); + // calculate translation + dx = cosTy * (sinTz * (ay - cy) + cosTz * (ax - cx)) - sinTy * (az - cz), + dy = sinTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) + cosTx * (cosTz * (ay - cy) - sinTz * (ax-cx)), + dz = cosTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) - sinTx * (cosTz * (ay - cy) - sinTz * (ax-cx)); - // calculate border widths - props.border.left = (dom.centerContainer.offsetWidth - dom.centerContainer.clientWidth) / 2; - props.border.right = props.border.left; - props.border.top = (dom.centerContainer.offsetHeight - dom.centerContainer.clientHeight) / 2; - props.border.bottom = props.border.top; - var borderRootHeight= dom.root.offsetHeight - dom.root.clientHeight; - var borderRootWidth = dom.root.offsetWidth - dom.root.clientWidth; + return new Point3d(dx, dy, dz); + }; - // calculate the heights. If any of the side panels is empty, we set the height to - // minus the border width, such that the border will be invisible - props.center.height = dom.center.offsetHeight; - props.left.height = dom.left.offsetHeight; - props.right.height = dom.right.offsetHeight; - props.top.height = dom.top.clientHeight || -props.border.top; - props.bottom.height = dom.bottom.clientHeight || -props.border.bottom; + /** + * Convert a translation point to a point on the screen + * @param {Point3d} translation A 3D point with parameters x, y, z This is + * the translation of the point, seen from the + * camera + * @return {Point2d} point2d A 2D point with parameters x, y + */ + Graph3d.prototype._convertTranslationToScreen = function(translation) { + var ex = this.eye.x, + ey = this.eye.y, + ez = this.eye.z, + dx = translation.x, + dy = translation.y, + dz = translation.z; - // TODO: compensate borders when any of the panels is empty. + // calculate position on screen from translation + var bx; + var by; + if (this.showPerspective) { + bx = (dx - ex) * (ez / dz); + by = (dy - ey) * (ez / dz); + } + else { + bx = dx * -(ez / this.camera.getArmLength()); + by = dy * -(ez / this.camera.getArmLength()); + } - // apply auto height - // TODO: only calculate autoHeight when needed (else we cause an extra reflow/repaint of the DOM) - var contentHeight = Math.max(props.left.height, props.center.height, props.right.height); - var autoHeight = props.top.height + contentHeight + props.bottom.height + - borderRootHeight + props.border.top + props.border.bottom; - dom.root.style.height = util.option.asSize(options.height, autoHeight + 'px'); + // shift and scale the point to the center of the screen + // use the width of the graph to scale both horizontally and vertically. + return new Point2d( + this.xcenter + bx * this.frame.canvas.clientWidth, + this.ycenter - by * this.frame.canvas.clientWidth); + }; - // calculate heights of the content panels - props.root.height = dom.root.offsetHeight; - props.background.height = props.root.height - borderRootHeight; - var containerHeight = props.root.height - props.top.height - props.bottom.height - - borderRootHeight; - props.centerContainer.height = containerHeight; - props.leftContainer.height = containerHeight; - props.rightContainer.height = props.leftContainer.height; + /** + * Set the background styling for the graph + * @param {string | {fill: string, stroke: string, strokeWidth: string}} backgroundColor + */ + Graph3d.prototype._setBackgroundColor = function(backgroundColor) { + var fill = 'white'; + var stroke = 'gray'; + var strokeWidth = 1; - // calculate the widths of the panels - props.root.width = dom.root.offsetWidth; - props.background.width = props.root.width - borderRootWidth; - props.left.width = dom.leftContainer.clientWidth || -props.border.left; - props.leftContainer.width = props.left.width; - props.right.width = dom.rightContainer.clientWidth || -props.border.right; - props.rightContainer.width = props.right.width; - var centerWidth = props.root.width - props.left.width - props.right.width - borderRootWidth; - props.center.width = centerWidth; - props.centerContainer.width = centerWidth; - props.top.width = centerWidth; - props.bottom.width = centerWidth; + if (typeof(backgroundColor) === 'string') { + fill = backgroundColor; + stroke = 'none'; + strokeWidth = 0; + } + else if (typeof(backgroundColor) === 'object') { + if (backgroundColor.fill !== undefined) fill = backgroundColor.fill; + if (backgroundColor.stroke !== undefined) stroke = backgroundColor.stroke; + if (backgroundColor.strokeWidth !== undefined) strokeWidth = backgroundColor.strokeWidth; + } + else if (backgroundColor === undefined) { + // use use defaults + } + else { + throw 'Unsupported type of backgroundColor'; + } - // resize the panels - dom.background.style.height = props.background.height + 'px'; - dom.backgroundVertical.style.height = props.background.height + 'px'; - dom.backgroundHorizontal.style.height = props.centerContainer.height + 'px'; - dom.centerContainer.style.height = props.centerContainer.height + 'px'; - dom.leftContainer.style.height = props.leftContainer.height + 'px'; - dom.rightContainer.style.height = props.rightContainer.height + 'px'; - - dom.background.style.width = props.background.width + 'px'; - dom.backgroundVertical.style.width = props.centerContainer.width + 'px'; - dom.backgroundHorizontal.style.width = props.background.width + 'px'; - dom.centerContainer.style.width = props.center.width + 'px'; - dom.top.style.width = props.top.width + 'px'; - dom.bottom.style.width = props.bottom.width + 'px'; - - // reposition the panels - dom.background.style.left = '0'; - dom.background.style.top = '0'; - dom.backgroundVertical.style.left = props.left.width + 'px'; - dom.backgroundVertical.style.top = '0'; - dom.backgroundHorizontal.style.left = '0'; - dom.backgroundHorizontal.style.top = props.top.height + 'px'; - dom.centerContainer.style.left = props.left.width + 'px'; - dom.centerContainer.style.top = props.top.height + 'px'; - dom.leftContainer.style.left = '0'; - dom.leftContainer.style.top = props.top.height + 'px'; - dom.rightContainer.style.left = (props.left.width + props.center.width) + 'px'; - dom.rightContainer.style.top = props.top.height + 'px'; - dom.top.style.left = props.left.width + 'px'; - dom.top.style.top = '0'; - dom.bottom.style.left = props.left.width + 'px'; - dom.bottom.style.top = (props.top.height + props.centerContainer.height) + 'px'; - - // update the scrollTop, feasible range for the offset can be changed - // when the height of the Timeline or of the contents of the center changed - this._updateScrollTop(); - - // reposition the scrollable contents - var offset = this.props.scrollTop; - if (options.orientation == 'bottom') { - offset += Math.max(this.props.centerContainer.height - this.props.center.height - - this.props.border.top - this.props.border.bottom, 0); - } - dom.center.style.left = '0'; - dom.center.style.top = offset + 'px'; - dom.left.style.left = '0'; - dom.left.style.top = offset + 'px'; - dom.right.style.left = '0'; - dom.right.style.top = offset + 'px'; - - // show shadows when vertical scrolling is available - var visibilityTop = this.props.scrollTop == 0 ? 'hidden' : ''; - var visibilityBottom = this.props.scrollTop == this.props.scrollTopMin ? 'hidden' : ''; - dom.shadowTop.style.visibility = visibilityTop; - dom.shadowBottom.style.visibility = visibilityBottom; - dom.shadowTopLeft.style.visibility = visibilityTop; - dom.shadowBottomLeft.style.visibility = visibilityBottom; - dom.shadowTopRight.style.visibility = visibilityTop; - dom.shadowBottomRight.style.visibility = visibilityBottom; - - // redraw all components - this.components.forEach(function (component) { - resized = component.redraw() || resized; - }); - if (resized) { - // keep repainting until all sizes are settled - this.redraw(); - } + this.frame.style.backgroundColor = fill; + this.frame.style.borderColor = stroke; + this.frame.style.borderWidth = strokeWidth + 'px'; + this.frame.style.borderStyle = 'solid'; }; - // TODO: deprecated since version 1.1.0, remove some day - Timeline.prototype.repaint = function () { - throw new Error('Function repaint is deprecated. Use redraw instead.'); - }; - /** - * Convert a position on screen (pixels) to a datetime - * @param {int} x Position on the screen in pixels - * @return {Date} time The datetime the corresponds with given position x - * @private - */ - // TODO: move this function to Range - Timeline.prototype._toTime = function(x) { - var conversion = this.range.conversion(this.props.center.width); - return new Date(x / conversion.scale + conversion.offset); + /// enumerate the available styles + Graph3d.STYLE = { + BAR: 0, + BARCOLOR: 1, + BARSIZE: 2, + DOT : 3, + DOTLINE : 4, + DOTCOLOR: 5, + DOTSIZE: 6, + GRID : 7, + LINE: 8, + SURFACE : 9 }; - /** - * Convert a position on the global screen (pixels) to a datetime - * @param {int} x Position on the screen in pixels - * @return {Date} time The datetime the corresponds with given position x - * @private + * Retrieve the style index from given styleName + * @param {string} styleName Style name such as 'dot', 'grid', 'dot-line' + * @return {Number} styleNumber Enumeration value representing the style, or -1 + * when not found */ - // TODO: move this function to Range - Timeline.prototype._toGlobalTime = function(x) { - var conversion = this.range.conversion(this.props.root.width); - return new Date(x / conversion.scale + conversion.offset); - }; + Graph3d.prototype._getStyleNumber = function(styleName) { + switch (styleName) { + case 'dot': return Graph3d.STYLE.DOT; + case 'dot-line': return Graph3d.STYLE.DOTLINE; + case 'dot-color': return Graph3d.STYLE.DOTCOLOR; + case 'dot-size': return Graph3d.STYLE.DOTSIZE; + case 'line': return Graph3d.STYLE.LINE; + case 'grid': return Graph3d.STYLE.GRID; + case 'surface': return Graph3d.STYLE.SURFACE; + case 'bar': return Graph3d.STYLE.BAR; + case 'bar-color': return Graph3d.STYLE.BARCOLOR; + case 'bar-size': return Graph3d.STYLE.BARSIZE; + } - /** - * Convert a datetime (Date object) into a position on the screen - * @param {Date} time A date - * @return {int} x The position on the screen in pixels which corresponds - * with the given date. - * @private - */ - // TODO: move this function to Range - Timeline.prototype._toScreen = function(time) { - var conversion = this.range.conversion(this.props.center.width); - return (time.valueOf() - conversion.offset) * conversion.scale; + return -1; }; - /** - * Convert a datetime (Date object) into a position on the root - * This is used to get the pixel density estimate for the screen, not the center panel - * @param {Date} time A date - * @return {int} x The position on root in pixels which corresponds - * with the given date. - * @private + * Determine the indexes of the data columns, based on the given style and data + * @param {DataSet} data + * @param {Number} style */ - // TODO: move this function to Range - Timeline.prototype._toGlobalScreen = function(time) { - var conversion = this.range.conversion(this.props.root.width); - return (time.valueOf() - conversion.offset) * conversion.scale; - }; + Graph3d.prototype._determineColumnIndexes = function(data, style) { + if (this.style === Graph3d.STYLE.DOT || + this.style === Graph3d.STYLE.DOTLINE || + this.style === Graph3d.STYLE.LINE || + this.style === Graph3d.STYLE.GRID || + this.style === Graph3d.STYLE.SURFACE || + this.style === Graph3d.STYLE.BAR) { + // 3 columns expected, and optionally a 4th with filter values + this.colX = 0; + this.colY = 1; + this.colZ = 2; + this.colValue = undefined; + if (data.getNumberOfColumns() > 3) { + this.colFilter = 3; + } + } + else if (this.style === Graph3d.STYLE.DOTCOLOR || + this.style === Graph3d.STYLE.DOTSIZE || + this.style === Graph3d.STYLE.BARCOLOR || + this.style === Graph3d.STYLE.BARSIZE) { + // 4 columns expected, and optionally a 5th with filter values + this.colX = 0; + this.colY = 1; + this.colZ = 2; + this.colValue = 3; - /** - * Initialize watching when option autoResize is true - * @private - */ - Timeline.prototype._initAutoResize = function () { - if (this.options.autoResize == true) { - this._startAutoResize(); + if (data.getNumberOfColumns() > 4) { + this.colFilter = 4; + } } else { - this._stopAutoResize(); + throw 'Unknown style "' + this.style + '"'; } }; - /** - * Watch for changes in the size of the container. On resize, the Panel will - * automatically redraw itself. - * @private - */ - Timeline.prototype._startAutoResize = function () { - var me = this; + Graph3d.prototype.getNumberOfRows = function(data) { + return data.length; + } - this._stopAutoResize(); - this._onResize = function() { - if (me.options.autoResize != true) { - // stop watching when the option autoResize is changed to false - me._stopAutoResize(); - return; + Graph3d.prototype.getNumberOfColumns = function(data) { + var counter = 0; + for (var column in data[0]) { + if (data[0].hasOwnProperty(column)) { + counter++; } + } + return counter; + } - if (me.dom.root) { - // check whether the frame is resized - if ((me.dom.root.clientWidth != me.props.lastWidth) || - (me.dom.root.clientHeight != me.props.lastHeight)) { - me.props.lastWidth = me.dom.root.clientWidth; - me.props.lastHeight = me.dom.root.clientHeight; - me.emit('change'); - } + Graph3d.prototype.getDistinctValues = function(data, column) { + var distinctValues = []; + for (var i = 0; i < data.length; i++) { + if (distinctValues.indexOf(data[i][column]) == -1) { + distinctValues.push(data[i][column]); } - }; + } + return distinctValues; + } - // add event listener to window resize - util.addEventListener(window, 'resize', this._onResize); - this.watchTimer = setInterval(this._onResize, 1000); + Graph3d.prototype.getColumnRange = function(data,column) { + var minMax = {min:data[0][column],max:data[0][column]}; + for (var i = 0; i < data.length; i++) { + if (minMax.min > data[i][column]) { minMax.min = data[i][column]; } + if (minMax.max < data[i][column]) { minMax.max = data[i][column]; } + } + return minMax; }; /** - * Stop watching for a resize of the frame. - * @private + * Initialize the data from the data table. Calculate minimum and maximum values + * and column index values + * @param {Array | DataSet | DataView} rawData The data containing the items for the Graph. + * @param {Number} style Style Number */ - Timeline.prototype._stopAutoResize = function () { - if (this.watchTimer) { - clearInterval(this.watchTimer); - this.watchTimer = undefined; + Graph3d.prototype._dataInitialize = function (rawData, style) { + var me = this; + + // unsubscribe from the dataTable + if (this.dataSet) { + this.dataSet.off('*', this._onChange); } - // remove event listener on window.resize - util.removeEventListener(window, 'resize', this._onResize); - this._onResize = null; - }; + if (rawData === undefined) + return; - /** - * Start moving the timeline vertically - * @param {Event} event - * @private - */ - Timeline.prototype._onTouch = function (event) { - this.touch.allowDragging = true; - }; + if (Array.isArray(rawData)) { + rawData = new DataSet(rawData); + } - /** - * Start moving the timeline vertically - * @param {Event} event - * @private - */ - Timeline.prototype._onPinch = function (event) { - this.touch.allowDragging = false; - }; + var data; + if (rawData instanceof DataSet || rawData instanceof DataView) { + data = rawData.get(); + } + else { + throw new Error('Array, DataSet, or DataView expected'); + } - /** - * Start moving the timeline vertically - * @param {Event} event - * @private - */ - Timeline.prototype._onDragStart = function (event) { - this.touch.initialScrollTop = this.props.scrollTop; - }; + if (data.length == 0) + return; - /** - * Move the timeline vertically - * @param {Event} event - * @private - */ - Timeline.prototype._onDrag = function (event) { - // refuse to drag when we where pinching to prevent the timeline make a jump - // when releasing the fingers in opposite order from the touch screen - if (!this.touch.allowDragging) return; + this.dataSet = rawData; + this.dataTable = data; - var delta = event.gesture.deltaY; + // subscribe to changes in the dataset + this._onChange = function () { + me.setData(me.dataSet); + }; + this.dataSet.on('*', this._onChange); - var oldScrollTop = this._getScrollTop(); - var newScrollTop = this._setScrollTop(this.touch.initialScrollTop + delta); + // _determineColumnIndexes + // getNumberOfRows (points) + // getNumberOfColumns (x,y,z,v,t,t1,t2...) + // getDistinctValues (unique values?) + // getColumnRange - if (newScrollTop != oldScrollTop) { - this.redraw(); // TODO: this causes two redraws when dragging, the other is triggered by rangechange already + // determine the location of x,y,z,value,filter columns + this.colX = 'x'; + this.colY = 'y'; + this.colZ = 'z'; + this.colValue = 'style'; + this.colFilter = 'filter'; + + + + // check if a filter column is provided + if (data[0].hasOwnProperty('filter')) { + if (this.dataFilter === undefined) { + this.dataFilter = new Filter(rawData, this.colFilter, this); + this.dataFilter.setOnLoadCallback(function() {me.redraw();}); + } } - }; - /** - * Apply a scrollTop - * @param {Number} scrollTop - * @returns {Number} scrollTop Returns the applied scrollTop - * @private - */ - Timeline.prototype._setScrollTop = function (scrollTop) { - this.props.scrollTop = scrollTop; - this._updateScrollTop(); - return this.props.scrollTop; - }; - /** - * Update the current scrollTop when the height of the containers has been changed - * @returns {Number} scrollTop Returns the applied scrollTop - * @private - */ - Timeline.prototype._updateScrollTop = function () { - // recalculate the scrollTopMin - var scrollTopMin = Math.min(this.props.centerContainer.height - this.props.center.height, 0); // is negative or zero - if (scrollTopMin != this.props.scrollTopMin) { - // in case of bottom orientation, change the scrollTop such that the contents - // do not move relative to the time axis at the bottom - if (this.options.orientation == 'bottom') { - this.props.scrollTop += (scrollTopMin - this.props.scrollTopMin); + var withBars = this.style == Graph3d.STYLE.BAR || + this.style == Graph3d.STYLE.BARCOLOR || + this.style == Graph3d.STYLE.BARSIZE; + + // determine barWidth from data + if (withBars) { + if (this.defaultXBarWidth !== undefined) { + this.xBarWidth = this.defaultXBarWidth; + } + else { + var dataX = this.getDistinctValues(data,this.colX); + this.xBarWidth = (dataX[1] - dataX[0]) || 1; + } + + if (this.defaultYBarWidth !== undefined) { + this.yBarWidth = this.defaultYBarWidth; + } + else { + var dataY = this.getDistinctValues(data,this.colY); + this.yBarWidth = (dataY[1] - dataY[0]) || 1; } - this.props.scrollTopMin = scrollTopMin; } - // limit the scrollTop to the feasible scroll range - if (this.props.scrollTop > 0) this.props.scrollTop = 0; - if (this.props.scrollTop < scrollTopMin) this.props.scrollTop = scrollTopMin; + // calculate minimums and maximums + var xRange = this.getColumnRange(data,this.colX); + if (withBars) { + xRange.min -= this.xBarWidth / 2; + xRange.max += this.xBarWidth / 2; + } + this.xMin = (this.defaultXMin !== undefined) ? this.defaultXMin : xRange.min; + this.xMax = (this.defaultXMax !== undefined) ? this.defaultXMax : xRange.max; + if (this.xMax <= this.xMin) this.xMax = this.xMin + 1; + this.xStep = (this.defaultXStep !== undefined) ? this.defaultXStep : (this.xMax-this.xMin)/5; - return this.props.scrollTop; - }; + var yRange = this.getColumnRange(data,this.colY); + if (withBars) { + yRange.min -= this.yBarWidth / 2; + yRange.max += this.yBarWidth / 2; + } + this.yMin = (this.defaultYMin !== undefined) ? this.defaultYMin : yRange.min; + this.yMax = (this.defaultYMax !== undefined) ? this.defaultYMax : yRange.max; + if (this.yMax <= this.yMin) this.yMax = this.yMin + 1; + this.yStep = (this.defaultYStep !== undefined) ? this.defaultYStep : (this.yMax-this.yMin)/5; - /** - * Get the current scrollTop - * @returns {number} scrollTop - * @private - */ - Timeline.prototype._getScrollTop = function () { - return this.props.scrollTop; - }; + var zRange = this.getColumnRange(data,this.colZ); + this.zMin = (this.defaultZMin !== undefined) ? this.defaultZMin : zRange.min; + this.zMax = (this.defaultZMax !== undefined) ? this.defaultZMax : zRange.max; + if (this.zMax <= this.zMin) this.zMax = this.zMin + 1; + this.zStep = (this.defaultZStep !== undefined) ? this.defaultZStep : (this.zMax-this.zMin)/5; - module.exports = Timeline; + if (this.colValue !== undefined) { + var valueRange = this.getColumnRange(data,this.colValue); + this.valueMin = (this.defaultValueMin !== undefined) ? this.defaultValueMin : valueRange.min; + this.valueMax = (this.defaultValueMax !== undefined) ? this.defaultValueMax : valueRange.max; + if (this.valueMax <= this.valueMin) this.valueMax = this.valueMin + 1; + } + // set the scale dependent on the ranges. + this._setScale(); + }; -/***/ }, -/* 7 */ -/***/ function(module, exports, __webpack_require__) { - var Emitter = __webpack_require__(41); - var Hammer = __webpack_require__(49); - var util = __webpack_require__(1); - var DataSet = __webpack_require__(3); - var DataView = __webpack_require__(4); - var Range = __webpack_require__(9); - var TimeAxis = __webpack_require__(21); - var CurrentTime = __webpack_require__(13); - var CustomTime = __webpack_require__(14); - var LineGraph = __webpack_require__(20); /** - * Create a timeline visualization - * @param {HTMLElement} container - * @param {vis.DataSet | Array | google.visualization.DataTable} [items] - * @param {Object} [options] See Graph2d.setOptions for the available options. - * @constructor + * Filter the data based on the current filter + * @param {Array} data + * @return {Array} dataPoints Array with point objects which can be drawn on screen */ - function Graph2d (container, items, options, groups) { - var me = this; - this.defaultOptions = { - start: null, - end: null, - - autoResize: true, + Graph3d.prototype._getDataPoints = function (data) { + // TODO: store the created matrix dataPoints in the filters instead of reloading each time + var x, y, i, z, obj, point; - orientation: 'bottom', - width: null, - height: null, - maxHeight: null, - minHeight: null - }; - this.options = util.deepExtend({}, this.defaultOptions); + var dataPoints = []; - // Create the DOM, props, and emitter - this._create(container); + if (this.style === Graph3d.STYLE.GRID || + this.style === Graph3d.STYLE.SURFACE) { + // copy all values from the google data table to a matrix + // the provided values are supposed to form a grid of (x,y) positions - // all components listed here will be repainted automatically - this.components = []; + // create two lists with all present x and y values + var dataX = []; + var dataY = []; + for (i = 0; i < this.getNumberOfRows(data); i++) { + x = data[i][this.colX] || 0; + y = data[i][this.colY] || 0; - this.body = { - dom: this.dom, - domProps: this.props, - emitter: { - on: this.on.bind(this), - off: this.off.bind(this), - emit: this.emit.bind(this) - }, - util: { - snap: null, // will be specified after TimeAxis is created - toScreen: me._toScreen.bind(me), - toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width - toTime: me._toTime.bind(me), - toGlobalTime : me._toGlobalTime.bind(me) + if (dataX.indexOf(x) === -1) { + dataX.push(x); + } + if (dataY.indexOf(y) === -1) { + dataY.push(y); + } } - }; - // range - this.range = new Range(this.body); - this.components.push(this.range); - this.body.range = this.range; + function sortNumber(a, b) { + return a - b; + } + dataX.sort(sortNumber); + dataY.sort(sortNumber); - // time axis - this.timeAxis = new TimeAxis(this.body); - this.components.push(this.timeAxis); - this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis); + // create a grid, a 2d matrix, with all values. + var dataMatrix = []; // temporary data matrix + for (i = 0; i < data.length; i++) { + x = data[i][this.colX] || 0; + y = data[i][this.colY] || 0; + z = data[i][this.colZ] || 0; - // current time bar - this.currentTime = new CurrentTime(this.body); - this.components.push(this.currentTime); + var xIndex = dataX.indexOf(x); // TODO: implement Array().indexOf() for Internet Explorer + var yIndex = dataY.indexOf(y); - // custom time bar - // Note: time bar will be attached in this.setOptions when selected - this.customTime = new CustomTime(this.body); - this.components.push(this.customTime); + if (dataMatrix[xIndex] === undefined) { + dataMatrix[xIndex] = []; + } - // item set - this.linegraph = new LineGraph(this.body); - this.components.push(this.linegraph); + var point3d = new Point3d(); + point3d.x = x; + point3d.y = y; + point3d.z = z; - this.itemsData = null; // DataSet - this.groupsData = null; // DataSet + obj = {}; + obj.point = point3d; + obj.trans = undefined; + obj.screen = undefined; + obj.bottom = new Point3d(x, y, this.zMin); - // apply options - if (options) { - this.setOptions(options); - } + dataMatrix[xIndex][yIndex] = obj; - // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS! - if (groups) { - this.setGroups(groups); - } + dataPoints.push(obj); + } - // create itemset - if (items) { - this.setItems(items); - } - else { - this.redraw(); + // fill in the pointers to the neighbors. + for (x = 0; x < dataMatrix.length; x++) { + for (y = 0; y < dataMatrix[x].length; y++) { + if (dataMatrix[x][y]) { + dataMatrix[x][y].pointRight = (x < dataMatrix.length-1) ? dataMatrix[x+1][y] : undefined; + dataMatrix[x][y].pointTop = (y < dataMatrix[x].length-1) ? dataMatrix[x][y+1] : undefined; + dataMatrix[x][y].pointCross = + (x < dataMatrix.length-1 && y < dataMatrix[x].length-1) ? + dataMatrix[x+1][y+1] : + undefined; + } + } + } } - } - - // turn Graph2d into an event emitter - Emitter(Graph2d.prototype); + else { // 'dot', 'dot-line', etc. + // copy all values from the google data table to a list with Point3d objects + for (i = 0; i < data.length; i++) { + point = new Point3d(); + point.x = data[i][this.colX] || 0; + point.y = data[i][this.colY] || 0; + point.z = data[i][this.colZ] || 0; - /** - * Create the main DOM for the Graph2d: a root panel containing left, right, - * top, bottom, content, and background panel. - * @param {Element} container The container element where the Graph2d will - * be attached. - * @private - */ - Graph2d.prototype._create = function (container) { - this.dom = {}; + if (this.colValue !== undefined) { + point.value = data[i][this.colValue] || 0; + } - this.dom.root = document.createElement('div'); - this.dom.background = document.createElement('div'); - this.dom.backgroundVertical = document.createElement('div'); - this.dom.backgroundHorizontalContainer = document.createElement('div'); - this.dom.centerContainer = document.createElement('div'); - this.dom.leftContainer = document.createElement('div'); - this.dom.rightContainer = document.createElement('div'); - this.dom.backgroundHorizontal = document.createElement('div'); - this.dom.center = document.createElement('div'); - this.dom.left = document.createElement('div'); - this.dom.right = document.createElement('div'); - this.dom.top = document.createElement('div'); - this.dom.bottom = document.createElement('div'); - this.dom.shadowTop = document.createElement('div'); - this.dom.shadowBottom = document.createElement('div'); - this.dom.shadowTopLeft = document.createElement('div'); - this.dom.shadowBottomLeft = document.createElement('div'); - this.dom.shadowTopRight = document.createElement('div'); - this.dom.shadowBottomRight = document.createElement('div'); + obj = {}; + obj.point = point; + obj.bottom = new Point3d(point.x, point.y, this.zMin); + obj.trans = undefined; + obj.screen = undefined; - this.dom.background.className = 'vispanel background'; - this.dom.backgroundVertical.className = 'vispanel background vertical'; - this.dom.backgroundHorizontalContainer.className = 'vispanel background horizontal'; - this.dom.backgroundHorizontal.className = 'vispanel background horizontal'; - this.dom.centerContainer.className = 'vispanel center'; - this.dom.leftContainer.className = 'vispanel left'; - this.dom.rightContainer.className = 'vispanel right'; - this.dom.top.className = 'vispanel top'; - this.dom.bottom.className = 'vispanel bottom'; - this.dom.left.className = 'content'; - this.dom.center.className = 'content'; - this.dom.right.className = 'content'; - this.dom.shadowTop.className = 'shadow top'; - this.dom.shadowBottom.className = 'shadow bottom'; - this.dom.shadowTopLeft.className = 'shadow top'; - this.dom.shadowBottomLeft.className = 'shadow bottom'; - this.dom.shadowTopRight.className = 'shadow top'; - this.dom.shadowBottomRight.className = 'shadow bottom'; + dataPoints.push(obj); + } + } - this.dom.root.appendChild(this.dom.background); - this.dom.root.appendChild(this.dom.backgroundVertical); - this.dom.root.appendChild(this.dom.backgroundHorizontalContainer); - this.dom.root.appendChild(this.dom.centerContainer); - this.dom.root.appendChild(this.dom.leftContainer); - this.dom.root.appendChild(this.dom.rightContainer); - this.dom.root.appendChild(this.dom.top); - this.dom.root.appendChild(this.dom.bottom); + return dataPoints; + }; - this.dom.backgroundHorizontalContainer.appendChild(this.dom.backgroundHorizontal); - this.dom.centerContainer.appendChild(this.dom.center); - this.dom.leftContainer.appendChild(this.dom.left); - this.dom.rightContainer.appendChild(this.dom.right); + /** + * Create the main frame for the Graph3d. + * This function is executed once when a Graph3d object is created. The frame + * contains a canvas, and this canvas contains all objects like the axis and + * nodes. + */ + Graph3d.prototype.create = function () { + // remove all elements from the container element. + while (this.containerElement.hasChildNodes()) { + this.containerElement.removeChild(this.containerElement.firstChild); + } - this.dom.centerContainer.appendChild(this.dom.shadowTop); - this.dom.centerContainer.appendChild(this.dom.shadowBottom); - this.dom.leftContainer.appendChild(this.dom.shadowTopLeft); - this.dom.leftContainer.appendChild(this.dom.shadowBottomLeft); - this.dom.rightContainer.appendChild(this.dom.shadowTopRight); - this.dom.rightContainer.appendChild(this.dom.shadowBottomRight); + this.frame = document.createElement('div'); + this.frame.style.position = 'relative'; + this.frame.style.overflow = 'hidden'; - this.on('rangechange', this.redraw.bind(this)); - this.on('change', this.redraw.bind(this)); - this.on('touch', this._onTouch.bind(this)); - this.on('pinch', this._onPinch.bind(this)); - this.on('dragstart', this._onDragStart.bind(this)); - this.on('drag', this._onDrag.bind(this)); + // create the graph canvas (HTML canvas element) + this.frame.canvas = document.createElement( 'canvas' ); + this.frame.canvas.style.position = 'relative'; + this.frame.appendChild(this.frame.canvas); + //if (!this.frame.canvas.getContext) { + { + var noCanvas = document.createElement( 'DIV' ); + noCanvas.style.color = 'red'; + noCanvas.style.fontWeight = 'bold' ; + noCanvas.style.padding = '10px'; + noCanvas.innerHTML = 'Error: your browser does not support HTML canvas'; + this.frame.canvas.appendChild(noCanvas); + } - // create event listeners for all interesting events, these events will be - // emitted via emitter - this.hammer = Hammer(this.dom.root, { - prevent_default: true - }); - this.listeners = {}; + this.frame.filter = document.createElement( 'div' ); + this.frame.filter.style.position = 'absolute'; + this.frame.filter.style.bottom = '0px'; + this.frame.filter.style.left = '0px'; + this.frame.filter.style.width = '100%'; + this.frame.appendChild(this.frame.filter); + // add event listeners to handle moving and zooming the contents var me = this; - var events = [ - 'touch', 'pinch', - 'tap', 'doubletap', 'hold', - 'dragstart', 'drag', 'dragend', - 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed for Firefox - ]; - events.forEach(function (event) { - var listener = function () { - var args = [event].concat(Array.prototype.slice.call(arguments, 0)); - me.emit.apply(me, args); - }; - me.hammer.on(event, listener); - me.listeners[event] = listener; - }); + var onmousedown = function (event) {me._onMouseDown(event);}; + var ontouchstart = function (event) {me._onTouchStart(event);}; + var onmousewheel = function (event) {me._onWheel(event);}; + var ontooltip = function (event) {me._onTooltip(event);}; + // TODO: these events are never cleaned up... can give a 'memory leakage' - // size properties of each of the panels - this.props = { - root: {}, - background: {}, - centerContainer: {}, - leftContainer: {}, - rightContainer: {}, - center: {}, - left: {}, - right: {}, - top: {}, - bottom: {}, - border: {}, - scrollTop: 0, - scrollTopMin: 0 - }; - this.touch = {}; // store state information needed for touch events + G3DaddEventListener(this.frame.canvas, 'keydown', onkeydown); + G3DaddEventListener(this.frame.canvas, 'mousedown', onmousedown); + G3DaddEventListener(this.frame.canvas, 'touchstart', ontouchstart); + G3DaddEventListener(this.frame.canvas, 'mousewheel', onmousewheel); + G3DaddEventListener(this.frame.canvas, 'mousemove', ontooltip); - // attach the root panel to the provided container - if (!container) throw new Error('No container provided'); - container.appendChild(this.dom.root); + // add the new graph to the container element + this.containerElement.appendChild(this.frame); }; + /** - * Destroy the Graph2d, clean up all DOM elements and event listeners. + * Set a new size for the graph + * @param {string} width Width in pixels or percentage (for example '800px' + * or '50%') + * @param {string} height Height in pixels or percentage (for example '400px' + * or '30%') */ - Graph2d.prototype.destroy = function () { - // unbind datasets - this.clear(); - - // remove all event listeners - this.off(); - - // stop checking for changed size - this._stopAutoResize(); - - // remove from DOM - if (this.dom.root.parentNode) { - this.dom.root.parentNode.removeChild(this.dom.root); - } - this.dom = null; - - // cleanup hammer touch events - for (var event in this.listeners) { - if (this.listeners.hasOwnProperty(event)) { - delete this.listeners[event]; - } - } - this.listeners = null; - this.hammer = null; - - // give all components the opportunity to cleanup - this.components.forEach(function (component) { - component.destroy(); - }); + Graph3d.prototype.setSize = function(width, height) { + this.frame.style.width = width; + this.frame.style.height = height; - this.body = null; + this._resizeCanvas(); }; /** - * Set options. Options will be passed to all components loaded in the Graph2d. - * @param {Object} [options] - * {String} orientation - * Vertical orientation for the Graph2d, - * can be 'bottom' (default) or 'top'. - * {String | Number} width - * Width for the timeline, a number in pixels or - * a css string like '1000px' or '75%'. '100%' by default. - * {String | Number} height - * Fixed height for the Graph2d, a number in pixels or - * a css string like '400px' or '75%'. If undefined, - * The Graph2d will automatically size such that - * its contents fit. - * {String | Number} minHeight - * Minimum height for the Graph2d, a number in pixels or - * a css string like '400px' or '75%'. - * {String | Number} maxHeight - * Maximum height for the Graph2d, a number in pixels or - * a css string like '400px' or '75%'. - * {Number | Date | String} start - * Start date for the visible window - * {Number | Date | String} end - * End date for the visible window + * Resize the canvas to the current size of the frame */ - Graph2d.prototype.setOptions = function (options) { - if (options) { - // copy the known options - var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'orientation']; - util.selectiveExtend(fields, this.options, options); + Graph3d.prototype._resizeCanvas = function() { + this.frame.canvas.style.width = '100%'; + this.frame.canvas.style.height = '100%'; - // enable/disable autoResize - this._initAutoResize(); - } + this.frame.canvas.width = this.frame.canvas.clientWidth; + this.frame.canvas.height = this.frame.canvas.clientHeight; - // propagate options to all components - this.components.forEach(function (component) { - component.setOptions(options); - }); + // adjust with for margin + this.frame.filter.style.width = (this.frame.canvas.clientWidth - 2 * 10) + 'px'; + }; - // TODO: remove deprecation error one day (deprecated since version 0.8.0) - if (options && options.order) { - throw new Error('Option order is deprecated. There is no replacement for this feature.'); - } + /** + * Start animation + */ + Graph3d.prototype.animationStart = function() { + if (!this.frame.filter || !this.frame.filter.slider) + throw 'No animation available'; - // redraw everything - this.redraw(); + this.frame.filter.slider.play(); }; + /** - * Set a custom time bar - * @param {Date} time + * Stop animation */ - Graph2d.prototype.setCustomTime = function (time) { - if (!this.customTime) { - throw new Error('Cannot get custom time: Custom time bar is not enabled'); - } + Graph3d.prototype.animationStop = function() { + if (!this.frame.filter || !this.frame.filter.slider) return; - this.customTime.setCustomTime(time); + this.frame.filter.slider.stop(); }; + /** - * Retrieve the current custom time. - * @return {Date} customTime + * Resize the center position based on the current values in this.defaultXCenter + * and this.defaultYCenter (which are strings with a percentage or a value + * in pixels). The center positions are the variables this.xCenter + * and this.yCenter */ - Graph2d.prototype.getCustomTime = function() { - if (!this.customTime) { - throw new Error('Cannot get custom time: Custom time bar is not enabled'); + Graph3d.prototype._resizeCenter = function() { + // calculate the horizontal center position + if (this.defaultXCenter.charAt(this.defaultXCenter.length-1) === '%') { + this.xcenter = + parseFloat(this.defaultXCenter) / 100 * + this.frame.canvas.clientWidth; + } + else { + this.xcenter = parseFloat(this.defaultXCenter); // supposed to be in px } - return this.customTime.getCustomTime(); + // calculate the vertical center position + if (this.defaultYCenter.charAt(this.defaultYCenter.length-1) === '%') { + this.ycenter = + parseFloat(this.defaultYCenter) / 100 * + (this.frame.canvas.clientHeight - this.frame.filter.clientHeight); + } + else { + this.ycenter = parseFloat(this.defaultYCenter); // supposed to be in px + } }; /** - * Set items - * @param {vis.DataSet | Array | google.visualization.DataTable | null} items + * Set the rotation and distance of the camera + * @param {Object} pos An object with the camera position. The object + * contains three parameters: + * - horizontal {Number} + * The horizontal rotation, between 0 and 2*PI. + * Optional, can be left undefined. + * - vertical {Number} + * The vertical rotation, between 0 and 0.5*PI + * if vertical=0.5*PI, the graph is shown from the + * top. Optional, can be left undefined. + * - distance {Number} + * The (normalized) distance of the camera to the + * center of the graph, a value between 0.71 and 5.0. + * Optional, can be left undefined. */ - Graph2d.prototype.setItems = function(items) { - var initialLoad = (this.itemsData == null); - - // convert to type DataSet when needed - var newDataSet; - if (!items) { - newDataSet = null; - } - else if (items instanceof DataSet || items instanceof DataView) { - newDataSet = items; + Graph3d.prototype.setCameraPosition = function(pos) { + if (pos === undefined) { + return; } - else { - // turn an array into a dataset - newDataSet = new DataSet(items, { - type: { - start: 'Date', - end: 'Date' - } - }); + + if (pos.horizontal !== undefined && pos.vertical !== undefined) { + this.camera.setArmRotation(pos.horizontal, pos.vertical); } - // set items - this.itemsData = newDataSet; - this.linegraph && this.linegraph.setItems(newDataSet); + if (pos.distance !== undefined) { + this.camera.setArmLength(pos.distance); + } - if (initialLoad && ('start' in this.options || 'end' in this.options)) { - this.fit(); + this.redraw(); + }; - var start = ('start' in this.options) ? util.convert(this.options.start, 'Date') : null; - var end = ('end' in this.options) ? util.convert(this.options.end, 'Date') : null; - this.setWindow(start, end); - } + /** + * Retrieve the current camera rotation + * @return {object} An object with parameters horizontal, vertical, and + * distance + */ + Graph3d.prototype.getCameraPosition = function() { + var pos = this.camera.getArmRotation(); + pos.distance = this.camera.getArmLength(); + return pos; }; /** - * Set groups - * @param {vis.DataSet | Array | google.visualization.DataTable} groups + * Load data into the 3D Graph */ - Graph2d.prototype.setGroups = function(groups) { - // convert to type DataSet when needed - var newDataSet; - if (!groups) { - newDataSet = null; - } - else if (groups instanceof DataSet || groups instanceof DataView) { - newDataSet = groups; + Graph3d.prototype._readData = function(data) { + // read the data + this._dataInitialize(data, this.style); + + + if (this.dataFilter) { + // apply filtering + this.dataPoints = this.dataFilter._getDataPoints(); } else { - // turn an array into a dataset - newDataSet = new DataSet(groups); + // no filtering. load all data + this.dataPoints = this._getDataPoints(this.dataTable); } - this.groupsData = newDataSet; - this.linegraph.setGroups(newDataSet); + // draw the filter + this._redrawFilter(); }; /** - * Clear the Graph2d. By Default, items, groups and options are cleared. - * Example usage: - * - * timeline.clear(); // clear items, groups, and options - * timeline.clear({options: true}); // clear options only - * - * @param {Object} [what] Optionally specify what to clear. By default: - * {items: true, groups: true, options: true} + * Replace the dataset of the Graph3d + * @param {Array | DataSet | DataView} data */ - Graph2d.prototype.clear = function(what) { - // clear items - if (!what || what.items) { - this.setItems(null); - } - - // clear groups - if (!what || what.groups) { - this.setGroups(null); - } - - // clear options of timeline and of each of the components - if (!what || what.options) { - this.components.forEach(function (component) { - component.setOptions(component.defaultOptions); - }); + Graph3d.prototype.setData = function (data) { + this._readData(data); + this.redraw(); - this.setOptions(this.defaultOptions); // this will also do a redraw + // start animation when option is true + if (this.animationAutoStart && this.dataFilter) { + this.animationStart(); } }; /** - * Set Graph2d window such that it fits all items + * Update the options. Options will be merged with current options + * @param {Object} options */ - Graph2d.prototype.fit = function() { - // apply the data range as range - var dataRange = this.getItemRange(); + Graph3d.prototype.setOptions = function (options) { + var cameraPosition = undefined; - // add 5% space on both sides - var start = dataRange.min; - var end = dataRange.max; - if (start != null && end != null) { - var interval = (end.valueOf() - start.valueOf()); - if (interval <= 0) { - // prevent an empty interval - interval = 24 * 60 * 60 * 1000; // 1 day + this.animationStop(); + + if (options !== undefined) { + // retrieve parameter values + if (options.width !== undefined) this.width = options.width; + if (options.height !== undefined) this.height = options.height; + + if (options.xCenter !== undefined) this.defaultXCenter = options.xCenter; + if (options.yCenter !== undefined) this.defaultYCenter = options.yCenter; + + if (options.filterLabel !== undefined) this.filterLabel = options.filterLabel; + if (options.legendLabel !== undefined) this.legendLabel = options.legendLabel; + if (options.xLabel !== undefined) this.xLabel = options.xLabel; + if (options.yLabel !== undefined) this.yLabel = options.yLabel; + if (options.zLabel !== undefined) this.zLabel = options.zLabel; + + if (options.style !== undefined) { + var styleNumber = this._getStyleNumber(options.style); + if (styleNumber !== -1) { + this.style = styleNumber; + } } - start = new Date(start.valueOf() - interval * 0.05); - end = new Date(end.valueOf() + interval * 0.05); - } + if (options.showGrid !== undefined) this.showGrid = options.showGrid; + if (options.showPerspective !== undefined) this.showPerspective = options.showPerspective; + if (options.showShadow !== undefined) this.showShadow = options.showShadow; + if (options.tooltip !== undefined) this.showTooltip = options.tooltip; + if (options.showAnimationControls !== undefined) this.showAnimationControls = options.showAnimationControls; + if (options.keepAspectRatio !== undefined) this.keepAspectRatio = options.keepAspectRatio; + if (options.verticalRatio !== undefined) this.verticalRatio = options.verticalRatio; - // skip range set if there is no start and end date - if (start === null && end === null) { - return; - } + if (options.animationInterval !== undefined) this.animationInterval = options.animationInterval; + if (options.animationPreload !== undefined) this.animationPreload = options.animationPreload; + if (options.animationAutoStart !== undefined)this.animationAutoStart = options.animationAutoStart; - this.range.setRange(start, end); - }; + if (options.xBarWidth !== undefined) this.defaultXBarWidth = options.xBarWidth; + if (options.yBarWidth !== undefined) this.defaultYBarWidth = options.yBarWidth; - /** - * Get the data range of the item set. - * @returns {{min: Date, max: Date}} range A range with a start and end Date. - * When no minimum is found, min==null - * When no maximum is found, max==null - */ - Graph2d.prototype.getItemRange = function() { - // calculate min from start filed - var itemsData = this.itemsData, - min = null, - max = null; + if (options.xMin !== undefined) this.defaultXMin = options.xMin; + if (options.xStep !== undefined) this.defaultXStep = options.xStep; + if (options.xMax !== undefined) this.defaultXMax = options.xMax; + if (options.yMin !== undefined) this.defaultYMin = options.yMin; + if (options.yStep !== undefined) this.defaultYStep = options.yStep; + if (options.yMax !== undefined) this.defaultYMax = options.yMax; + if (options.zMin !== undefined) this.defaultZMin = options.zMin; + if (options.zStep !== undefined) this.defaultZStep = options.zStep; + if (options.zMax !== undefined) this.defaultZMax = options.zMax; + if (options.valueMin !== undefined) this.defaultValueMin = options.valueMin; + if (options.valueMax !== undefined) this.defaultValueMax = options.valueMax; - if (itemsData) { - // calculate the minimum value of the field 'start' - var minItem = itemsData.min('start'); - min = minItem ? util.convert(minItem.start, 'Date').valueOf() : null; - // Note: we convert first to Date and then to number because else - // a conversion from ISODate to Number will fail + if (options.cameraPosition !== undefined) cameraPosition = options.cameraPosition; - // calculate maximum value of fields 'start' and 'end' - var maxStartItem = itemsData.max('start'); - if (maxStartItem) { - max = util.convert(maxStartItem.start, 'Date').valueOf(); + if (cameraPosition !== undefined) { + this.camera.setArmRotation(cameraPosition.horizontal, cameraPosition.vertical); + this.camera.setArmLength(cameraPosition.distance); } - var maxEndItem = itemsData.max('end'); - if (maxEndItem) { - if (max == null) { - max = util.convert(maxEndItem.end, 'Date').valueOf(); - } - else { - max = Math.max(max, util.convert(maxEndItem.end, 'Date').valueOf()); - } + else { + this.camera.setArmRotation(1.0, 0.5); + this.camera.setArmLength(1.7); } } - return { - min: (min != null) ? new Date(min) : null, - max: (max != null) ? new Date(max) : null - }; + this._setBackgroundColor(options && options.backgroundColor); + + this.setSize(this.width, this.height); + + // re-load the data + if (this.dataTable) { + this.setData(this.dataTable); + } + + // start animation when option is true + if (this.animationAutoStart && this.dataFilter) { + this.animationStart(); + } }; /** - * Set the visible window. Both parameters are optional, you can change only - * start or only end. Syntax: - * - * TimeLine.setWindow(start, end) - * TimeLine.setWindow(range) - * - * Where start and end can be a Date, number, or string, and range is an - * object with properties start and end. - * - * @param {Date | Number | String | Object} [start] Start date of visible window - * @param {Date | Number | String} [end] End date of visible window + * Redraw the Graph. */ - Graph2d.prototype.setWindow = function(start, end) { - if (arguments.length == 1) { - var range = arguments[0]; - this.range.setRange(range.start, range.end); + Graph3d.prototype.redraw = function() { + if (this.dataPoints === undefined) { + throw 'Error: graph data not initialized'; + } + + this._resizeCanvas(); + this._resizeCenter(); + this._redrawSlider(); + this._redrawClear(); + this._redrawAxis(); + + if (this.style === Graph3d.STYLE.GRID || + this.style === Graph3d.STYLE.SURFACE) { + this._redrawDataGrid(); + } + else if (this.style === Graph3d.STYLE.LINE) { + this._redrawDataLine(); + } + else if (this.style === Graph3d.STYLE.BAR || + this.style === Graph3d.STYLE.BARCOLOR || + this.style === Graph3d.STYLE.BARSIZE) { + this._redrawDataBar(); } else { - this.range.setRange(start, end); + // style is DOT, DOTLINE, DOTCOLOR, DOTSIZE + this._redrawDataDot(); } + + this._redrawInfo(); + this._redrawLegend(); }; /** - * Get the visible window - * @return {{start: Date, end: Date}} Visible range + * Clear the canvas before redrawing */ - Graph2d.prototype.getWindow = function() { - var range = this.range.getRange(); - return { - start: new Date(range.start), - end: new Date(range.end) - }; + Graph3d.prototype._redrawClear = function() { + var canvas = this.frame.canvas; + var ctx = canvas.getContext('2d'); + + ctx.clearRect(0, 0, canvas.width, canvas.height); }; + /** - * Force a redraw of the Graph2d. Can be useful to manually redraw when - * option autoResize=false + * Redraw the legend showing the colors */ - Graph2d.prototype.redraw = function() { - var resized = false, - options = this.options, - props = this.props, - dom = this.dom; - - if (!dom) return; // when destroyed + Graph3d.prototype._redrawLegend = function() { + var y; - // update class names - dom.root.className = 'vis timeline root ' + options.orientation; + if (this.style === Graph3d.STYLE.DOTCOLOR || + this.style === Graph3d.STYLE.DOTSIZE) { - // update root width and height options - dom.root.style.maxHeight = util.option.asSize(options.maxHeight, ''); - dom.root.style.minHeight = util.option.asSize(options.minHeight, ''); - dom.root.style.width = util.option.asSize(options.width, ''); + var dotSize = this.frame.clientWidth * 0.02; - // calculate border widths - props.border.left = (dom.centerContainer.offsetWidth - dom.centerContainer.clientWidth) / 2; - props.border.right = props.border.left; - props.border.top = (dom.centerContainer.offsetHeight - dom.centerContainer.clientHeight) / 2; - props.border.bottom = props.border.top; - var borderRootHeight= dom.root.offsetHeight - dom.root.clientHeight; - var borderRootWidth = dom.root.offsetWidth - dom.root.clientWidth; + var widthMin, widthMax; + if (this.style === Graph3d.STYLE.DOTSIZE) { + widthMin = dotSize / 2; // px + widthMax = dotSize / 2 + dotSize * 2; // Todo: put this in one function + } + else { + widthMin = 20; // px + widthMax = 20; // px + } - // calculate the heights. If any of the side panels is empty, we set the height to - // minus the border width, such that the border will be invisible - props.center.height = dom.center.offsetHeight; - props.left.height = dom.left.offsetHeight; - props.right.height = dom.right.offsetHeight; - props.top.height = dom.top.clientHeight || -props.border.top; - props.bottom.height = dom.bottom.clientHeight || -props.border.bottom; + var height = Math.max(this.frame.clientHeight * 0.25, 100); + var top = this.margin; + var right = this.frame.clientWidth - this.margin; + var left = right - widthMax; + var bottom = top + height; + } - // TODO: compensate borders when any of the panels is empty. + var canvas = this.frame.canvas; + var ctx = canvas.getContext('2d'); + ctx.lineWidth = 1; + ctx.font = '14px arial'; // TODO: put in options - // apply auto height - // TODO: only calculate autoHeight when needed (else we cause an extra reflow/repaint of the DOM) - var contentHeight = Math.max(props.left.height, props.center.height, props.right.height); - var autoHeight = props.top.height + contentHeight + props.bottom.height + - borderRootHeight + props.border.top + props.border.bottom; - dom.root.style.height = util.option.asSize(options.height, autoHeight + 'px'); + if (this.style === Graph3d.STYLE.DOTCOLOR) { + // draw the color bar + var ymin = 0; + var ymax = height; // Todo: make height customizable + for (y = ymin; y < ymax; y++) { + var f = (y - ymin) / (ymax - ymin); - // calculate heights of the content panels - props.root.height = dom.root.offsetHeight; - props.background.height = props.root.height - borderRootHeight; - var containerHeight = props.root.height - props.top.height - props.bottom.height - - borderRootHeight; - props.centerContainer.height = containerHeight; - props.leftContainer.height = containerHeight; - props.rightContainer.height = props.leftContainer.height; + //var width = (dotSize / 2 + (1-f) * dotSize * 2); // Todo: put this in one function + var hue = f * 240; + var color = this._hsv2rgb(hue, 1, 1); - // calculate the widths of the panels - props.root.width = dom.root.offsetWidth; - props.background.width = props.root.width - borderRootWidth; - props.left.width = dom.leftContainer.clientWidth || -props.border.left; - props.leftContainer.width = props.left.width; - props.right.width = dom.rightContainer.clientWidth || -props.border.right; - props.rightContainer.width = props.right.width; - var centerWidth = props.root.width - props.left.width - props.right.width - borderRootWidth; - props.center.width = centerWidth; - props.centerContainer.width = centerWidth; - props.top.width = centerWidth; - props.bottom.width = centerWidth; + ctx.strokeStyle = color; + ctx.beginPath(); + ctx.moveTo(left, top + y); + ctx.lineTo(right, top + y); + ctx.stroke(); + } - // resize the panels - dom.background.style.height = props.background.height + 'px'; - dom.backgroundVertical.style.height = props.background.height + 'px'; - dom.backgroundHorizontalContainer.style.height = props.centerContainer.height + 'px'; - dom.centerContainer.style.height = props.centerContainer.height + 'px'; - dom.leftContainer.style.height = props.leftContainer.height + 'px'; - dom.rightContainer.style.height = props.rightContainer.height + 'px'; + ctx.strokeStyle = this.colorAxis; + ctx.strokeRect(left, top, widthMax, height); + } - dom.background.style.width = props.background.width + 'px'; - dom.backgroundVertical.style.width = props.centerContainer.width + 'px'; - dom.backgroundHorizontalContainer.style.width = props.background.width + 'px'; - dom.backgroundHorizontal.style.width = props.background.width + 'px'; - dom.centerContainer.style.width = props.center.width + 'px'; - dom.top.style.width = props.top.width + 'px'; - dom.bottom.style.width = props.bottom.width + 'px'; + if (this.style === Graph3d.STYLE.DOTSIZE) { + // draw border around color bar + ctx.strokeStyle = this.colorAxis; + ctx.fillStyle = this.colorDot; + ctx.beginPath(); + ctx.moveTo(left, top); + ctx.lineTo(right, top); + ctx.lineTo(right - widthMax + widthMin, bottom); + ctx.lineTo(left, bottom); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + } - // reposition the panels - dom.background.style.left = '0'; - dom.background.style.top = '0'; - dom.backgroundVertical.style.left = props.left.width + 'px'; - dom.backgroundVertical.style.top = '0'; - dom.backgroundHorizontalContainer.style.left = '0'; - dom.backgroundHorizontalContainer.style.top = props.top.height + 'px'; - dom.centerContainer.style.left = props.left.width + 'px'; - dom.centerContainer.style.top = props.top.height + 'px'; - dom.leftContainer.style.left = '0'; - dom.leftContainer.style.top = props.top.height + 'px'; - dom.rightContainer.style.left = (props.left.width + props.center.width) + 'px'; - dom.rightContainer.style.top = props.top.height + 'px'; - dom.top.style.left = props.left.width + 'px'; - dom.top.style.top = '0'; - dom.bottom.style.left = props.left.width + 'px'; - dom.bottom.style.top = (props.top.height + props.centerContainer.height) + 'px'; + if (this.style === Graph3d.STYLE.DOTCOLOR || + this.style === Graph3d.STYLE.DOTSIZE) { + // print values along the color bar + var gridLineLen = 5; // px + var step = new StepNumber(this.valueMin, this.valueMax, (this.valueMax-this.valueMin)/5, true); + step.start(); + if (step.getCurrent() < this.valueMin) { + step.next(); + } + while (!step.end()) { + y = bottom - (step.getCurrent() - this.valueMin) / (this.valueMax - this.valueMin) * height; - // update the scrollTop, feasible range for the offset can be changed - // when the height of the Graph2d or of the contents of the center changed - this._updateScrollTop(); + ctx.beginPath(); + ctx.moveTo(left - gridLineLen, y); + ctx.lineTo(left, y); + ctx.stroke(); - // reposition the scrollable contents - var offset = this.props.scrollTop; - if (options.orientation == 'bottom') { - offset += Math.max(this.props.centerContainer.height - this.props.center.height - - this.props.border.top - this.props.border.bottom, 0); - } - dom.center.style.left = '0'; - dom.center.style.top = offset + 'px'; - dom.backgroundHorizontal.style.left = '0'; - dom.backgroundHorizontal.style.top = offset + 'px'; - dom.left.style.left = '0'; - dom.left.style.top = offset + 'px'; - dom.right.style.left = '0'; - dom.right.style.top = offset + 'px'; + ctx.textAlign = 'right'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = this.colorAxis; + ctx.fillText(step.getCurrent(), left - 2 * gridLineLen, y); - // show shadows when vertical scrolling is available - var visibilityTop = this.props.scrollTop == 0 ? 'hidden' : ''; - var visibilityBottom = this.props.scrollTop == this.props.scrollTopMin ? 'hidden' : ''; - dom.shadowTop.style.visibility = visibilityTop; - dom.shadowBottom.style.visibility = visibilityBottom; - dom.shadowTopLeft.style.visibility = visibilityTop; - dom.shadowBottomLeft.style.visibility = visibilityBottom; - dom.shadowTopRight.style.visibility = visibilityTop; - dom.shadowBottomRight.style.visibility = visibilityBottom; + step.next(); + } - // redraw all components - this.components.forEach(function (component) { - resized = component.redraw() || resized; - }); - if (resized) { - // keep redrawing until all sizes are settled - this.redraw(); + ctx.textAlign = 'right'; + ctx.textBaseline = 'top'; + var label = this.legendLabel; + ctx.fillText(label, right, bottom + this.margin); } }; /** - * Convert a position on screen (pixels) to a datetime - * @param {int} x Position on the screen in pixels - * @return {Date} time The datetime the corresponds with given position x - * @private + * Redraw the filter */ - // TODO: move this function to Range - Graph2d.prototype._toTime = function(x) { - var conversion = this.range.conversion(this.props.center.width); - return new Date(x / conversion.scale + conversion.offset); - }; + Graph3d.prototype._redrawFilter = function() { + this.frame.filter.innerHTML = ''; - /** - * Convert a datetime (Date object) into a position on the root - * This is used to get the pixel density estimate for the screen, not the center panel - * @param {Date} time A date - * @return {int} x The position on root in pixels which corresponds - * with the given date. - * @private - */ - // TODO: move this function to Range - Graph2d.prototype._toGlobalTime = function(x) { - var conversion = this.range.conversion(this.props.root.width); - return new Date(x / conversion.scale + conversion.offset); - }; + if (this.dataFilter) { + var options = { + 'visible': this.showAnimationControls + }; + var slider = new Slider(this.frame.filter, options); + this.frame.filter.slider = slider; - /** - * Convert a datetime (Date object) into a position on the screen - * @param {Date} time A date - * @return {int} x The position on the screen in pixels which corresponds - * with the given date. - * @private - */ - // TODO: move this function to Range - Graph2d.prototype._toScreen = function(time) { - var conversion = this.range.conversion(this.props.center.width); - return (time.valueOf() - conversion.offset) * conversion.scale; - }; + // TODO: css here is not nice here... + this.frame.filter.style.padding = '10px'; + //this.frame.filter.style.backgroundColor = '#EFEFEF'; + slider.setValues(this.dataFilter.values); + slider.setPlayInterval(this.animationInterval); - /** - * Convert a datetime (Date object) into a position on the root - * This is used to get the pixel density estimate for the screen, not the center panel - * @param {Date} time A date - * @return {int} x The position on root in pixels which corresponds - * with the given date. - * @private - */ - // TODO: move this function to Range - Graph2d.prototype._toGlobalScreen = function(time) { - var conversion = this.range.conversion(this.props.root.width); - return (time.valueOf() - conversion.offset) * conversion.scale; - }; + // create an event handler + var me = this; + var onchange = function () { + var index = slider.getIndex(); - /** - * Initialize watching when option autoResize is true - * @private - */ - Graph2d.prototype._initAutoResize = function () { - if (this.options.autoResize == true) { - this._startAutoResize(); + me.dataFilter.selectValue(index); + me.dataPoints = me.dataFilter._getDataPoints(); + + me.redraw(); + }; + slider.setOnChangeCallback(onchange); } else { - this._stopAutoResize(); + this.frame.filter.slider = undefined; } }; /** - * Watch for changes in the size of the container. On resize, the Panel will - * automatically redraw itself. - * @private + * Redraw the slider */ - Graph2d.prototype._startAutoResize = function () { - var me = this; - - this._stopAutoResize(); - - this._onResize = function() { - if (me.options.autoResize != true) { - // stop watching when the option autoResize is changed to false - me._stopAutoResize(); - return; - } + Graph3d.prototype._redrawSlider = function() { + if ( this.frame.filter.slider !== undefined) { + this.frame.filter.slider.redraw(); + } + }; - if (me.dom.root) { - // check whether the frame is resized - if ((me.dom.root.clientWidth != me.props.lastWidth) || - (me.dom.root.clientHeight != me.props.lastHeight)) { - me.props.lastWidth = me.dom.root.clientWidth; - me.props.lastHeight = me.dom.root.clientHeight; - me.emit('change'); - } - } - }; + /** + * Redraw common information + */ + Graph3d.prototype._redrawInfo = function() { + if (this.dataFilter) { + var canvas = this.frame.canvas; + var ctx = canvas.getContext('2d'); - // add event listener to window resize - util.addEventListener(window, 'resize', this._onResize); + ctx.font = '14px arial'; // TODO: put in options + ctx.lineStyle = 'gray'; + ctx.fillStyle = 'gray'; + ctx.textAlign = 'left'; + ctx.textBaseline = 'top'; - this.watchTimer = setInterval(this._onResize, 1000); + var x = this.margin; + var y = this.margin; + ctx.fillText(this.dataFilter.getLabel() + ': ' + this.dataFilter.getSelectedValue(), x, y); + } }; + /** - * Stop watching for a resize of the frame. - * @private + * Redraw the axis */ - Graph2d.prototype._stopAutoResize = function () { - if (this.watchTimer) { - clearInterval(this.watchTimer); - this.watchTimer = undefined; - } + Graph3d.prototype._redrawAxis = function() { + var canvas = this.frame.canvas, + ctx = canvas.getContext('2d'), + from, to, step, prettyStep, + text, xText, yText, zText, + offset, xOffset, yOffset, + xMin2d, xMax2d; - // remove event listener on window.resize - util.removeEventListener(window, 'resize', this._onResize); - this._onResize = null; - }; + // TODO: get the actual rendered style of the containerElement + //ctx.font = this.containerElement.style.font; + ctx.font = 24 / this.camera.getArmLength() + 'px arial'; - /** - * Start moving the timeline vertically - * @param {Event} event - * @private - */ - Graph2d.prototype._onTouch = function (event) { - this.touch.allowDragging = true; - }; + // calculate the length for the short grid lines + var gridLenX = 0.025 / this.scale.x; + var gridLenY = 0.025 / this.scale.y; + var textMargin = 5 / this.camera.getArmLength(); // px + var armAngle = this.camera.getArmRotation().horizontal; - /** - * Start moving the timeline vertically - * @param {Event} event - * @private - */ - Graph2d.prototype._onPinch = function (event) { - this.touch.allowDragging = false; - }; + // draw x-grid lines + ctx.lineWidth = 1; + prettyStep = (this.defaultXStep === undefined); + step = new StepNumber(this.xMin, this.xMax, this.xStep, prettyStep); + step.start(); + if (step.getCurrent() < this.xMin) { + step.next(); + } + while (!step.end()) { + var x = step.getCurrent(); - /** - * Start moving the timeline vertically - * @param {Event} event - * @private - */ - Graph2d.prototype._onDragStart = function (event) { - this.touch.initialScrollTop = this.props.scrollTop; - }; + if (this.showGrid) { + from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin)); + to = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin)); + ctx.strokeStyle = this.colorGrid; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); + } + else { + from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin)); + to = this._convert3Dto2D(new Point3d(x, this.yMin+gridLenX, this.zMin)); + ctx.strokeStyle = this.colorAxis; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); - /** - * Move the timeline vertically - * @param {Event} event - * @private - */ - Graph2d.prototype._onDrag = function (event) { - // refuse to drag when we where pinching to prevent the timeline make a jump - // when releasing the fingers in opposite order from the touch screen - if (!this.touch.allowDragging) return; + from = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin)); + to = this._convert3Dto2D(new Point3d(x, this.yMax-gridLenX, this.zMin)); + ctx.strokeStyle = this.colorAxis; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); + } - var delta = event.gesture.deltaY; + yText = (Math.cos(armAngle) > 0) ? this.yMin : this.yMax; + text = this._convert3Dto2D(new Point3d(x, yText, this.zMin)); + if (Math.cos(armAngle * 2) > 0) { + ctx.textAlign = 'center'; + ctx.textBaseline = 'top'; + text.y += textMargin; + } + else if (Math.sin(armAngle * 2) < 0){ + ctx.textAlign = 'right'; + ctx.textBaseline = 'middle'; + } + else { + ctx.textAlign = 'left'; + ctx.textBaseline = 'middle'; + } + ctx.fillStyle = this.colorAxis; + ctx.fillText(' ' + step.getCurrent() + ' ', text.x, text.y); - var oldScrollTop = this._getScrollTop(); - var newScrollTop = this._setScrollTop(this.touch.initialScrollTop + delta); + step.next(); + } - if (newScrollTop != oldScrollTop) { - this.redraw(); // TODO: this causes two redraws when dragging, the other is triggered by rangechange already + // draw y-grid lines + ctx.lineWidth = 1; + prettyStep = (this.defaultYStep === undefined); + step = new StepNumber(this.yMin, this.yMax, this.yStep, prettyStep); + step.start(); + if (step.getCurrent() < this.yMin) { + step.next(); } - }; + while (!step.end()) { + if (this.showGrid) { + from = this._convert3Dto2D(new Point3d(this.xMin, step.getCurrent(), this.zMin)); + to = this._convert3Dto2D(new Point3d(this.xMax, step.getCurrent(), this.zMin)); + ctx.strokeStyle = this.colorGrid; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); + } + else { + from = this._convert3Dto2D(new Point3d(this.xMin, step.getCurrent(), this.zMin)); + to = this._convert3Dto2D(new Point3d(this.xMin+gridLenY, step.getCurrent(), this.zMin)); + ctx.strokeStyle = this.colorAxis; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); - /** - * Apply a scrollTop - * @param {Number} scrollTop - * @returns {Number} scrollTop Returns the applied scrollTop - * @private - */ - Graph2d.prototype._setScrollTop = function (scrollTop) { - this.props.scrollTop = scrollTop; - this._updateScrollTop(); - return this.props.scrollTop; - }; + from = this._convert3Dto2D(new Point3d(this.xMax, step.getCurrent(), this.zMin)); + to = this._convert3Dto2D(new Point3d(this.xMax-gridLenY, step.getCurrent(), this.zMin)); + ctx.strokeStyle = this.colorAxis; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); + } - /** - * Update the current scrollTop when the height of the containers has been changed - * @returns {Number} scrollTop Returns the applied scrollTop - * @private - */ - Graph2d.prototype._updateScrollTop = function () { - // recalculate the scrollTopMin - var scrollTopMin = Math.min(this.props.centerContainer.height - this.props.center.height, 0); // is negative or zero - if (scrollTopMin != this.props.scrollTopMin) { - // in case of bottom orientation, change the scrollTop such that the contents - // do not move relative to the time axis at the bottom - if (this.options.orientation == 'bottom') { - this.props.scrollTop += (scrollTopMin - this.props.scrollTopMin); + xText = (Math.sin(armAngle ) > 0) ? this.xMin : this.xMax; + text = this._convert3Dto2D(new Point3d(xText, step.getCurrent(), this.zMin)); + if (Math.cos(armAngle * 2) < 0) { + ctx.textAlign = 'center'; + ctx.textBaseline = 'top'; + text.y += textMargin; } - this.props.scrollTopMin = scrollTopMin; + else if (Math.sin(armAngle * 2) > 0){ + ctx.textAlign = 'right'; + ctx.textBaseline = 'middle'; + } + else { + ctx.textAlign = 'left'; + ctx.textBaseline = 'middle'; + } + ctx.fillStyle = this.colorAxis; + ctx.fillText(' ' + step.getCurrent() + ' ', text.x, text.y); + + step.next(); } - // limit the scrollTop to the feasible scroll range - if (this.props.scrollTop > 0) this.props.scrollTop = 0; - if (this.props.scrollTop < scrollTopMin) this.props.scrollTop = scrollTopMin; + // draw z-grid lines and axis + ctx.lineWidth = 1; + prettyStep = (this.defaultZStep === undefined); + step = new StepNumber(this.zMin, this.zMax, this.zStep, prettyStep); + step.start(); + if (step.getCurrent() < this.zMin) { + step.next(); + } + xText = (Math.cos(armAngle ) > 0) ? this.xMin : this.xMax; + yText = (Math.sin(armAngle ) < 0) ? this.yMin : this.yMax; + while (!step.end()) { + // TODO: make z-grid lines really 3d? + from = this._convert3Dto2D(new Point3d(xText, yText, step.getCurrent())); + ctx.strokeStyle = this.colorAxis; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(from.x - textMargin, from.y); + ctx.stroke(); - return this.props.scrollTop; - }; + ctx.textAlign = 'right'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = this.colorAxis; + ctx.fillText(step.getCurrent() + ' ', from.x - 5, from.y); - /** - * Get the current scrollTop - * @returns {number} scrollTop - * @private - */ - Graph2d.prototype._getScrollTop = function () { - return this.props.scrollTop; - }; + step.next(); + } + ctx.lineWidth = 1; + from = this._convert3Dto2D(new Point3d(xText, yText, this.zMin)); + to = this._convert3Dto2D(new Point3d(xText, yText, this.zMax)); + ctx.strokeStyle = this.colorAxis; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); - module.exports = Graph2d; - - -/***/ }, -/* 8 */ -/***/ function(module, exports, __webpack_require__) { - - /** - * @constructor DataStep - * The class DataStep is an iterator for data for the lineGraph. You provide a start data point and an - * end data point. The class itself determines the best scale (step size) based on the - * provided start Date, end Date, and minimumStep. - * - * If minimumStep is provided, the step size is chosen as close as possible - * to the minimumStep but larger than minimumStep. If minimumStep is not - * provided, the scale is set to 1 DAY. - * The minimumStep should correspond with the onscreen size of about 6 characters - * - * Alternatively, you can set a scale by hand. - * After creation, you can initialize the class by executing first(). Then you - * can iterate from the start date to the end date via next(). You can check if - * the end date is reached with the function hasNext(). After each step, you can - * retrieve the current date via getCurrent(). - * The DataStep has scales ranging from milliseconds, seconds, minutes, hours, - * days, to years. - * - * Version: 1.2 - * - * @param {Date} [start] The start date, for example new Date(2010, 9, 21) - * or new Date(2010, 9, 21, 23, 45, 00) - * @param {Date} [end] The end date - * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds - */ - function DataStep(start, end, minimumStep, containerHeight, forcedStepSize) { - // variables - this.current = 0; - - this.autoScale = true; - this.stepIndex = 0; - this.step = 1; - this.scale = 1; - - this.marginStart; - this.marginEnd; - - this.majorSteps = [1, 2, 5, 10]; - this.minorSteps = [0.25, 0.5, 1, 2]; - - this.setRange(start, end, minimumStep, containerHeight, forcedStepSize); - } + // draw x-axis + ctx.lineWidth = 1; + // line at yMin + xMin2d = this._convert3Dto2D(new Point3d(this.xMin, this.yMin, this.zMin)); + xMax2d = this._convert3Dto2D(new Point3d(this.xMax, this.yMin, this.zMin)); + ctx.strokeStyle = this.colorAxis; + ctx.beginPath(); + ctx.moveTo(xMin2d.x, xMin2d.y); + ctx.lineTo(xMax2d.x, xMax2d.y); + ctx.stroke(); + // line at ymax + xMin2d = this._convert3Dto2D(new Point3d(this.xMin, this.yMax, this.zMin)); + xMax2d = this._convert3Dto2D(new Point3d(this.xMax, this.yMax, this.zMin)); + ctx.strokeStyle = this.colorAxis; + ctx.beginPath(); + ctx.moveTo(xMin2d.x, xMin2d.y); + ctx.lineTo(xMax2d.x, xMax2d.y); + ctx.stroke(); + // draw y-axis + ctx.lineWidth = 1; + // line at xMin + from = this._convert3Dto2D(new Point3d(this.xMin, this.yMin, this.zMin)); + to = this._convert3Dto2D(new Point3d(this.xMin, this.yMax, this.zMin)); + ctx.strokeStyle = this.colorAxis; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); + // line at xMax + from = this._convert3Dto2D(new Point3d(this.xMax, this.yMin, this.zMin)); + to = this._convert3Dto2D(new Point3d(this.xMax, this.yMax, this.zMin)); + ctx.strokeStyle = this.colorAxis; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); + // draw x-label + var xLabel = this.xLabel; + if (xLabel.length > 0) { + yOffset = 0.1 / this.scale.y; + xText = (this.xMin + this.xMax) / 2; + yText = (Math.cos(armAngle) > 0) ? this.yMin - yOffset: this.yMax + yOffset; + text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin)); + if (Math.cos(armAngle * 2) > 0) { + ctx.textAlign = 'center'; + ctx.textBaseline = 'top'; + } + else if (Math.sin(armAngle * 2) < 0){ + ctx.textAlign = 'right'; + ctx.textBaseline = 'middle'; + } + else { + ctx.textAlign = 'left'; + ctx.textBaseline = 'middle'; + } + ctx.fillStyle = this.colorAxis; + ctx.fillText(xLabel, text.x, text.y); + } - /** - * Set a new range - * If minimumStep is provided, the step size is chosen as close as possible - * to the minimumStep but larger than minimumStep. If minimumStep is not - * provided, the scale is set to 1 DAY. - * The minimumStep should correspond with the onscreen size of about 6 characters - * @param {Number} [start] The start date and time. - * @param {Number} [end] The end date and time. - * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds - */ - DataStep.prototype.setRange = function(start, end, minimumStep, containerHeight, forcedStepSize) { - this._start = start; - this._end = end; + // draw y-label + var yLabel = this.yLabel; + if (yLabel.length > 0) { + xOffset = 0.1 / this.scale.x; + xText = (Math.sin(armAngle ) > 0) ? this.xMin - xOffset : this.xMax + xOffset; + yText = (this.yMin + this.yMax) / 2; + text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin)); + if (Math.cos(armAngle * 2) < 0) { + ctx.textAlign = 'center'; + ctx.textBaseline = 'top'; + } + else if (Math.sin(armAngle * 2) > 0){ + ctx.textAlign = 'right'; + ctx.textBaseline = 'middle'; + } + else { + ctx.textAlign = 'left'; + ctx.textBaseline = 'middle'; + } + ctx.fillStyle = this.colorAxis; + ctx.fillText(yLabel, text.x, text.y); + } - if (this.autoScale) { - this.setMinimumStep(minimumStep, containerHeight, forcedStepSize); + // draw z-label + var zLabel = this.zLabel; + if (zLabel.length > 0) { + offset = 30; // pixels. // TODO: relate to the max width of the values on the z axis? + xText = (Math.cos(armAngle ) > 0) ? this.xMin : this.xMax; + yText = (Math.sin(armAngle ) < 0) ? this.yMin : this.yMax; + zText = (this.zMin + this.zMax) / 2; + text = this._convert3Dto2D(new Point3d(xText, yText, zText)); + ctx.textAlign = 'right'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = this.colorAxis; + ctx.fillText(zLabel, text.x - offset, text.y); } - this.setFirst(); }; /** - * Automatically determine the scale that bests fits the provided minimum step - * @param {Number} [minimumStep] The minimum step size in milliseconds + * Calculate the color based on the given value. + * @param {Number} H Hue, a value be between 0 and 360 + * @param {Number} S Saturation, a value between 0 and 1 + * @param {Number} V Value, a value between 0 and 1 */ - DataStep.prototype.setMinimumStep = function(minimumStep, containerHeight) { - // round to floor - var size = this._end - this._start; - var safeSize = size * 1.1; - var minimumStepValue = minimumStep * (safeSize / containerHeight); - var orderOfMagnitude = Math.round(Math.log(safeSize)/Math.LN10); + Graph3d.prototype._hsv2rgb = function(H, S, V) { + var R, G, B, C, Hi, X; - var minorStepIdx = -1; - var magnitudefactor = Math.pow(10,orderOfMagnitude); + C = V * S; + Hi = Math.floor(H/60); // hi = 0,1,2,3,4,5 + X = C * (1 - Math.abs(((H/60) % 2) - 1)); - var start = 0; - if (orderOfMagnitude < 0) { - start = orderOfMagnitude; - } + switch (Hi) { + case 0: R = C; G = X; B = 0; break; + case 1: R = X; G = C; B = 0; break; + case 2: R = 0; G = C; B = X; break; + case 3: R = 0; G = X; B = C; break; + case 4: R = X; G = 0; B = C; break; + case 5: R = C; G = 0; B = X; break; - var solutionFound = false; - for (var i = start; Math.abs(i) <= Math.abs(orderOfMagnitude); i++) { - magnitudefactor = Math.pow(10,i); - for (var j = 0; j < this.minorSteps.length; j++) { - var stepSize = magnitudefactor * this.minorSteps[j]; - if (stepSize >= minimumStepValue) { - solutionFound = true; - minorStepIdx = j; - break; - } - } - if (solutionFound == true) { - break; - } + default: R = 0; G = 0; B = 0; break; } - this.stepIndex = minorStepIdx; - this.scale = magnitudefactor; - this.step = magnitudefactor * this.minorSteps[minorStepIdx]; + + return 'RGB(' + parseInt(R*255) + ',' + parseInt(G*255) + ',' + parseInt(B*255) + ')'; }; /** - * Set the range iterator to the start date. + * Draw all datapoints as a grid + * This function can be used when the style is 'grid' */ - DataStep.prototype.first = function() { - this.setFirst(); - }; + Graph3d.prototype._redrawDataGrid = function() { + var canvas = this.frame.canvas, + ctx = canvas.getContext('2d'), + point, right, top, cross, + i, + topSideVisible, fillStyle, strokeStyle, lineWidth, + h, s, v, zAvg; - /** - * Round the current date to the first minor date value - * This must be executed once when the current date is set to start Date - */ - DataStep.prototype.setFirst = function() { - var niceStart = this._start - (this.scale * this.minorSteps[this.stepIndex]); - var niceEnd = this._end + (this.scale * this.minorSteps[this.stepIndex]); - this.marginEnd = this.roundToMinor(niceEnd); - this.marginStart = this.roundToMinor(niceStart); - this.marginRange = this.marginEnd - this.marginStart; + if (this.dataPoints === undefined || this.dataPoints.length <= 0) + return; // TODO: throw exception? - this.current = this.marginEnd; + // calculate the translations and screen position of all points + for (i = 0; i < this.dataPoints.length; i++) { + var trans = this._convertPointToTranslation(this.dataPoints[i].point); + var screen = this._convertTranslationToScreen(trans); - }; + this.dataPoints[i].trans = trans; + this.dataPoints[i].screen = screen; - DataStep.prototype.roundToMinor = function(value) { - var rounded = value - (value % (this.scale * this.minorSteps[this.stepIndex])); - if (value % (this.scale * this.minorSteps[this.stepIndex]) > 0.5 * (this.scale * this.minorSteps[this.stepIndex])) { - return rounded + (this.scale * this.minorSteps[this.stepIndex]); - } - else { - return rounded; + // calculate the translation of the point at the bottom (needed for sorting) + var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom); + this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z; } - } + // sort the points on depth of their (x,y) position (not on z) + var sortDepth = function (a, b) { + return b.dist - a.dist; + }; + this.dataPoints.sort(sortDepth); - /** - * Check if the there is a next step - * @return {boolean} true if the current date has not passed the end date - */ - DataStep.prototype.hasNext = function () { - return (this.current >= this.marginStart); - }; + if (this.style === Graph3d.STYLE.SURFACE) { + for (i = 0; i < this.dataPoints.length; i++) { + point = this.dataPoints[i]; + right = this.dataPoints[i].pointRight; + top = this.dataPoints[i].pointTop; + cross = this.dataPoints[i].pointCross; - /** - * Do the next step - */ - DataStep.prototype.next = function() { - var prev = this.current; - this.current -= this.step; + if (point !== undefined && right !== undefined && top !== undefined && cross !== undefined) { - // safety mechanism: if current time is still unchanged, move to the end - if (this.current == prev) { - this.current = this._end; - } - }; + if (this.showGrayBottom || this.showShadow) { + // calculate the cross product of the two vectors from center + // to left and right, in order to know whether we are looking at the + // bottom or at the top side. We can also use the cross product + // for calculating light intensity + var aDiff = Point3d.subtract(cross.trans, point.trans); + var bDiff = Point3d.subtract(top.trans, right.trans); + var crossproduct = Point3d.crossProduct(aDiff, bDiff); + var len = crossproduct.length(); + // FIXME: there is a bug with determining the surface side (shadow or colored) - /** - * Do the next step - */ - DataStep.prototype.previous = function() { - this.current += this.step; - this.marginEnd += this.step; - this.marginRange = this.marginEnd - this.marginStart; - }; + topSideVisible = (crossproduct.z > 0); + } + else { + topSideVisible = true; + } + if (topSideVisible) { + // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 + zAvg = (point.point.z + right.point.z + top.point.z + cross.point.z) / 4; + h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; + s = 1; // saturation + if (this.showShadow) { + v = Math.min(1 + (crossproduct.x / len) / 2, 1); // value. TODO: scale + fillStyle = this._hsv2rgb(h, s, v); + strokeStyle = fillStyle; + } + else { + v = 1; + fillStyle = this._hsv2rgb(h, s, v); + strokeStyle = this.colorAxis; + } + } + else { + fillStyle = 'gray'; + strokeStyle = this.colorAxis; + } + lineWidth = 0.5; - /** - * Get the current datetime - * @return {String} current The current date - */ - DataStep.prototype.getCurrent = function() { - var toPrecision = '' + Number(this.current).toPrecision(5); - for (var i = toPrecision.length-1; i > 0; i--) { - if (toPrecision[i] == "0") { - toPrecision = toPrecision.slice(0,i); - } - else if (toPrecision[i] == "." || toPrecision[i] == ",") { - toPrecision = toPrecision.slice(0,i); - break; - } - else{ - break; + ctx.lineWidth = lineWidth; + ctx.fillStyle = fillStyle; + ctx.strokeStyle = strokeStyle; + ctx.beginPath(); + ctx.moveTo(point.screen.x, point.screen.y); + ctx.lineTo(right.screen.x, right.screen.y); + ctx.lineTo(cross.screen.x, cross.screen.y); + ctx.lineTo(top.screen.x, top.screen.y); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + } } } + else { // grid style + for (i = 0; i < this.dataPoints.length; i++) { + point = this.dataPoints[i]; + right = this.dataPoints[i].pointRight; + top = this.dataPoints[i].pointTop; - return toPrecision; - }; + if (point !== undefined) { + if (this.showPerspective) { + lineWidth = 2 / -point.trans.z; + } + else { + lineWidth = 2 * -(this.eye.z / this.camera.getArmLength()); + } + } + if (point !== undefined && right !== undefined) { + // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 + zAvg = (point.point.z + right.point.z) / 2; + h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; + ctx.lineWidth = lineWidth; + ctx.strokeStyle = this._hsv2rgb(h, 1, 1); + ctx.beginPath(); + ctx.moveTo(point.screen.x, point.screen.y); + ctx.lineTo(right.screen.x, right.screen.y); + ctx.stroke(); + } - /** - * Snap a date to a rounded value. - * The snap intervals are dependent on the current scale and step. - * @param {Date} date the date to be snapped. - * @return {Date} snappedDate - */ - DataStep.prototype.snap = function(date) { + if (point !== undefined && top !== undefined) { + // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 + zAvg = (point.point.z + top.point.z) / 2; + h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; + ctx.lineWidth = lineWidth; + ctx.strokeStyle = this._hsv2rgb(h, 1, 1); + ctx.beginPath(); + ctx.moveTo(point.screen.x, point.screen.y); + ctx.lineTo(top.screen.x, top.screen.y); + ctx.stroke(); + } + } + } }; + /** - * Check if the current value is a major value (for example when the step - * is DAY, a major value is each first day of the MONTH) - * @return {boolean} true if current date is major, else false. + * Draw all datapoints as dots. + * This function can be used when the style is 'dot' or 'dot-line' */ - DataStep.prototype.isMajor = function() { - return (this.current % (this.scale * this.majorSteps[this.stepIndex]) == 0); - }; - - module.exports = DataStep; - - -/***/ }, -/* 9 */ -/***/ function(module, exports, __webpack_require__) { + Graph3d.prototype._redrawDataDot = function() { + var canvas = this.frame.canvas; + var ctx = canvas.getContext('2d'); + var i; - var util = __webpack_require__(1); - var moment = __webpack_require__(39); - var Component = __webpack_require__(12); + if (this.dataPoints === undefined || this.dataPoints.length <= 0) + return; // TODO: throw exception? - /** - * @constructor Range - * A Range controls a numeric range with a start and end value. - * The Range adjusts the range based on mouse events or programmatic changes, - * and triggers events when the range is changing or has been changed. - * @param {{dom: Object, domProps: Object, emitter: Emitter}} body - * @param {Object} [options] See description at Range.setOptions - */ - function Range(body, options) { - var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0); - this.start = now.clone().add('days', -3).valueOf(); // Number - this.end = now.clone().add('days', 4).valueOf(); // Number + // calculate the translations of all points + for (i = 0; i < this.dataPoints.length; i++) { + var trans = this._convertPointToTranslation(this.dataPoints[i].point); + var screen = this._convertTranslationToScreen(trans); + this.dataPoints[i].trans = trans; + this.dataPoints[i].screen = screen; - this.body = body; + // calculate the distance from the point at the bottom to the camera + var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom); + this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z; + } - // default options - this.defaultOptions = { - start: null, - end: null, - direction: 'horizontal', // 'horizontal' or 'vertical' - moveable: true, - zoomable: true, - min: null, - max: null, - zoomMin: 10, // milliseconds - zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000 // milliseconds + // order the translated points by depth + var sortDepth = function (a, b) { + return b.dist - a.dist; }; - this.options = util.extend({}, this.defaultOptions); + this.dataPoints.sort(sortDepth); - this.props = { - touch: {} - }; + // draw the datapoints as colored circles + var dotSize = this.frame.clientWidth * 0.02; // px + for (i = 0; i < this.dataPoints.length; i++) { + var point = this.dataPoints[i]; - // drag listeners for dragging - this.body.emitter.on('dragstart', this._onDragStart.bind(this)); - this.body.emitter.on('drag', this._onDrag.bind(this)); - this.body.emitter.on('dragend', this._onDragEnd.bind(this)); + if (this.style === Graph3d.STYLE.DOTLINE) { + // draw a vertical line from the bottom to the graph value + //var from = this._convert3Dto2D(new Point3d(point.point.x, point.point.y, this.zMin)); + var from = this._convert3Dto2D(point.bottom); + ctx.lineWidth = 1; + ctx.strokeStyle = this.colorGrid; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(point.screen.x, point.screen.y); + ctx.stroke(); + } - // ignore dragging when holding - this.body.emitter.on('hold', this._onHold.bind(this)); + // calculate radius for the circle + var size; + if (this.style === Graph3d.STYLE.DOTSIZE) { + size = dotSize/2 + 2*dotSize * (point.point.value - this.valueMin) / (this.valueMax - this.valueMin); + } + else { + size = dotSize; + } - // mouse wheel for zooming - this.body.emitter.on('mousewheel', this._onMouseWheel.bind(this)); - this.body.emitter.on('DOMMouseScroll', this._onMouseWheel.bind(this)); // For FF + var radius; + if (this.showPerspective) { + radius = size / -point.trans.z; + } + else { + radius = size * -(this.eye.z / this.camera.getArmLength()); + } + if (radius < 0) { + radius = 0; + } - // pinch to zoom - this.body.emitter.on('touch', this._onTouch.bind(this)); - this.body.emitter.on('pinch', this._onPinch.bind(this)); + var hue, color, borderColor; + if (this.style === Graph3d.STYLE.DOTCOLOR ) { + // calculate the color based on the value + hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240; + color = this._hsv2rgb(hue, 1, 1); + borderColor = this._hsv2rgb(hue, 1, 0.8); + } + else if (this.style === Graph3d.STYLE.DOTSIZE) { + color = this.colorDot; + borderColor = this.colorDotBorder; + } + else { + // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 + hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240; + color = this._hsv2rgb(hue, 1, 1); + borderColor = this._hsv2rgb(hue, 1, 0.8); + } - this.setOptions(options); - } - - Range.prototype = new Component(); - - /** - * Set options for the range controller - * @param {Object} options Available options: - * {Number | Date | String} start Start date for the range - * {Number | Date | String} end End date for the range - * {Number} min Minimum value for start - * {Number} max Maximum value for end - * {Number} zoomMin Set a minimum value for - * (end - start). - * {Number} zoomMax Set a maximum value for - * (end - start). - * {Boolean} moveable Enable moving of the range - * by dragging. True by default - * {Boolean} zoomable Enable zooming of the range - * by pinching/scrolling. True by default - */ - Range.prototype.setOptions = function (options) { - if (options) { - // copy the options that we know - var fields = ['direction', 'min', 'max', 'zoomMin', 'zoomMax', 'moveable', 'zoomable']; - util.selectiveExtend(fields, this.options, options); - - if ('start' in options || 'end' in options) { - // apply a new range. both start and end are optional - this.setRange(options.start, options.end); - } + // draw the circle + ctx.lineWidth = 1.0; + ctx.strokeStyle = borderColor; + ctx.fillStyle = color; + ctx.beginPath(); + ctx.arc(point.screen.x, point.screen.y, radius, 0, Math.PI*2, true); + ctx.fill(); + ctx.stroke(); } }; /** - * Test whether direction has a valid value - * @param {String} direction 'horizontal' or 'vertical' - */ - function validateDirection (direction) { - if (direction != 'horizontal' && direction != 'vertical') { - throw new TypeError('Unknown direction "' + direction + '". ' + - 'Choose "horizontal" or "vertical".'); - } - } - - /** - * Set a new start and end range - * @param {Number} [start] - * @param {Number} [end] + * Draw all datapoints as bars. + * This function can be used when the style is 'bar', 'bar-color', or 'bar-size' */ - Range.prototype.setRange = function(start, end) { - var changed = this._applyRange(start, end); - if (changed) { - var params = { - start: new Date(this.start), - end: new Date(this.end) - }; - this.body.emitter.emit('rangechange', params); - this.body.emitter.emit('rangechanged', params); - } - }; + Graph3d.prototype._redrawDataBar = function() { + var canvas = this.frame.canvas; + var ctx = canvas.getContext('2d'); + var i, j, surface, corners; - /** - * Set a new start and end range. This method is the same as setRange, but - * does not trigger a range change and range changed event, and it returns - * true when the range is changed - * @param {Number} [start] - * @param {Number} [end] - * @return {Boolean} changed - * @private - */ - Range.prototype._applyRange = function(start, end) { - var newStart = (start != null) ? util.convert(start, 'Date').valueOf() : this.start, - newEnd = (end != null) ? util.convert(end, 'Date').valueOf() : this.end, - max = (this.options.max != null) ? util.convert(this.options.max, 'Date').valueOf() : null, - min = (this.options.min != null) ? util.convert(this.options.min, 'Date').valueOf() : null, - diff; + if (this.dataPoints === undefined || this.dataPoints.length <= 0) + return; // TODO: throw exception? - // check for valid number - if (isNaN(newStart) || newStart === null) { - throw new Error('Invalid start "' + start + '"'); - } - if (isNaN(newEnd) || newEnd === null) { - throw new Error('Invalid end "' + end + '"'); - } + // calculate the translations of all points + for (i = 0; i < this.dataPoints.length; i++) { + var trans = this._convertPointToTranslation(this.dataPoints[i].point); + var screen = this._convertTranslationToScreen(trans); + this.dataPoints[i].trans = trans; + this.dataPoints[i].screen = screen; - // prevent start < end - if (newEnd < newStart) { - newEnd = newStart; + // calculate the distance from the point at the bottom to the camera + var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom); + this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z; } - // prevent start < min - if (min !== null) { - if (newStart < min) { - diff = (min - newStart); - newStart += diff; - newEnd += diff; - - // prevent end > max - if (max != null) { - if (newEnd > max) { - newEnd = max; - } - } - } - } + // order the translated points by depth + var sortDepth = function (a, b) { + return b.dist - a.dist; + }; + this.dataPoints.sort(sortDepth); - // prevent end > max - if (max !== null) { - if (newEnd > max) { - diff = (newEnd - max); - newStart -= diff; - newEnd -= diff; + // draw the datapoints as bars + var xWidth = this.xBarWidth / 2; + var yWidth = this.yBarWidth / 2; + for (i = 0; i < this.dataPoints.length; i++) { + var point = this.dataPoints[i]; - // prevent start < min - if (min != null) { - if (newStart < min) { - newStart = min; - } - } + // determine color + var hue, color, borderColor; + if (this.style === Graph3d.STYLE.BARCOLOR ) { + // calculate the color based on the value + hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240; + color = this._hsv2rgb(hue, 1, 1); + borderColor = this._hsv2rgb(hue, 1, 0.8); } - } - - // prevent (end-start) < zoomMin - if (this.options.zoomMin !== null) { - var zoomMin = parseFloat(this.options.zoomMin); - if (zoomMin < 0) { - zoomMin = 0; + else if (this.style === Graph3d.STYLE.BARSIZE) { + color = this.colorDot; + borderColor = this.colorDotBorder; } - if ((newEnd - newStart) < zoomMin) { - if ((this.end - this.start) === zoomMin) { - // ignore this action, we are already zoomed to the minimum - newStart = this.start; - newEnd = this.end; - } - else { - // zoom to the minimum - diff = (zoomMin - (newEnd - newStart)); - newStart -= diff / 2; - newEnd += diff / 2; - } + else { + // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 + hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240; + color = this._hsv2rgb(hue, 1, 1); + borderColor = this._hsv2rgb(hue, 1, 0.8); } - } - // prevent (end-start) > zoomMax - if (this.options.zoomMax !== null) { - var zoomMax = parseFloat(this.options.zoomMax); - if (zoomMax < 0) { - zoomMax = 0; - } - if ((newEnd - newStart) > zoomMax) { - if ((this.end - this.start) === zoomMax) { - // ignore this action, we are already zoomed to the maximum - newStart = this.start; - newEnd = this.end; - } - else { - // zoom to the maximum - diff = ((newEnd - newStart) - zoomMax); - newStart += diff / 2; - newEnd -= diff / 2; - } + // calculate size for the bar + if (this.style === Graph3d.STYLE.BARSIZE) { + xWidth = (this.xBarWidth / 2) * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2); + yWidth = (this.yBarWidth / 2) * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2); } - } - var changed = (this.start != newStart || this.end != newEnd); + // calculate all corner points + var me = this; + var point3d = point.point; + var top = [ + {point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, point3d.z)}, + {point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, point3d.z)}, + {point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, point3d.z)}, + {point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, point3d.z)} + ]; + var bottom = [ + {point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, this.zMin)}, + {point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, this.zMin)}, + {point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, this.zMin)}, + {point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, this.zMin)} + ]; - this.start = newStart; - this.end = newEnd; + // calculate screen location of the points + top.forEach(function (obj) { + obj.screen = me._convert3Dto2D(obj.point); + }); + bottom.forEach(function (obj) { + obj.screen = me._convert3Dto2D(obj.point); + }); - return changed; - }; + // create five sides, calculate both corner points and center points + var surfaces = [ + {corners: top, center: Point3d.avg(bottom[0].point, bottom[2].point)}, + {corners: [top[0], top[1], bottom[1], bottom[0]], center: Point3d.avg(bottom[1].point, bottom[0].point)}, + {corners: [top[1], top[2], bottom[2], bottom[1]], center: Point3d.avg(bottom[2].point, bottom[1].point)}, + {corners: [top[2], top[3], bottom[3], bottom[2]], center: Point3d.avg(bottom[3].point, bottom[2].point)}, + {corners: [top[3], top[0], bottom[0], bottom[3]], center: Point3d.avg(bottom[0].point, bottom[3].point)} + ]; + point.surfaces = surfaces; - /** - * Retrieve the current range. - * @return {Object} An object with start and end properties - */ - Range.prototype.getRange = function() { - return { - start: this.start, - end: this.end - }; - }; + // calculate the distance of each of the surface centers to the camera + for (j = 0; j < surfaces.length; j++) { + surface = surfaces[j]; + var transCenter = this._convertPointToTranslation(surface.center); + surface.dist = this.showPerspective ? transCenter.length() : -transCenter.z; + // TODO: this dept calculation doesn't work 100% of the cases due to perspective, + // but the current solution is fast/simple and works in 99.9% of all cases + // the issue is visible in example 14, with graph.setCameraPosition({horizontal: 2.97, vertical: 0.5, distance: 0.9}) + } - /** - * Calculate the conversion offset and scale for current range, based on - * the provided width - * @param {Number} width - * @returns {{offset: number, scale: number}} conversion - */ - Range.prototype.conversion = function (width) { - return Range.conversion(this.start, this.end, width); - }; + // order the surfaces by their (translated) depth + surfaces.sort(function (a, b) { + var diff = b.dist - a.dist; + if (diff) return diff; - /** - * Static method to calculate the conversion offset and scale for a range, - * based on the provided start, end, and width - * @param {Number} start - * @param {Number} end - * @param {Number} width - * @returns {{offset: number, scale: number}} conversion - */ - Range.conversion = function (start, end, width) { - if (width != 0 && (end - start != 0)) { - return { - offset: start, - scale: width / (end - start) + // if equal depth, sort the top surface last + if (a.corners === top) return 1; + if (b.corners === top) return -1; + + // both are equal + return 0; + }); + + // draw the ordered surfaces + ctx.lineWidth = 1; + ctx.strokeStyle = borderColor; + ctx.fillStyle = color; + // NOTE: we start at j=2 instead of j=0 as we don't need to draw the two surfaces at the backside + for (j = 2; j < surfaces.length; j++) { + surface = surfaces[j]; + corners = surface.corners; + ctx.beginPath(); + ctx.moveTo(corners[3].screen.x, corners[3].screen.y); + ctx.lineTo(corners[0].screen.x, corners[0].screen.y); + ctx.lineTo(corners[1].screen.x, corners[1].screen.y); + ctx.lineTo(corners[2].screen.x, corners[2].screen.y); + ctx.lineTo(corners[3].screen.x, corners[3].screen.y); + ctx.fill(); + ctx.stroke(); } } - else { - return { - offset: 0, - scale: 1 - }; - } }; + /** - * Start dragging horizontally or vertically - * @param {Event} event - * @private + * Draw a line through all datapoints. + * This function can be used when the style is 'line' */ - Range.prototype._onDragStart = function(event) { - // only allow dragging when configured as movable - if (!this.options.moveable) return; + Graph3d.prototype._redrawDataLine = function() { + var canvas = this.frame.canvas, + ctx = canvas.getContext('2d'), + point, i; - // refuse to drag when we where pinching to prevent the timeline make a jump - // when releasing the fingers in opposite order from the touch screen - if (!this.props.touch.allowDragging) return; + if (this.dataPoints === undefined || this.dataPoints.length <= 0) + return; // TODO: throw exception? - this.props.touch.start = this.start; - this.props.touch.end = this.end; + // calculate the translations of all points + for (i = 0; i < this.dataPoints.length; i++) { + var trans = this._convertPointToTranslation(this.dataPoints[i].point); + var screen = this._convertTranslationToScreen(trans); - if (this.body.dom.root) { - this.body.dom.root.style.cursor = 'move'; + this.dataPoints[i].trans = trans; + this.dataPoints[i].screen = screen; } - }; - - /** - * Perform dragging operation - * @param {Event} event - * @private - */ - Range.prototype._onDrag = function (event) { - // only allow dragging when configured as movable - if (!this.options.moveable) return; - var direction = this.options.direction; - validateDirection(direction); - // refuse to drag when we where pinching to prevent the timeline make a jump - // when releasing the fingers in opposite order from the touch screen - if (!this.props.touch.allowDragging) return; - var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY, - interval = (this.props.touch.end - this.props.touch.start), - width = (direction == 'horizontal') ? this.body.domProps.center.width : this.body.domProps.center.height, - diffRange = -delta / width * interval; - this._applyRange(this.props.touch.start + diffRange, this.props.touch.end + diffRange); - this.body.emitter.emit('rangechange', { - start: new Date(this.start), - end: new Date(this.end) - }); - }; - /** - * Stop dragging operation - * @param {event} event - * @private - */ - Range.prototype._onDragEnd = function (event) { - // only allow dragging when configured as movable - if (!this.options.moveable) return; + // start the line + if (this.dataPoints.length > 0) { + point = this.dataPoints[0]; - // refuse to drag when we where pinching to prevent the timeline make a jump - // when releasing the fingers in opposite order from the touch screen - if (!this.props.touch.allowDragging) return; + ctx.lineWidth = 1; // TODO: make customizable + ctx.strokeStyle = 'blue'; // TODO: make customizable + ctx.beginPath(); + ctx.moveTo(point.screen.x, point.screen.y); + } - if (this.body.dom.root) { - this.body.dom.root.style.cursor = 'auto'; + // draw the datapoints as colored circles + for (i = 1; i < this.dataPoints.length; i++) { + point = this.dataPoints[i]; + ctx.lineTo(point.screen.x, point.screen.y); } - // fire a rangechanged event - this.body.emitter.emit('rangechanged', { - start: new Date(this.start), - end: new Date(this.end) - }); + // finish the line + if (this.dataPoints.length > 0) { + ctx.stroke(); + } }; /** - * Event handler for mouse wheel event, used to zoom - * Code from http://adomas.org/javascript-mouse-wheel/ - * @param {Event} event - * @private + * Start a moving operation inside the provided parent element + * @param {Event} event The event that occurred (required for + * retrieving the mouse position) */ - Range.prototype._onMouseWheel = function(event) { - // only allow zooming when configured as zoomable and moveable - if (!(this.options.zoomable && this.options.moveable)) return; + Graph3d.prototype._onMouseDown = function(event) { + event = event || window.event; - // retrieve delta - var delta = 0; - if (event.wheelDelta) { /* IE/Opera. */ - delta = event.wheelDelta / 120; - } else if (event.detail) { /* Mozilla case. */ - // In Mozilla, sign of delta is different than in IE. - // Also, delta is multiple of 3. - delta = -event.detail / 3; + // check if mouse is still down (may be up when focus is lost for example + // in an iframe) + if (this.leftButtonDown) { + this._onMouseUp(event); } - // If delta is nonzero, handle it. - // Basically, delta is now positive if wheel was scrolled up, - // and negative, if wheel was scrolled down. - if (delta) { - // perform the zoom action. Delta is normally 1 or -1 - - // adjust a negative delta such that zooming in with delta 0.1 - // equals zooming out with a delta -0.1 - var scale; - if (delta < 0) { - scale = 1 - (delta / 5); - } - else { - scale = 1 / (1 + (delta / 5)) ; - } + // only react on left mouse button down + this.leftButtonDown = event.which ? (event.which === 1) : (event.button === 1); + if (!this.leftButtonDown && !this.touchDown) return; - // calculate center, the date to zoom around - var gesture = util.fakeGesture(this, event), - pointer = getPointer(gesture.center, this.body.dom.center), - pointerDate = this._pointerToDate(pointer); + // get mouse position (different code for IE and all other browsers) + this.startMouseX = getMouseX(event); + this.startMouseY = getMouseY(event); - this.zoom(scale, pointerDate); - } + this.startStart = new Date(this.start); + this.startEnd = new Date(this.end); + this.startArmRotation = this.camera.getArmRotation(); - // Prevent default actions caused by mouse wheel - // (else the page and timeline both zoom and scroll) - event.preventDefault(); - }; + this.frame.style.cursor = 'move'; - /** - * Start of a touch gesture - * @private - */ - Range.prototype._onTouch = function (event) { - this.props.touch.start = this.start; - this.props.touch.end = this.end; - this.props.touch.allowDragging = true; - this.props.touch.center = null; + // add event listeners to handle moving the contents + // we store the function onmousemove and onmouseup in the graph, so we can + // remove the eventlisteners lateron in the function mouseUp() + var me = this; + this.onmousemove = function (event) {me._onMouseMove(event);}; + this.onmouseup = function (event) {me._onMouseUp(event);}; + G3DaddEventListener(document, 'mousemove', me.onmousemove); + G3DaddEventListener(document, 'mouseup', me.onmouseup); + G3DpreventDefault(event); }; - /** - * On start of a hold gesture - * @private - */ - Range.prototype._onHold = function () { - this.props.touch.allowDragging = false; - }; /** - * Handle pinch event - * @param {Event} event - * @private + * Perform moving operating. + * This function activated from within the funcion Graph.mouseDown(). + * @param {Event} event Well, eehh, the event */ - Range.prototype._onPinch = function (event) { - // only allow zooming when configured as zoomable and moveable - if (!(this.options.zoomable && this.options.moveable)) return; - - this.props.touch.allowDragging = false; + Graph3d.prototype._onMouseMove = function (event) { + event = event || window.event; - if (event.gesture.touches.length > 1) { - if (!this.props.touch.center) { - this.props.touch.center = getPointer(event.gesture.center, this.body.dom.center); - } + // calculate change in mouse position + var diffX = parseFloat(getMouseX(event)) - this.startMouseX; + var diffY = parseFloat(getMouseY(event)) - this.startMouseY; - var scale = 1 / event.gesture.scale, - initDate = this._pointerToDate(this.props.touch.center); + var horizontalNew = this.startArmRotation.horizontal + diffX / 200; + var verticalNew = this.startArmRotation.vertical + diffY / 200; - // calculate new start and end - var newStart = parseInt(initDate + (this.props.touch.start - initDate) * scale); - var newEnd = parseInt(initDate + (this.props.touch.end - initDate) * scale); + var snapAngle = 4; // degrees + var snapValue = Math.sin(snapAngle / 360 * 2 * Math.PI); - // apply new range - this.setRange(newStart, newEnd); + // snap horizontally to nice angles at 0pi, 0.5pi, 1pi, 1.5pi, etc... + // the -0.001 is to take care that the vertical axis is always drawn at the left front corner + if (Math.abs(Math.sin(horizontalNew)) < snapValue) { + horizontalNew = Math.round((horizontalNew / Math.PI)) * Math.PI - 0.001; + } + if (Math.abs(Math.cos(horizontalNew)) < snapValue) { + horizontalNew = (Math.round((horizontalNew/ Math.PI - 0.5)) + 0.5) * Math.PI - 0.001; } - }; - - /** - * Helper function to calculate the center date for zooming - * @param {{x: Number, y: Number}} pointer - * @return {number} date - * @private - */ - Range.prototype._pointerToDate = function (pointer) { - var conversion; - var direction = this.options.direction; - - validateDirection(direction); - if (direction == 'horizontal') { - var width = this.body.domProps.center.width; - conversion = this.conversion(width); - return pointer.x / conversion.scale + conversion.offset; + // snap vertically to nice angles + if (Math.abs(Math.sin(verticalNew)) < snapValue) { + verticalNew = Math.round((verticalNew / Math.PI)) * Math.PI; } - else { - var height = this.body.domProps.center.height; - conversion = this.conversion(height); - return pointer.y / conversion.scale + conversion.offset; + if (Math.abs(Math.cos(verticalNew)) < snapValue) { + verticalNew = (Math.round((verticalNew/ Math.PI - 0.5)) + 0.5) * Math.PI; } + + this.camera.setArmRotation(horizontalNew, verticalNew); + this.redraw(); + + // fire a cameraPositionChange event + var parameters = this.getCameraPosition(); + this.emit('cameraPositionChange', parameters); + + G3DpreventDefault(event); }; - /** - * Get the pointer location relative to the location of the dom element - * @param {{pageX: Number, pageY: Number}} touch - * @param {Element} element HTML DOM element - * @return {{x: Number, y: Number}} pointer - * @private - */ - function getPointer (touch, element) { - return { - x: touch.pageX - util.getAbsoluteLeft(element), - y: touch.pageY - util.getAbsoluteTop(element) - }; - } /** - * Zoom the range the given scale in or out. Start and end date will - * be adjusted, and the timeline will be redrawn. You can optionally give a - * date around which to zoom. - * For example, try scale = 0.9 or 1.1 - * @param {Number} scale Scaling factor. Values above 1 will zoom out, - * values below 1 will zoom in. - * @param {Number} [center] Value representing a date around which will - * be zoomed. + * Stop moving operating. + * This function activated from within the funcion Graph.mouseDown(). + * @param {event} event The event */ - Range.prototype.zoom = function(scale, center) { - // if centerDate is not provided, take it half between start Date and end Date - if (center == null) { - center = (this.start + this.end) / 2; - } - - // calculate new start and end - var newStart = center + (this.start - center) * scale; - var newEnd = center + (this.end - center) * scale; + Graph3d.prototype._onMouseUp = function (event) { + this.frame.style.cursor = 'auto'; + this.leftButtonDown = false; - this.setRange(newStart, newEnd); + // remove event listeners here + G3DremoveEventListener(document, 'mousemove', this.onmousemove); + G3DremoveEventListener(document, 'mouseup', this.onmouseup); + G3DpreventDefault(event); }; /** - * Move the range with a given delta to the left or right. Start and end - * value will be adjusted. For example, try delta = 0.1 or -0.1 - * @param {Number} delta Moving amount. Positive value will move right, - * negative value will move left + * After having moved the mouse, a tooltip should pop up when the mouse is resting on a data point + * @param {Event} event A mouse move event */ - Range.prototype.move = function(delta) { - // zoom start Date and end Date relative to the centerDate - var diff = (this.end - this.start); + Graph3d.prototype._onTooltip = function (event) { + var delay = 300; // ms + var mouseX = getMouseX(event) - getAbsoluteLeft(this.frame); + var mouseY = getMouseY(event) - getAbsoluteTop(this.frame); - // apply new values - var newStart = this.start + diff * delta; - var newEnd = this.end + diff * delta; + if (!this.showTooltip) { + return; + } - // TODO: reckon with min and max range + if (this.tooltipTimeout) { + clearTimeout(this.tooltipTimeout); + } - this.start = newStart; - this.end = newEnd; + // (delayed) display of a tooltip only if no mouse button is down + if (this.leftButtonDown) { + this._hideTooltip(); + return; + } + + if (this.tooltip && this.tooltip.dataPoint) { + // tooltip is currently visible + var dataPoint = this._dataPointFromXY(mouseX, mouseY); + if (dataPoint !== this.tooltip.dataPoint) { + // datapoint changed + if (dataPoint) { + this._showTooltip(dataPoint); + } + else { + this._hideTooltip(); + } + } + } + else { + // tooltip is currently not visible + var me = this; + this.tooltipTimeout = setTimeout(function () { + me.tooltipTimeout = null; + + // show a tooltip if we have a data point + var dataPoint = me._dataPointFromXY(mouseX, mouseY); + if (dataPoint) { + me._showTooltip(dataPoint); + } + }, delay); + } }; /** - * Move the range to a new center point - * @param {Number} moveTo New center point of the range + * Event handler for touchstart event on mobile devices */ - Range.prototype.moveTo = function(moveTo) { - var center = (this.start + this.end) / 2; - - var diff = center - moveTo; + Graph3d.prototype._onTouchStart = function(event) { + this.touchDown = true; - // calculate new start and end - var newStart = this.start - diff; - var newEnd = this.end - diff; + var me = this; + this.ontouchmove = function (event) {me._onTouchMove(event);}; + this.ontouchend = function (event) {me._onTouchEnd(event);}; + G3DaddEventListener(document, 'touchmove', me.ontouchmove); + G3DaddEventListener(document, 'touchend', me.ontouchend); - this.setRange(newStart, newEnd); + this._onMouseDown(event); }; - module.exports = Range; - - -/***/ }, -/* 10 */ -/***/ function(module, exports, __webpack_require__) { - - // Utility functions for ordering and stacking of items - var EPSILON = 0.001; // used when checking collisions, to prevent round-off errors - /** - * Order items by their start data - * @param {Item[]} items + * Event handler for touchmove event on mobile devices */ - exports.orderByStart = function(items) { - items.sort(function (a, b) { - return a.data.start - b.data.start; - }); + Graph3d.prototype._onTouchMove = function(event) { + this._onMouseMove(event); }; /** - * Order items by their end date. If they have no end date, their start date - * is used. - * @param {Item[]} items + * Event handler for touchend event on mobile devices */ - exports.orderByEnd = function(items) { - items.sort(function (a, b) { - var aTime = ('end' in a.data) ? a.data.end : a.data.start, - bTime = ('end' in b.data) ? b.data.end : b.data.start; + Graph3d.prototype._onTouchEnd = function(event) { + this.touchDown = false; - return aTime - bTime; - }); + G3DremoveEventListener(document, 'touchmove', this.ontouchmove); + G3DremoveEventListener(document, 'touchend', this.ontouchend); + + this._onMouseUp(event); }; + /** - * Adjust vertical positions of the items such that they don't overlap each - * other. - * @param {Item[]} items - * All visible items - * @param {{item: number, axis: number}} margin - * Margins between items and between items and the axis. - * @param {boolean} [force=false] - * If true, all items will be repositioned. If false (default), only - * items having a top===null will be re-stacked + * Event handler for mouse wheel event, used to zoom the graph + * Code from http://adomas.org/javascript-mouse-wheel/ + * @param {event} event The event */ - exports.stack = function(items, margin, force) { - var i, iMax; + Graph3d.prototype._onWheel = function(event) { + if (!event) /* For IE. */ + event = window.event; - if (force) { - // reset top position of all items - for (i = 0, iMax = items.length; i < iMax; i++) { - items[i].top = null; - } + // retrieve delta + var delta = 0; + if (event.wheelDelta) { /* IE/Opera. */ + delta = event.wheelDelta/120; + } else if (event.detail) { /* Mozilla case. */ + // In Mozilla, sign of delta is different than in IE. + // Also, delta is multiple of 3. + delta = -event.detail/3; } - // calculate new, non-overlapping positions - for (i = 0, iMax = items.length; i < iMax; i++) { - var item = items[i]; - if (item.top === null) { - // initialize top position - item.top = margin.axis; + // If delta is nonzero, handle it. + // Basically, delta is now positive if wheel was scrolled up, + // and negative, if wheel was scrolled down. + if (delta) { + var oldLength = this.camera.getArmLength(); + var newLength = oldLength * (1 - delta / 10); - do { - // TODO: optimize checking for overlap. when there is a gap without items, - // you only need to check for items from the next item on, not from zero - var collidingItem = null; - for (var j = 0, jj = items.length; j < jj; j++) { - var other = items[j]; - if (other.top !== null && other !== item && exports.collision(item, other, margin.item)) { - collidingItem = other; - break; - } - } + this.camera.setArmLength(newLength); + this.redraw(); - if (collidingItem != null) { - // There is a collision. Reposition the items above the colliding element - item.top = collidingItem.top + collidingItem.height + margin.item; - } - } while (collidingItem); - } + this._hideTooltip(); } + + // fire a cameraPositionChange event + var parameters = this.getCameraPosition(); + this.emit('cameraPositionChange', parameters); + + // Prevent default actions caused by mouse wheel. + // That might be ugly, but we handle scrolls somehow + // anyway, so don't bother here.. + G3DpreventDefault(event); }; /** - * Adjust vertical positions of the items without stacking them - * @param {Item[]} items - * All visible items - * @param {{item: number, axis: number}} margin - * Margins between items and between items and the axis. + * Test whether a point lies inside given 2D triangle + * @param {Point2d} point + * @param {Point2d[]} triangle + * @return {boolean} Returns true if given point lies inside or on the edge of the triangle + * @private */ - exports.nostack = function(items, margin) { - var i, iMax; + Graph3d.prototype._insideTriangle = function (point, triangle) { + var a = triangle[0], + b = triangle[1], + c = triangle[2]; - // reset top position of all items - for (i = 0, iMax = items.length; i < iMax; i++) { - items[i].top = margin.axis; + function sign (x) { + return x > 0 ? 1 : x < 0 ? -1 : 0; } + + var as = sign((b.x - a.x) * (point.y - a.y) - (b.y - a.y) * (point.x - a.x)); + var bs = sign((c.x - b.x) * (point.y - b.y) - (c.y - b.y) * (point.x - b.x)); + var cs = sign((a.x - c.x) * (point.y - c.y) - (a.y - c.y) * (point.x - c.x)); + + // each of the three signs must be either equal to each other or zero + return (as == 0 || bs == 0 || as == bs) && + (bs == 0 || cs == 0 || bs == cs) && + (as == 0 || cs == 0 || as == cs); }; /** - * Test if the two provided items collide - * The items must have parameters left, width, top, and height. - * @param {Item} a The first item - * @param {Item} b The second item - * @param {Number} margin A minimum required margin. - * If margin is provided, the two items will be - * marked colliding when they overlap or - * when the margin between the two is smaller than - * the requested margin. - * @return {boolean} true if a and b collide, else false + * Find a data point close to given screen position (x, y) + * @param {Number} x + * @param {Number} y + * @return {Object | null} The closest data point or null if not close to any data point + * @private */ - exports.collision = function(a, b, margin) { - return ((a.left - margin + EPSILON) < (b.left + b.width) && - (a.left + a.width + margin - EPSILON) > b.left && - (a.top - margin + EPSILON) < (b.top + b.height) && - (a.top + a.height + margin - EPSILON) > b.top); - }; + Graph3d.prototype._dataPointFromXY = function (x, y) { + var i, + distMax = 100, // px + dataPoint = null, + closestDataPoint = null, + closestDist = null, + center = new Point2d(x, y); + + if (this.style === Graph3d.STYLE.BAR || + this.style === Graph3d.STYLE.BARCOLOR || + this.style === Graph3d.STYLE.BARSIZE) { + // the data points are ordered from far away to closest + for (i = this.dataPoints.length - 1; i >= 0; i--) { + dataPoint = this.dataPoints[i]; + var surfaces = dataPoint.surfaces; + if (surfaces) { + for (var s = surfaces.length - 1; s >= 0; s--) { + // split each surface in two triangles, and see if the center point is inside one of these + var surface = surfaces[s]; + var corners = surface.corners; + var triangle1 = [corners[0].screen, corners[1].screen, corners[2].screen]; + var triangle2 = [corners[2].screen, corners[3].screen, corners[0].screen]; + if (this._insideTriangle(center, triangle1) || + this._insideTriangle(center, triangle2)) { + // return immediately at the first hit + return dataPoint; + } + } + } + } + } + else { + // find the closest data point, using distance to the center of the point on 2d screen + for (i = 0; i < this.dataPoints.length; i++) { + dataPoint = this.dataPoints[i]; + var point = dataPoint.screen; + if (point) { + var distX = Math.abs(x - point.x); + var distY = Math.abs(y - point.y); + var dist = Math.sqrt(distX * distX + distY * distY); + if ((closestDist === null || dist < closestDist) && dist < distMax) { + closestDist = dist; + closestDataPoint = dataPoint; + } + } + } + } -/***/ }, -/* 11 */ -/***/ function(module, exports, __webpack_require__) { - var moment = __webpack_require__(39); + return closestDataPoint; + }; /** - * @constructor TimeStep - * The class TimeStep is an iterator for dates. You provide a start date and an - * end date. The class itself determines the best scale (step size) based on the - * provided start Date, end Date, and minimumStep. - * - * If minimumStep is provided, the step size is chosen as close as possible - * to the minimumStep but larger than minimumStep. If minimumStep is not - * provided, the scale is set to 1 DAY. - * The minimumStep should correspond with the onscreen size of about 6 characters - * - * Alternatively, you can set a scale by hand. - * After creation, you can initialize the class by executing first(). Then you - * can iterate from the start date to the end date via next(). You can check if - * the end date is reached with the function hasNext(). After each step, you can - * retrieve the current date via getCurrent(). - * The TimeStep has scales ranging from milliseconds, seconds, minutes, hours, - * days, to years. - * - * Version: 1.2 - * - * @param {Date} [start] The start date, for example new Date(2010, 9, 21) - * or new Date(2010, 9, 21, 23, 45, 00) - * @param {Date} [end] The end date - * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds + * Display a tooltip for given data point + * @param {Object} dataPoint + * @private */ - function TimeStep(start, end, minimumStep) { - // variables - this.current = new Date(); - this._start = new Date(); - this._end = new Date(); - - this.autoScale = true; - this.scale = TimeStep.SCALE.DAY; - this.step = 1; + Graph3d.prototype._showTooltip = function (dataPoint) { + var content, line, dot; - // initialize the range - this.setRange(start, end, minimumStep); - } + if (!this.tooltip) { + content = document.createElement('div'); + content.style.position = 'absolute'; + content.style.padding = '10px'; + content.style.border = '1px solid #4d4d4d'; + content.style.color = '#1a1a1a'; + content.style.background = 'rgba(255,255,255,0.7)'; + content.style.borderRadius = '2px'; + content.style.boxShadow = '5px 5px 10px rgba(128,128,128,0.5)'; - /// enum scale - TimeStep.SCALE = { - MILLISECOND: 1, - SECOND: 2, - MINUTE: 3, - HOUR: 4, - DAY: 5, - WEEKDAY: 6, - MONTH: 7, - YEAR: 8 - }; + line = document.createElement('div'); + line.style.position = 'absolute'; + line.style.height = '40px'; + line.style.width = '0'; + line.style.borderLeft = '1px solid #4d4d4d'; + dot = document.createElement('div'); + dot.style.position = 'absolute'; + dot.style.height = '0'; + dot.style.width = '0'; + dot.style.border = '5px solid #4d4d4d'; + dot.style.borderRadius = '5px'; - /** - * Set a new range - * If minimumStep is provided, the step size is chosen as close as possible - * to the minimumStep but larger than minimumStep. If minimumStep is not - * provided, the scale is set to 1 DAY. - * The minimumStep should correspond with the onscreen size of about 6 characters - * @param {Date} [start] The start date and time. - * @param {Date} [end] The end date and time. - * @param {int} [minimumStep] Optional. Minimum step size in milliseconds - */ - TimeStep.prototype.setRange = function(start, end, minimumStep) { - if (!(start instanceof Date) || !(end instanceof Date)) { - throw "No legal start or end date in method setRange"; + this.tooltip = { + dataPoint: null, + dom: { + content: content, + line: line, + dot: dot + } + }; + } + else { + content = this.tooltip.dom.content; + line = this.tooltip.dom.line; + dot = this.tooltip.dom.dot; } - this._start = (start != undefined) ? new Date(start.valueOf()) : new Date(); - this._end = (end != undefined) ? new Date(end.valueOf()) : new Date(); + this._hideTooltip(); - if (this.autoScale) { - this.setMinimumStep(minimumStep); + this.tooltip.dataPoint = dataPoint; + if (typeof this.showTooltip === 'function') { + content.innerHTML = this.showTooltip(dataPoint.point); + } + else { + content.innerHTML = '' + + '' + + '' + + '' + + '
x:' + dataPoint.point.x + '
y:' + dataPoint.point.y + '
z:' + dataPoint.point.z + '
'; } - }; - /** - * Set the range iterator to the start date. - */ - TimeStep.prototype.first = function() { - this.current = new Date(this._start.valueOf()); - this.roundToMinor(); + content.style.left = '0'; + content.style.top = '0'; + this.frame.appendChild(content); + this.frame.appendChild(line); + this.frame.appendChild(dot); + + // calculate sizes + var contentWidth = content.offsetWidth; + var contentHeight = content.offsetHeight; + var lineHeight = line.offsetHeight; + var dotWidth = dot.offsetWidth; + var dotHeight = dot.offsetHeight; + + var left = dataPoint.screen.x - contentWidth / 2; + left = Math.min(Math.max(left, 10), this.frame.clientWidth - 10 - contentWidth); + + line.style.left = dataPoint.screen.x + 'px'; + line.style.top = (dataPoint.screen.y - lineHeight) + 'px'; + content.style.left = left + 'px'; + content.style.top = (dataPoint.screen.y - lineHeight - contentHeight) + 'px'; + dot.style.left = (dataPoint.screen.x - dotWidth / 2) + 'px'; + dot.style.top = (dataPoint.screen.y - dotHeight / 2) + 'px'; }; /** - * Round the current date to the first minor date value - * This must be executed once when the current date is set to start Date + * Hide the tooltip when displayed + * @private */ - TimeStep.prototype.roundToMinor = function() { - // round to floor - // IMPORTANT: we have no breaks in this switch! (this is no bug) - //noinspection FallthroughInSwitchStatementJS - switch (this.scale) { - case TimeStep.SCALE.YEAR: - this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step)); - this.current.setMonth(0); - case TimeStep.SCALE.MONTH: this.current.setDate(1); - case TimeStep.SCALE.DAY: // intentional fall through - case TimeStep.SCALE.WEEKDAY: this.current.setHours(0); - case TimeStep.SCALE.HOUR: this.current.setMinutes(0); - case TimeStep.SCALE.MINUTE: this.current.setSeconds(0); - case TimeStep.SCALE.SECOND: this.current.setMilliseconds(0); - //case TimeStep.SCALE.MILLISECOND: // nothing to do for milliseconds - } + Graph3d.prototype._hideTooltip = function () { + if (this.tooltip) { + this.tooltip.dataPoint = null; - if (this.step != 1) { - // round down to the first minor value that is a multiple of the current step size - switch (this.scale) { - case TimeStep.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break; - case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break; - case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break; - case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break; - case TimeStep.SCALE.WEEKDAY: // intentional fall through - case TimeStep.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break; - case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break; - case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break; - default: break; + for (var prop in this.tooltip.dom) { + if (this.tooltip.dom.hasOwnProperty(prop)) { + var elem = this.tooltip.dom[prop]; + if (elem && elem.parentNode) { + elem.parentNode.removeChild(elem); + } + } } } }; + /** - * Check if the there is a next step - * @return {boolean} true if the current date has not passed the end date + * Add and event listener. Works for all browsers + * @param {Element} element An html element + * @param {string} action The action, for example 'click', + * without the prefix 'on' + * @param {function} listener The callback function to be executed + * @param {boolean} useCapture */ - TimeStep.prototype.hasNext = function () { - return (this.current.valueOf() <= this._end.valueOf()); + G3DaddEventListener = function(element, action, listener, useCapture) { + if (element.addEventListener) { + if (useCapture === undefined) + useCapture = false; + + if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) { + action = 'DOMMouseScroll'; // For Firefox + } + + element.addEventListener(action, listener, useCapture); + } else { + element.attachEvent('on' + action, listener); // IE browsers + } }; /** - * Do the next step + * Remove an event listener from an element + * @param {Element} element An html dom element + * @param {string} action The name of the event, for example 'mousedown' + * @param {function} listener The listener function + * @param {boolean} useCapture */ - TimeStep.prototype.next = function() { - var prev = this.current.valueOf(); - - // Two cases, needed to prevent issues with switching daylight savings - // (end of March and end of October) - if (this.current.getMonth() < 6) { - switch (this.scale) { - case TimeStep.SCALE.MILLISECOND: + G3DremoveEventListener = function(element, action, listener, useCapture) { + if (element.removeEventListener) { + // non-IE browsers + if (useCapture === undefined) + useCapture = false; - this.current = new Date(this.current.valueOf() + this.step); break; - case TimeStep.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break; - case TimeStep.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break; - case TimeStep.SCALE.HOUR: - this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60); - // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...) - var h = this.current.getHours(); - this.current.setHours(h - (h % this.step)); - break; - case TimeStep.SCALE.WEEKDAY: // intentional fall through - case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break; - case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break; - case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break; - default: break; - } - } - else { - switch (this.scale) { - case TimeStep.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break; - case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break; - case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break; - case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break; - case TimeStep.SCALE.WEEKDAY: // intentional fall through - case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break; - case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break; - case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break; - default: break; + if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) { + action = 'DOMMouseScroll'; // For Firefox } - } - if (this.step != 1) { - // round down to the correct major value - switch (this.scale) { - case TimeStep.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break; - case TimeStep.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break; - case TimeStep.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break; - case TimeStep.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break; - case TimeStep.SCALE.WEEKDAY: // intentional fall through - case TimeStep.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break; - case TimeStep.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break; - case TimeStep.SCALE.YEAR: break; // nothing to do for year - default: break; - } + element.removeEventListener(action, listener, useCapture); + } else { + // IE browsers + element.detachEvent('on' + action, listener); } + }; - // safety mechanism: if current time is still unchanged, move to the end - if (this.current.valueOf() == prev) { - this.current = new Date(this._end.valueOf()); + /** + * Stop event propagation + */ + G3DstopPropagation = function(event) { + if (!event) + event = window.event; + + if (event.stopPropagation) { + event.stopPropagation(); // non-IE browsers + } + else { + event.cancelBubble = true; // IE browsers } }; /** - * Get the current datetime - * @return {Date} current The current date + * Cancels the event if it is cancelable, without stopping further propagation of the event. */ - TimeStep.prototype.getCurrent = function() { - return this.current; + G3DpreventDefault = function (event) { + if (!event) + event = window.event; + + if (event.preventDefault) { + event.preventDefault(); // non-IE browsers + } + else { + event.returnValue = false; // IE browsers + } }; /** - * Set a custom scale. Autoscaling will be disabled. - * For example setScale(SCALE.MINUTES, 5) will result - * in minor steps of 5 minutes, and major steps of an hour. + * @constructor Slider * - * @param {TimeStep.SCALE} newScale - * A scale. Choose from SCALE.MILLISECOND, - * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR, - * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH, - * SCALE.YEAR. - * @param {Number} newStep A step size, by default 1. Choose for - * example 1, 2, 5, or 10. + * An html slider control with start/stop/prev/next buttons + * @param {Element} container The element where the slider will be created + * @param {Object} options Available options: + * {boolean} visible If true (default) the + * slider is visible. */ - TimeStep.prototype.setScale = function(newScale, newStep) { - this.scale = newScale; + function Slider(container, options) { + if (container === undefined) { + throw 'Error: No container element defined'; + } + this.container = container; + this.visible = (options && options.visible != undefined) ? options.visible : true; - if (newStep > 0) { - this.step = newStep; + if (this.visible) { + this.frame = document.createElement('DIV'); + //this.frame.style.backgroundColor = '#E5E5E5'; + this.frame.style.width = '100%'; + this.frame.style.position = 'relative'; + this.container.appendChild(this.frame); + + this.frame.prev = document.createElement('INPUT'); + this.frame.prev.type = 'BUTTON'; + this.frame.prev.value = 'Prev'; + this.frame.appendChild(this.frame.prev); + + this.frame.play = document.createElement('INPUT'); + this.frame.play.type = 'BUTTON'; + this.frame.play.value = 'Play'; + this.frame.appendChild(this.frame.play); + + this.frame.next = document.createElement('INPUT'); + this.frame.next.type = 'BUTTON'; + this.frame.next.value = 'Next'; + this.frame.appendChild(this.frame.next); + + this.frame.bar = document.createElement('INPUT'); + this.frame.bar.type = 'BUTTON'; + this.frame.bar.style.position = 'absolute'; + this.frame.bar.style.border = '1px solid red'; + this.frame.bar.style.width = '100px'; + this.frame.bar.style.height = '6px'; + this.frame.bar.style.borderRadius = '2px'; + this.frame.bar.style.MozBorderRadius = '2px'; + this.frame.bar.style.border = '1px solid #7F7F7F'; + this.frame.bar.style.backgroundColor = '#E5E5E5'; + this.frame.appendChild(this.frame.bar); + + this.frame.slide = document.createElement('INPUT'); + this.frame.slide.type = 'BUTTON'; + this.frame.slide.style.margin = '0px'; + this.frame.slide.value = ' '; + this.frame.slide.style.position = 'relative'; + this.frame.slide.style.left = '-100px'; + this.frame.appendChild(this.frame.slide); + + // create events + var me = this; + this.frame.slide.onmousedown = function (event) {me._onMouseDown(event);}; + this.frame.prev.onclick = function (event) {me.prev(event);}; + this.frame.play.onclick = function (event) {me.togglePlay(event);}; + this.frame.next.onclick = function (event) {me.next(event);}; } - this.autoScale = false; - }; + this.onChangeCallback = undefined; - /** - * Enable or disable autoscaling - * @param {boolean} enable If true, autoascaling is set true - */ - TimeStep.prototype.setAutoScale = function (enable) { - this.autoScale = enable; - }; + this.values = []; + this.index = undefined; + this.playTimeout = undefined; + this.playInterval = 1000; // milliseconds + this.playLoop = true; + } /** - * Automatically determine the scale that bests fits the provided minimum step - * @param {Number} [minimumStep] The minimum step size in milliseconds + * Select the previous index */ - TimeStep.prototype.setMinimumStep = function(minimumStep) { - if (minimumStep == undefined) { - return; + Slider.prototype.prev = function() { + var index = this.getIndex(); + if (index > 0) { + index--; + this.setIndex(index); } - - var stepYear = (1000 * 60 * 60 * 24 * 30 * 12); - var stepMonth = (1000 * 60 * 60 * 24 * 30); - var stepDay = (1000 * 60 * 60 * 24); - var stepHour = (1000 * 60 * 60); - var stepMinute = (1000 * 60); - var stepSecond = (1000); - var stepMillisecond= (1); - - // find the smallest step that is larger than the provided minimumStep - if (stepYear*1000 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1000;} - if (stepYear*500 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 500;} - if (stepYear*100 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 100;} - if (stepYear*50 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 50;} - if (stepYear*10 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 10;} - if (stepYear*5 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 5;} - if (stepYear > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1;} - if (stepMonth*3 > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 3;} - if (stepMonth > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 1;} - if (stepDay*5 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 5;} - if (stepDay*2 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 2;} - if (stepDay > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 1;} - if (stepDay/2 > minimumStep) {this.scale = TimeStep.SCALE.WEEKDAY; this.step = 1;} - if (stepHour*4 > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 4;} - if (stepHour > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 1;} - if (stepMinute*15 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 15;} - if (stepMinute*10 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 10;} - if (stepMinute*5 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 5;} - if (stepMinute > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 1;} - if (stepSecond*15 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 15;} - if (stepSecond*10 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 10;} - if (stepSecond*5 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 5;} - if (stepSecond > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 1;} - if (stepMillisecond*200 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 200;} - if (stepMillisecond*100 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 100;} - if (stepMillisecond*50 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 50;} - if (stepMillisecond*10 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 10;} - if (stepMillisecond*5 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 5;} - if (stepMillisecond > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 1;} }; /** - * Snap a date to a rounded value. - * The snap intervals are dependent on the current scale and step. - * @param {Date} date the date to be snapped. - * @return {Date} snappedDate + * Select the next index */ - TimeStep.prototype.snap = function(date) { - var clone = new Date(date.valueOf()); - - if (this.scale == TimeStep.SCALE.YEAR) { - var year = clone.getFullYear() + Math.round(clone.getMonth() / 12); - clone.setFullYear(Math.round(year / this.step) * this.step); - clone.setMonth(0); - clone.setDate(0); - clone.setHours(0); - clone.setMinutes(0); - clone.setSeconds(0); - clone.setMilliseconds(0); + Slider.prototype.next = function() { + var index = this.getIndex(); + if (index < this.values.length - 1) { + index++; + this.setIndex(index); } - else if (this.scale == TimeStep.SCALE.MONTH) { - if (clone.getDate() > 15) { - clone.setDate(1); - clone.setMonth(clone.getMonth() + 1); - // important: first set Date to 1, after that change the month. - } - else { - clone.setDate(1); - } + }; - clone.setHours(0); - clone.setMinutes(0); - clone.setSeconds(0); - clone.setMilliseconds(0); - } - else if (this.scale == TimeStep.SCALE.DAY) { - //noinspection FallthroughInSwitchStatementJS - switch (this.step) { - case 5: - case 2: - clone.setHours(Math.round(clone.getHours() / 24) * 24); break; - default: - clone.setHours(Math.round(clone.getHours() / 12) * 12); break; - } - clone.setMinutes(0); - clone.setSeconds(0); - clone.setMilliseconds(0); - } - else if (this.scale == TimeStep.SCALE.WEEKDAY) { - //noinspection FallthroughInSwitchStatementJS - switch (this.step) { - case 5: - case 2: - clone.setHours(Math.round(clone.getHours() / 12) * 12); break; - default: - clone.setHours(Math.round(clone.getHours() / 6) * 6); break; - } - clone.setMinutes(0); - clone.setSeconds(0); - clone.setMilliseconds(0); - } - else if (this.scale == TimeStep.SCALE.HOUR) { - switch (this.step) { - case 4: - clone.setMinutes(Math.round(clone.getMinutes() / 60) * 60); break; - default: - clone.setMinutes(Math.round(clone.getMinutes() / 30) * 30); break; - } - clone.setSeconds(0); - clone.setMilliseconds(0); - } else if (this.scale == TimeStep.SCALE.MINUTE) { - //noinspection FallthroughInSwitchStatementJS - switch (this.step) { - case 15: - case 10: - clone.setMinutes(Math.round(clone.getMinutes() / 5) * 5); - clone.setSeconds(0); - break; - case 5: - clone.setSeconds(Math.round(clone.getSeconds() / 60) * 60); break; - default: - clone.setSeconds(Math.round(clone.getSeconds() / 30) * 30); break; - } - clone.setMilliseconds(0); - } - else if (this.scale == TimeStep.SCALE.SECOND) { - //noinspection FallthroughInSwitchStatementJS - switch (this.step) { - case 15: - case 10: - clone.setSeconds(Math.round(clone.getSeconds() / 5) * 5); - clone.setMilliseconds(0); - break; - case 5: - clone.setMilliseconds(Math.round(clone.getMilliseconds() / 1000) * 1000); break; - default: - clone.setMilliseconds(Math.round(clone.getMilliseconds() / 500) * 500); break; - } + /** + * Select the next index + */ + Slider.prototype.playNext = function() { + var start = new Date(); + + var index = this.getIndex(); + if (index < this.values.length - 1) { + index++; + this.setIndex(index); } - else if (this.scale == TimeStep.SCALE.MILLISECOND) { - var step = this.step > 5 ? this.step / 2 : 1; - clone.setMilliseconds(Math.round(clone.getMilliseconds() / step) * step); + else if (this.playLoop) { + // jump to the start + index = 0; + this.setIndex(index); } - - return clone; + + var end = new Date(); + var diff = (end - start); + + // calculate how much time it to to set the index and to execute the callback + // function. + var interval = Math.max(this.playInterval - diff, 0); + // document.title = diff // TODO: cleanup + + var me = this; + this.playTimeout = setTimeout(function() {me.playNext();}, interval); }; /** - * Check if the current value is a major value (for example when the step - * is DAY, a major value is each first day of the MONTH) - * @return {boolean} true if current date is major, else false. + * Toggle start or stop playing */ - TimeStep.prototype.isMajor = function() { - switch (this.scale) { - case TimeStep.SCALE.MILLISECOND: - return (this.current.getMilliseconds() == 0); - case TimeStep.SCALE.SECOND: - return (this.current.getSeconds() == 0); - case TimeStep.SCALE.MINUTE: - return (this.current.getHours() == 0) && (this.current.getMinutes() == 0); - // Note: this is no bug. Major label is equal for both minute and hour scale - case TimeStep.SCALE.HOUR: - return (this.current.getHours() == 0); - case TimeStep.SCALE.WEEKDAY: // intentional fall through - case TimeStep.SCALE.DAY: - return (this.current.getDate() == 1); - case TimeStep.SCALE.MONTH: - return (this.current.getMonth() == 0); - case TimeStep.SCALE.YEAR: - return false; - default: - return false; + Slider.prototype.togglePlay = function() { + if (this.playTimeout === undefined) { + this.play(); + } else { + this.stop(); } }; - /** - * Returns formatted text for the minor axislabel, depending on the current - * date and the scale. For example when scale is MINUTE, the current time is - * formatted as "hh:mm". - * @param {Date} [date] custom date. if not provided, current date is taken + * Start playing */ - TimeStep.prototype.getLabelMinor = function(date) { - if (date == undefined) { - date = this.current; - } + Slider.prototype.play = function() { + // Test whether already playing + if (this.playTimeout) return; - switch (this.scale) { - case TimeStep.SCALE.MILLISECOND: return moment(date).format('SSS'); - case TimeStep.SCALE.SECOND: return moment(date).format('s'); - case TimeStep.SCALE.MINUTE: return moment(date).format('HH:mm'); - case TimeStep.SCALE.HOUR: return moment(date).format('HH:mm'); - case TimeStep.SCALE.WEEKDAY: return moment(date).format('ddd D'); - case TimeStep.SCALE.DAY: return moment(date).format('D'); - case TimeStep.SCALE.MONTH: return moment(date).format('MMM'); - case TimeStep.SCALE.YEAR: return moment(date).format('YYYY'); - default: return ''; + this.playNext(); + + if (this.frame) { + this.frame.play.value = 'Stop'; } }; - /** - * Returns formatted text for the major axis label, depending on the current - * date and the scale. For example when scale is MINUTE, the major scale is - * hours, and the hour will be formatted as "hh". - * @param {Date} [date] custom date. if not provided, current date is taken + * Stop playing */ - TimeStep.prototype.getLabelMajor = function(date) { - if (date == undefined) { - date = this.current; + Slider.prototype.stop = function() { + clearInterval(this.playTimeout); + this.playTimeout = undefined; + + if (this.frame) { + this.frame.play.value = 'Play'; } + }; - //noinspection FallthroughInSwitchStatementJS - switch (this.scale) { - case TimeStep.SCALE.MILLISECOND:return moment(date).format('HH:mm:ss'); - case TimeStep.SCALE.SECOND: return moment(date).format('D MMMM HH:mm'); - case TimeStep.SCALE.MINUTE: - case TimeStep.SCALE.HOUR: return moment(date).format('ddd D MMMM'); - case TimeStep.SCALE.WEEKDAY: - case TimeStep.SCALE.DAY: return moment(date).format('MMMM YYYY'); - case TimeStep.SCALE.MONTH: return moment(date).format('YYYY'); - case TimeStep.SCALE.YEAR: return ''; - default: return ''; + /** + * Set a callback function which will be triggered when the value of the + * slider bar has changed. + */ + Slider.prototype.setOnChangeCallback = function(callback) { + this.onChangeCallback = callback; + }; + + /** + * Set the interval for playing the list + * @param {Number} interval The interval in milliseconds + */ + Slider.prototype.setPlayInterval = function(interval) { + this.playInterval = interval; + }; + + /** + * Retrieve the current play interval + * @return {Number} interval The interval in milliseconds + */ + Slider.prototype.getPlayInterval = function(interval) { + return this.playInterval; + }; + + /** + * Set looping on or off + * @pararm {boolean} doLoop If true, the slider will jump to the start when + * the end is passed, and will jump to the end + * when the start is passed. + */ + Slider.prototype.setPlayLoop = function(doLoop) { + this.playLoop = doLoop; + }; + + + /** + * Execute the onchange callback function + */ + Slider.prototype.onChange = function() { + if (this.onChangeCallback !== undefined) { + this.onChangeCallback(); } }; - module.exports = TimeStep; + /** + * redraw the slider on the correct place + */ + Slider.prototype.redraw = function() { + if (this.frame) { + // resize the bar + this.frame.bar.style.top = (this.frame.clientHeight/2 - + this.frame.bar.offsetHeight/2) + 'px'; + this.frame.bar.style.width = (this.frame.clientWidth - + this.frame.prev.clientWidth - + this.frame.play.clientWidth - + this.frame.next.clientWidth - 30) + 'px'; + + // position the slider button + var left = this.indexToLeft(this.index); + this.frame.slide.style.left = (left) + 'px'; + } + }; + + + /** + * Set the list with values for the slider + * @param {Array} values A javascript array with values (any type) + */ + Slider.prototype.setValues = function(values) { + this.values = values; + + if (this.values.length > 0) + this.setIndex(0); + else + this.index = undefined; + }; + + /** + * Select a value by its index + * @param {Number} index + */ + Slider.prototype.setIndex = function(index) { + if (index < this.values.length) { + this.index = index; + + this.redraw(); + this.onChange(); + } + else { + throw 'Error: index out of range'; + } + }; + + /** + * retrieve the index of the currently selected vaue + * @return {Number} index + */ + Slider.prototype.getIndex = function() { + return this.index; + }; + + + /** + * retrieve the currently selected value + * @return {*} value + */ + Slider.prototype.get = function() { + return this.values[this.index]; + }; + + + Slider.prototype._onMouseDown = function(event) { + // only react on left mouse button down + var leftButtonDown = event.which ? (event.which === 1) : (event.button === 1); + if (!leftButtonDown) return; + + this.startClientX = event.clientX; + this.startSlideX = parseFloat(this.frame.slide.style.left); + + this.frame.style.cursor = 'move'; + + // add event listeners to handle moving the contents + // we store the function onmousemove and onmouseup in the graph, so we can + // remove the eventlisteners lateron in the function mouseUp() + var me = this; + this.onmousemove = function (event) {me._onMouseMove(event);}; + this.onmouseup = function (event) {me._onMouseUp(event);}; + G3DaddEventListener(document, 'mousemove', this.onmousemove); + G3DaddEventListener(document, 'mouseup', this.onmouseup); + G3DpreventDefault(event); + }; + + + Slider.prototype.leftToIndex = function (left) { + var width = parseFloat(this.frame.bar.style.width) - + this.frame.slide.clientWidth - 10; + var x = left - 3; + + var index = Math.round(x / width * (this.values.length-1)); + if (index < 0) index = 0; + if (index > this.values.length-1) index = this.values.length-1; + + return index; + }; + + Slider.prototype.indexToLeft = function (index) { + var width = parseFloat(this.frame.bar.style.width) - + this.frame.slide.clientWidth - 10; + + var x = index / (this.values.length-1) * width; + var left = x + 3; + + return left; + }; + + + + Slider.prototype._onMouseMove = function (event) { + var diff = event.clientX - this.startClientX; + var x = this.startSlideX + diff; + + var index = this.leftToIndex(x); + + this.setIndex(index); + + G3DpreventDefault(); + }; + + + Slider.prototype._onMouseUp = function (event) { + this.frame.style.cursor = 'auto'; + + // remove event listeners + G3DremoveEventListener(document, 'mousemove', this.onmousemove); + G3DremoveEventListener(document, 'mouseup', this.onmouseup); + + G3DpreventDefault(); + }; + + + + /**--------------------------------------------------------------------------**/ + + + + /** + * Retrieve the absolute left value of a DOM element + * @param {Element} elem A dom element, for example a div + * @return {Number} left The absolute left position of this element + * in the browser page. + */ + getAbsoluteLeft = function(elem) { + var left = 0; + while( elem !== null ) { + left += elem.offsetLeft; + left -= elem.scrollLeft; + elem = elem.offsetParent; + } + return left; + }; + + /** + * Retrieve the absolute top value of a DOM element + * @param {Element} elem A dom element, for example a div + * @return {Number} top The absolute top position of this element + * in the browser page. + */ + getAbsoluteTop = function(elem) { + var top = 0; + while( elem !== null ) { + top += elem.offsetTop; + top -= elem.scrollTop; + elem = elem.offsetParent; + } + return top; + }; + + /** + * Get the horizontal mouse position from a mouse event + * @param {Event} event + * @return {Number} mouse x + */ + getMouseX = function(event) { + if ('clientX' in event) return event.clientX; + return event.targetTouches[0] && event.targetTouches[0].clientX || 0; + }; + + /** + * Get the vertical mouse position from a mouse event + * @param {Event} event + * @return {Number} mouse y + */ + getMouseY = function(event) { + if ('clientY' in event) return event.clientY; + return event.targetTouches[0] && event.targetTouches[0].clientY || 0; + }; + + module.exports = Graph3d; /***/ }, @@ -9041,7 +9040,7 @@ return /******/ (function(modules) { // webpackBootstrap /* 14 */ /***/ function(module, exports, __webpack_require__) { - var Hammer = __webpack_require__(49); + var Hammer = __webpack_require__(50); var util = __webpack_require__(1); var Component = __webpack_require__(12); @@ -9238,7 +9237,7 @@ return /******/ (function(modules) { // webpackBootstrap var util = __webpack_require__(1); var DOMutil = __webpack_require__(2); var Component = __webpack_require__(12); - var DataStep = __webpack_require__(8); + var DataStep = __webpack_require__(6); /** * A horizontal time axis @@ -9844,7 +9843,7 @@ return /******/ (function(modules) { // webpackBootstrap /***/ function(module, exports, __webpack_require__) { var util = __webpack_require__(1); - var stack = __webpack_require__(10); + var stack = __webpack_require__(9); var ItemRange = __webpack_require__(25); /** @@ -10273,7 +10272,7 @@ return /******/ (function(modules) { // webpackBootstrap /* 18 */ /***/ function(module, exports, __webpack_require__) { - var Hammer = __webpack_require__(49); + var Hammer = __webpack_require__(50); var util = __webpack_require__(1); var DataSet = __webpack_require__(3); var DataView = __webpack_require__(4); @@ -12879,7 +12878,7 @@ return /******/ (function(modules) { // webpackBootstrap var util = __webpack_require__(1); var Component = __webpack_require__(12); - var TimeStep = __webpack_require__(11); + var TimeStep = __webpack_require__(10); /** * A horizontal time axis @@ -13278,7 +13277,7 @@ return /******/ (function(modules) { // webpackBootstrap /* 22 */ /***/ function(module, exports, __webpack_require__) { - var Hammer = __webpack_require__(49); + var Hammer = __webpack_require__(50); /** * @constructor Item @@ -13879,7 +13878,7 @@ return /******/ (function(modules) { // webpackBootstrap /* 25 */ /***/ function(module, exports, __webpack_require__) { - var Hammer = __webpack_require__(49); + var Hammer = __webpack_require__(50); var Item = __webpack_require__(22); /** @@ -14177,8 +14176,8 @@ return /******/ (function(modules) { // webpackBootstrap /***/ function(module, exports, __webpack_require__) { var Emitter = __webpack_require__(41); - var Hammer = __webpack_require__(49); - var mousetrap = __webpack_require__(42); + var Hammer = __webpack_require__(50); + var mousetrap = __webpack_require__(48); var util = __webpack_require__(1); var DataSet = __webpack_require__(3); var DataView = __webpack_require__(4); @@ -14234,9 +14233,9 @@ return /******/ (function(modules) { // webpackBootstrap // set constant values this.constants = { nodes: { - radiusMin: 5, - radiusMax: 20, - radius: 5, + radiusMin: 10, + radiusMax: 30, + radius: 10, shape: 'ellipse', image: undefined, widthMin: 16, // px @@ -14284,7 +14283,8 @@ return /******/ (function(modules) { // webpackBootstrap length: 10, gap: 5, altLength: undefined - } + }, + inheritColor: false // to, from, false, true (== from) }, configurePhysics:false, physics: { @@ -14356,7 +14356,13 @@ return /******/ (function(modules) { // webpackBootstrap direction: "UD" // UD, DU, LR, RL }, freezeForStabilization: false, - smoothCurves: true, + smoothCurves: { + enabled: true, + dynamic: true, + type: "continuous", + roundness: 0.5 + }, + dynamicSmoothCurves: true, maxVelocity: 10, minVelocity: 0.1, // px/s stabilizationIterations: 1000, // maximum number of iteration to stabilize @@ -14391,10 +14397,12 @@ return /******/ (function(modules) { // webpackBootstrap dragNetwork: true, dragNodes: true, zoomable: true, - hover: false + hover: false, + hideEdgesOnDrag: false, + hideNodesOnDrag: false }; this.hoverObj = {nodes:{},edges:{}}; - + this.controlNodesActive = false; // Node variables var network = this; @@ -14727,7 +14735,6 @@ return /******/ (function(modules) { // webpackBootstrap if (options.height !== undefined) {this.height = options.height;} if (options.stabilize !== undefined) {this.stabilize = options.stabilize;} if (options.selectable !== undefined) {this.selectable = options.selectable;} - if (options.smoothCurves !== undefined) {this.constants.smoothCurves = options.smoothCurves;} if (options.freezeForStabilization !== undefined) {this.constants.freezeForStabilization = options.freezeForStabilization;} if (options.configurePhysics !== undefined){this.constants.configurePhysics = options.configurePhysics;} if (options.stabilizationIterations !== undefined) {this.constants.stabilizationIterations = options.stabilizationIterations;} @@ -14735,6 +14742,8 @@ return /******/ (function(modules) { // webpackBootstrap if (options.dragNodes !== undefined) {this.constants.dragNodes = options.dragNodes;} if (options.zoomable !== undefined) {this.constants.zoomable = options.zoomable;} if (options.hover !== undefined) {this.constants.hover = options.hover;} + if (options.hideEdgesOnDrag !== undefined) {this.constants.hideEdgesOnDrag = options.hideEdgesOnDrag;} + if (options.hideNodesOnDrag !== undefined) {this.constants.hideNodesOnDrag = options.hideNodesOnDrag;} // TODO: deprecated since version 3.0.0. Cleanup some day if (options.dragGraph !== undefined) { @@ -14800,6 +14809,20 @@ return /******/ (function(modules) { // webpackBootstrap } } + if (options.smoothCurves !== undefined) { + if (typeof options.smoothCurves == 'boolean') { + this.constants.smoothCurves.enabled = options.smoothCurves; + } + else { + this.constants.smoothCurves.enabled = true; + for (prop in options.smoothCurves) { + if (options.smoothCurves.hasOwnProperty(prop)) { + this.constants.smoothCurves[prop] = options.smoothCurves[prop]; + } + } + } + } + if (options.hierarchicalLayout) { this.constants.hierarchicalLayout.enabled = true; for (prop in options.hierarchicalLayout) { @@ -14871,7 +14894,6 @@ return /******/ (function(modules) { // webpackBootstrap } } - if (options.edges.color !== undefined) { if (util.isString(options.edges.color)) { this.constants.edges.color = {}; @@ -15201,10 +15223,11 @@ return /******/ (function(modules) { // webpackBootstrap this._setTranslation( this.drag.translation.x + diffX, - this.drag.translation.y + diffY); + this.drag.translation.y + diffY + ); this._redraw(); - this.moving = true; - this.start(); + // this.moving = true; + // this.start(); } } }; @@ -15223,6 +15246,7 @@ return /******/ (function(modules) { // webpackBootstrap s.node.yFixed = s.yFixed; }); } + this._redraw(); }; /** @@ -15918,10 +15942,19 @@ return /******/ (function(modules) { // webpackBootstrap "y": this._YconvertDOMtoCanvas(this.frame.canvas.clientHeight) }; + this._doInAllSectors("_drawAllSectorNodes",ctx); - this._doInAllSectors("_drawEdges",ctx); - this._doInAllSectors("_drawNodes",ctx,false); - this._doInAllSectors("_drawControlNodes",ctx); + if (this.drag.dragging == false || this.drag.dragging === undefined || this.constants.hideEdgesOnDrag == false) { + this._doInAllSectors("_drawEdges",ctx); + } + + if (this.drag.dragging == false || this.drag.dragging === undefined || this.constants.hideNodesOnDrag == false) { + this._doInAllSectors("_drawNodes",ctx,false); + } + + if (this.controlNodesActive == true) { + this._doInAllSectors("_drawControlNodes",ctx); + } // this._doInSupportSector("_drawNodes",ctx,true); // this._drawTree(ctx,"#F00F0F"); @@ -16233,7 +16266,7 @@ return /******/ (function(modules) { // webpackBootstrap this.moving = true; } else { - this.moving = this._isMoving(vminCorrected); + this.moving = this._isMoving(vminCorrected) || this.constants.configurePhysics; } } }; @@ -16282,10 +16315,12 @@ return /******/ (function(modules) { // webpackBootstrap timeRequired = Date.now() - calculationTime; maxSteps++; } + // start the rendering process var renderTime = Date.now(); this._redraw(); this.renderTime = Date.now() - renderTime; + }; if (typeof window !== 'undefined') { @@ -16369,8 +16404,7 @@ return /******/ (function(modules) { // webpackBootstrap if (disableStart === undefined) { disableStart = true; } - - if (this.constants.smoothCurves == true) { + if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { this._createBezierNodes(); } else { @@ -16398,7 +16432,7 @@ return /******/ (function(modules) { // webpackBootstrap * @private */ Network.prototype._createBezierNodes = function() { - if (this.constants.smoothCurves == true) { + if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { for (var edgeId in this.edges) { if (this.edges.hasOwnProperty(edgeId)) { var edge = this.edges[edgeId]; @@ -16534,8 +16568,10 @@ return /******/ (function(modules) { // webpackBootstrap this.customLength = false; this.selected = false; this.hover = false; - this.smooth = constants.smoothCurves; + this.smoothCurves = constants.smoothCurves; + this.dynamicSmoothCurves = constants.dynamicSmoothCurves; this.arrowScaleFactor = constants.edges.arrowScaleFactor; + this.inheritColor = constants.edges.inheritColor; this.from = null; // a node this.to = null; // a node @@ -16607,6 +16643,8 @@ return /******/ (function(modules) { // webpackBootstrap // scale the arrow if (properties.arrowScaleFactor !== undefined) {this.arrowScaleFactor = properties.arrowScaleFactor;} + if (properties.inheritColor !== undefined) {this.inheritColor = properties.inheritColor;} + // Added to support dashed lines // David Jordan // 2012-08-08 @@ -16751,6 +16789,28 @@ return /******/ (function(modules) { // webpackBootstrap } }; + Edge.prototype._getColor = function() { + var colorObj = this.color; + if (this.inheritColor == "to") { + colorObj = { + highlight: this.to.color.highlight.border, + hover: this.to.color.hover.border, + color: this.to.color.border + }; + } + else if (this.inheritColor == "from" || this.inheritColor == true) { + colorObj = { + highlight: this.from.color.highlight.border, + hover: this.from.color.hover.border, + color: this.from.color.border + }; + } + + if (this.selected == true) {return colorObj.highlight;} + else if (this.hover == true) {return colorObj.hover;} + else {return colorObj.color;} + } + /** * Redraw a edge as a line @@ -16761,21 +16821,19 @@ return /******/ (function(modules) { // webpackBootstrap */ Edge.prototype._drawLine = function(ctx) { // set style - if (this.selected == true) {ctx.strokeStyle = this.color.highlight;} - else if (this.hover == true) {ctx.strokeStyle = this.color.hover;} - else {ctx.strokeStyle = this.color.color;} - ctx.lineWidth = this._getLineWidth(); + ctx.strokeStyle = this._getColor(); + ctx.lineWidth = this._getLineWidth(); if (this.from != this.to) { // draw line - this._line(ctx); + var via = this._line(ctx); // draw label var point; if (this.label) { - if (this.smooth == true) { - var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x)); - var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y)); + if (this.smoothCurves.enabled == true && via != null) { + var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x)); + var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y)); point = {x:midpointX, y:midpointY}; } else { @@ -16825,6 +16883,154 @@ return /******/ (function(modules) { // webpackBootstrap } }; + Edge.prototype._getViaCoordinates = function () { + var xVia = null; + var yVia = null; + var factor = this.smoothCurves.roundness; + var type = this.smoothCurves.type; + if (factor == 0) { + return {x:null,y:null}; + } + + var dx = Math.abs(this.from.x - this.to.x); + var dy = Math.abs(this.from.y - this.to.y); + if (type == 'discrete' || type == 'diagonalCross') { + if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { + if (this.from.y > this.to.y) { + if (this.from.x < this.to.x) { + xVia = this.from.x + factor * dy; + yVia = this.from.y - factor * dy; + } + else if (this.from.x > this.to.x) { + xVia = this.from.x - factor * dy; + yVia = this.from.y - factor * dy; + } + } + else if (this.from.y < this.to.y) { + if (this.from.x < this.to.x) { + xVia = this.from.x + factor * dy; + yVia = this.from.y + factor * dy; + } + else if (this.from.x > this.to.x) { + xVia = this.from.x - factor * dy; + yVia = this.from.y + factor * dy; + } + } + if (type == "discrete") { + xVia = dx < factor * dy ? this.from.x : xVia; + } + } + else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { + if (this.from.y > this.to.y) { + if (this.from.x < this.to.x) { + xVia = this.from.x + factor * dx; + yVia = this.from.y - factor * dx; + } + else if (this.from.x > this.to.x) { + xVia = this.from.x - factor * dx; + yVia = this.from.y - factor * dx; + } + } + else if (this.from.y < this.to.y) { + if (this.from.x < this.to.x) { + xVia = this.from.x + factor * dx; + yVia = this.from.y + factor * dx; + } + else if (this.from.x > this.to.x) { + xVia = this.from.x - factor * dx; + yVia = this.from.y + factor * dx; + } + } + if (type == "discrete") { + yVia = dy < factor * dx ? this.from.y : yVia; + } + } + } + else if (type == "straightCross") { + if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { + xVia = this.from.x; + yVia = this.to.y + } + else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { + xVia = this.to.x; + yVia = this.from.y; + } + } + else if (type == 'horizontal') { + xVia = this.to.x; + yVia = this.from.y; + } + else if (type == 'vertical') { + xVia = this.from.x; + yVia = this.to.y; + } + else { // continuous + if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { + if (this.from.y > this.to.y) { + if (this.from.x < this.to.x) { + // console.log(1) + xVia = this.from.x + factor * dy; + yVia = this.from.y - factor * dy; + xVia = this.to.x < xVia ? this.to.x : xVia; + } + else if (this.from.x > this.to.x) { + // console.log(2) + xVia = this.from.x - factor * dy; + yVia = this.from.y - factor * dy; + xVia = this.to.x > xVia ? this.to.x :xVia; + } + } + else if (this.from.y < this.to.y) { + if (this.from.x < this.to.x) { + // console.log(3) + xVia = this.from.x + factor * dy; + yVia = this.from.y + factor * dy; + xVia = this.to.x < xVia ? this.to.x : xVia; + } + else if (this.from.x > this.to.x) { + // console.log(4, this.from.x, this.to.x) + xVia = this.from.x - factor * dy; + yVia = this.from.y + factor * dy; + xVia = this.to.x > xVia ? this.to.x : xVia; + } + } + } + else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { + if (this.from.y > this.to.y) { + if (this.from.x < this.to.x) { + // console.log(5) + xVia = this.from.x + factor * dx; + yVia = this.from.y - factor * dx; + yVia = this.to.y > yVia ? this.to.y : yVia; + } + else if (this.from.x > this.to.x) { + // console.log(6) + xVia = this.from.x - factor * dx; + yVia = this.from.y - factor * dx; + yVia = this.to.y > yVia ? this.to.y : yVia; + } + } + else if (this.from.y < this.to.y) { + if (this.from.x < this.to.x) { + // console.log(7) + xVia = this.from.x + factor * dx; + yVia = this.from.y + factor * dx; + yVia = this.to.y < yVia ? this.to.y : yVia; + } + else if (this.from.x > this.to.x) { + // console.log(8) + xVia = this.from.x - factor * dx; + yVia = this.from.y + factor * dx; + yVia = this.to.y < yVia ? this.to.y : yVia; + } + } + } + } + + + return {x:xVia, y:yVia}; + } + /** * Draw a line between two nodes * @param {CanvasRenderingContext2D} ctx @@ -16834,13 +17040,33 @@ return /******/ (function(modules) { // webpackBootstrap // draw a straight line ctx.beginPath(); ctx.moveTo(this.from.x, this.from.y); - if (this.smooth == true) { + if (this.smoothCurves.enabled == true) { + if (this.smoothCurves.dynamic == false) { + var via = this._getViaCoordinates(); + if (via.x == null) { + ctx.lineTo(this.to.x, this.to.y); + ctx.stroke(); + return null; + } + else { + // this.via.x = via.x; + // this.via.y = via.y; + ctx.quadraticCurveTo(via.x,via.y,this.to.x, this.to.y); + ctx.stroke(); + return via; + } + } + else { ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y); + ctx.stroke(); + return this.via; + } } else { ctx.lineTo(this.to.x, this.to.y); + ctx.stroke(); + return null; } - ctx.stroke(); }; /** @@ -16904,11 +17130,9 @@ return /******/ (function(modules) { // webpackBootstrap ctx.lineWidth = this._getLineWidth(); + var via = null; // only firefox and chrome support this method, else we use the legacy one. if (ctx.mozDash !== undefined || ctx.setLineDash !== undefined) { - ctx.beginPath(); - ctx.moveTo(this.from.x, this.from.y); - // configure the dash pattern var pattern = [0]; if (this.dash.length !== undefined && this.dash.gap !== undefined) { @@ -16929,13 +17153,7 @@ return /******/ (function(modules) { // webpackBootstrap } // draw the line - if (this.smooth == true) { - ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y); - } - else { - ctx.lineTo(this.to.x, this.to.y); - } - ctx.stroke(); + via = this._line(ctx); // restore the dash settings. if (typeof ctx.setLineDash !== 'undefined') { //Chrome @@ -16972,9 +17190,9 @@ return /******/ (function(modules) { // webpackBootstrap // draw label if (this.label) { var point; - if (this.smooth == true) { - var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x)); - var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y)); + if (this.smoothCurves.enabled == true && via != null) { + var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x)); + var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y)); point = {x:midpointX, y:midpointY}; } else { @@ -17031,14 +17249,14 @@ return /******/ (function(modules) { // webpackBootstrap if (this.from != this.to) { // draw line - this._line(ctx); + var via = this._line(ctx); var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x)); var length = (10 + 5 * this.width) * this.arrowScaleFactor; // draw an arrow halfway the line - if (this.smooth == true) { - var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x)); - var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y)); + if (this.smoothCurves.enabled == true && via != null) { + var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x)); + var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y)); point = {x:midpointX, y:midpointY}; } else { @@ -17118,20 +17336,27 @@ return /******/ (function(modules) { // webpackBootstrap var xFrom = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x; var yFrom = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y; + var via; + if (this.smoothCurves.dynamic == true && this.smoothCurves.enabled == true ) { + via = this.via; + } + else if (this.smoothCurves.enabled == true) { + via = this._getViaCoordinates(); + } - if (this.smooth == true) { - angle = Math.atan2((this.to.y - this.via.y), (this.to.x - this.via.x)); - dx = (this.to.x - this.via.x); - dy = (this.to.y - this.via.y); + if (this.smoothCurves.enabled == true && via.x != null) { + angle = Math.atan2((this.to.y - via.y), (this.to.x - via.x)); + dx = (this.to.x - via.x); + dy = (this.to.y - via.y); edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); } var toBorderDist = this.to.distanceToBorder(ctx, angle); var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength; var xTo,yTo; - if (this.smooth == true) { - xTo = (1 - toBorderPoint) * this.via.x + toBorderPoint * this.to.x; - yTo = (1 - toBorderPoint) * this.via.y + toBorderPoint * this.to.y; + if (this.smoothCurves.enabled == true && via.x != null) { + xTo = (1 - toBorderPoint) * via.x + toBorderPoint * this.to.x; + yTo = (1 - toBorderPoint) * via.y + toBorderPoint * this.to.y; } else { xTo = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x; @@ -17140,8 +17365,8 @@ return /******/ (function(modules) { // webpackBootstrap ctx.beginPath(); ctx.moveTo(xFrom,yFrom); - if (this.smooth == true) { - ctx.quadraticCurveTo(this.via.x,this.via.y,xTo, yTo); + if (this.smoothCurves.enabled == true && via.x != null) { + ctx.quadraticCurveTo(via.x,via.y,xTo, yTo); } else { ctx.lineTo(xTo, yTo); @@ -17157,9 +17382,9 @@ return /******/ (function(modules) { // webpackBootstrap // draw label if (this.label) { var point; - if (this.smooth == true) { - var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x)); - var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y)); + if (this.smoothCurves.enabled == true && via != null) { + var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x)); + var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y)); point = {x:midpointX, y:midpointY}; } else { @@ -17229,13 +17454,23 @@ return /******/ (function(modules) { // webpackBootstrap */ Edge.prototype._getDistanceToEdge = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point if (this.from != this.to) { - if (this.smooth == true) { + if (this.smoothCurves.enabled == true) { + var xVia, yVia; + if (this.smoothCurves.enabled == true && this.smoothCurves.dynamic == true) { + xVia = this.via.x; + yVia = this.via.y; + } + else { + var via = this._getViaCoordinates(); + xVia = via.x; + yVia = via.y; + } var minDistance = 1e9; var i,t,x,y,dx,dy; for (i = 0; i < 10; i++) { t = 0.1*i; - x = Math.pow(1-t,2)*x1 + (2*t*(1 - t))*this.via.x + Math.pow(t,2)*x2; - y = Math.pow(1-t,2)*y1 + (2*t*(1 - t))*this.via.y + Math.pow(t,2)*y2; + x = Math.pow(1-t,2)*x1 + (2*t*(1 - t))*xVia + Math.pow(t,2)*x2; + y = Math.pow(1-t,2)*y1 + (2*t*(1 - t))*yVia + Math.pow(t,2)*y2; dx = Math.abs(x3-x); dy = Math.abs(y3-y); minDistance = Math.min(minDistance,Math.sqrt(dx*dx + dy*dy)); @@ -17436,20 +17671,27 @@ return /******/ (function(modules) { // webpackBootstrap var xFrom = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x; var yFrom = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y; + var via; + if (this.smoothCurves.dynamic == true && this.smoothCurves.enabled == true) { + via = this.via; + } + else if (this.smoothCurves.enabled == true) { + via = this._getViaCoordinates(); + } - if (this.smooth == true) { - angle = Math.atan2((this.to.y - this.via.y), (this.to.x - this.via.x)); - dx = (this.to.x - this.via.x); - dy = (this.to.y - this.via.y); + if (this.smoothCurves.enabled == true && via.x != null) { + angle = Math.atan2((this.to.y - via.y), (this.to.x - via.x)); + dx = (this.to.x - via.x); + dy = (this.to.y - via.y); edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); } var toBorderDist = this.to.distanceToBorder(ctx, angle); var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength; var xTo,yTo; - if (this.smooth == true) { - xTo = (1 - toBorderPoint) * this.via.x + toBorderPoint * this.to.x; - yTo = (1 - toBorderPoint) * this.via.y + toBorderPoint * this.to.y; + if (this.smoothCurves.enabled == true && via.x != null) { + xTo = (1 - toBorderPoint) * via.x + toBorderPoint * this.to.x; + yTo = (1 - toBorderPoint) * via.y + toBorderPoint * this.to.y; } else { xTo = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x; @@ -17481,16 +17723,16 @@ return /******/ (function(modules) { // webpackBootstrap * default constants for group colors */ Groups.DEFAULT = [ - {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue - {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}}, // yellow - {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}}, // red - {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}}, // green - {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}}, // magenta - {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}}, // purple - {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}}, // orange - {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue - {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}}, // pink - {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}} // mint + {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}, hover: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue + {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}, hover: {border: "#FFA500", background: "#FFFFA3"}}, // yellow + {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}, hover: {border: "#FA0A10", background: "#FFAFB1"}}, // red + {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}, hover: {border: "#41A906", background: "#A1EC76"}}, // green + {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}, hover: {border: "#E129F0", background: "#F0B3F5"}}, // magenta + {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}, hover: {border: "#7C29F0", background: "#D3BDF0"}}, // purple + {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}, hover: {border: "#C37F00", background: "#FFCA66"}}, // orange + {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}, hover: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue + {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}, hover: {border: "#FD5A77", background: "#FFD1D9"}}, // pink + {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}, hover: {border: "#4AD63A", background: "#E6FFE3"}} // mint ]; @@ -20266,7 +20508,7 @@ return /******/ (function(modules) { // webpackBootstrap // Only load hammer.js when in a browser environment // (loading hammer.js in a node.js environment gives errors) if (typeof window !== 'undefined') { - module.exports = window['Hammer'] || __webpack_require__(49); + module.exports = window['Hammer'] || __webpack_require__(50); // TODO: throw an error when hammerjs is not available? } else { @@ -20289,13 +20531,13 @@ return /******/ (function(modules) { // webpackBootstrap /* 40 */ /***/ function(module, exports, __webpack_require__) { - var PhysicsMixin = __webpack_require__(50); - var ClusterMixin = __webpack_require__(43); - var SectorsMixin = __webpack_require__(44); - var SelectionMixin = __webpack_require__(45); - var ManipulationMixin = __webpack_require__(46); - var NavigationMixin = __webpack_require__(47); - var HierarchicalLayoutMixin = __webpack_require__(48); + var PhysicsMixin = __webpack_require__(49); + var ClusterMixin = __webpack_require__(42); + var SectorsMixin = __webpack_require__(43); + var SelectionMixin = __webpack_require__(44); + var ManipulationMixin = __webpack_require__(45); + var NavigationMixin = __webpack_require__(46); + var HierarchicalLayoutMixin = __webpack_require__(47); /** * Load a mixin into the network object @@ -20664,1946 +20906,1695 @@ return /******/ (function(modules) { // webpackBootstrap /***/ function(module, exports, __webpack_require__) { /** - * Copyright 2012 Craig Campbell - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * Mousetrap is a simple keyboard shortcut library for Javascript with - * no external dependencies + * Creation of the ClusterMixin var. * - * @version 1.1.2 - * @url craig.is/killing/mice + * This contains all the functions the Network object can use to employ clustering */ - /** - * mapping of special keycodes to their corresponding keys - * - * everything in this dictionary cannot use keypress events - * so it has to be here to map to the correct keycodes for - * keyup/keydown events - * - * @type {Object} - */ - var _MAP = { - 8: 'backspace', - 9: 'tab', - 13: 'enter', - 16: 'shift', - 17: 'ctrl', - 18: 'alt', - 20: 'capslock', - 27: 'esc', - 32: 'space', - 33: 'pageup', - 34: 'pagedown', - 35: 'end', - 36: 'home', - 37: 'left', - 38: 'up', - 39: 'right', - 40: 'down', - 45: 'ins', - 46: 'del', - 91: 'meta', - 93: 'meta', - 224: 'meta' - }, - - /** - * mapping for special characters so they can support - * - * this dictionary is only used incase you want to bind a - * keyup or keydown event to one of these keys - * - * @type {Object} - */ - _KEYCODE_MAP = { - 106: '*', - 107: '+', - 109: '-', - 110: '.', - 111 : '/', - 186: ';', - 187: '=', - 188: ',', - 189: '-', - 190: '.', - 191: '/', - 192: '`', - 219: '[', - 220: '\\', - 221: ']', - 222: '\'' - }, + /** + * This is only called in the constructor of the network object + * + */ + exports.startWithClustering = function() { + // cluster if the data set is big + this.clusterToFit(this.constants.clustering.initialMaxNodes, true); - /** - * this is a mapping of keys that require shift on a US keypad - * back to the non shift equivelents - * - * this is so you can use keyup events with these keys - * - * note that this will only work reliably on US keyboards - * - * @type {Object} - */ - _SHIFT_MAP = { - '~': '`', - '!': '1', - '@': '2', - '#': '3', - '$': '4', - '%': '5', - '^': '6', - '&': '7', - '*': '8', - '(': '9', - ')': '0', - '_': '-', - '+': '=', - ':': ';', - '\"': '\'', - '<': ',', - '>': '.', - '?': '/', - '|': '\\' - }, + // updates the lables after clustering + this.updateLabels(); - /** - * this is a list of special strings you can use to map - * to modifier keys when you specify your keyboard shortcuts - * - * @type {Object} - */ - _SPECIAL_ALIASES = { - 'option': 'alt', - 'command': 'meta', - 'return': 'enter', - 'escape': 'esc' - }, + // this is called here because if clusterin is disabled, the start and stabilize are called in + // the setData function. + if (this.stabilize) { + this._stabilize(); + } + this.start(); + }; - /** - * variable to store the flipped version of _MAP from above - * needed to check if we should use keypress or not when no action - * is specified - * - * @type {Object|undefined} - */ - _REVERSE_MAP, + /** + * This function clusters until the initialMaxNodes has been reached + * + * @param {Number} maxNumberOfNodes + * @param {Boolean} reposition + */ + exports.clusterToFit = function(maxNumberOfNodes, reposition) { + var numberOfNodes = this.nodeIndices.length; - /** - * a list of all the callbacks setup via Mousetrap.bind() - * - * @type {Object} - */ - _callbacks = {}, + var maxLevels = 50; + var level = 0; - /** - * direct map of string combinations to callbacks used for trigger() - * - * @type {Object} - */ - _direct_map = {}, + // we first cluster the hubs, then we pull in the outliers, repeat + while (numberOfNodes > maxNumberOfNodes && level < maxLevels) { + if (level % 3 == 0) { + this.forceAggregateHubs(true); + this.normalizeClusterLevels(); + } + else { + this.increaseClusterLevel(); // this also includes a cluster normalization + } - /** - * keeps track of what level each sequence is at since multiple - * sequences can start out with the same sequence - * - * @type {Object} - */ - _sequence_levels = {}, + numberOfNodes = this.nodeIndices.length; + level += 1; + } - /** - * variable to store the setTimeout call - * - * @type {null|number} - */ - _reset_timer, + // after the clustering we reposition the nodes to reduce the initial chaos + if (level > 0 && reposition == true) { + this.repositionNodes(); + } + this._updateCalculationNodes(); + }; - /** - * temporary state where we will ignore the next keyup - * - * @type {boolean|string} - */ - _ignore_next_keyup = false, + /** + * This function can be called to open up a specific cluster. It is only called by + * It will unpack the cluster back one level. + * + * @param node | Node object: cluster to open. + */ + exports.openCluster = function(node) { + var isMovingBeforeClustering = this.moving; + if (node.clusterSize > this.constants.clustering.sectorThreshold && this._nodeInActiveArea(node) && + !(this._sector() == "default" && this.nodeIndices.length == 1)) { + // this loads a new sector, loads the nodes and edges and nodeIndices of it. + this._addSector(node); + var level = 0; - /** - * are we currently inside of a sequence? - * type of action ("keyup" or "keydown" or "keypress") or false - * - * @type {boolean|string} - */ - _inside_sequence = false; + // we decluster until we reach a decent number of nodes + while ((this.nodeIndices.length < this.constants.clustering.initialMaxNodes) && (level < 10)) { + this.decreaseClusterLevel(); + level += 1; + } - /** - * loop through the f keys, f1 to f19 and add them to the map - * programatically - */ - for (var i = 1; i < 20; ++i) { - _MAP[111 + i] = 'f' + i; } + else { + this._expandClusterNode(node,false,true); - /** - * loop through to map numbers on the numeric keypad - */ - for (i = 0; i <= 9; ++i) { - _MAP[i + 96] = i; + // update the index list, dynamic edges and labels + this._updateNodeIndexList(); + this._updateDynamicEdges(); + this._updateCalculationNodes(); + this.updateLabels(); } - /** - * cross browser add event method - * - * @param {Element|HTMLDocument} object - * @param {string} type - * @param {Function} callback - * @returns void - */ - function _addEvent(object, type, callback) { - if (object.addEventListener) { - return object.addEventListener(type, callback, false); - } + // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded + if (this.moving != isMovingBeforeClustering) { + this.start(); + } + }; - object.attachEvent('on' + type, callback); + + /** + * This calls the updateClustes with default arguments + */ + exports.updateClustersDefault = function() { + if (this.constants.clustering.enabled == true) { + this.updateClusters(0,false,false); } + }; - /** - * takes the event and returns the key character - * - * @param {Event} e - * @return {string} - */ - function _characterFromEvent(e) { - // for keypress events we should return the character as is - if (e.type == 'keypress') { - return String.fromCharCode(e.which); - } + /** + * This function can be called to increase the cluster level. This means that the nodes with only one edge connection will + * be clustered with their connected node. This can be repeated as many times as needed. + * This can be called externally (by a keybind for instance) to reduce the complexity of big datasets. + */ + exports.increaseClusterLevel = function() { + this.updateClusters(-1,false,true); + }; - // for non keypress events the special maps are needed - if (_MAP[e.which]) { - return _MAP[e.which]; - } - if (_KEYCODE_MAP[e.which]) { - return _KEYCODE_MAP[e.which]; - } + /** + * This function can be called to decrease the cluster level. This means that the nodes with only one edge connection will + * be unpacked if they are a cluster. This can be repeated as many times as needed. + * This can be called externally (by a key-bind for instance) to look into clusters without zooming. + */ + exports.decreaseClusterLevel = function() { + this.updateClusters(1,false,true); + }; - // if it is not in the special map - return String.fromCharCode(e.which).toLowerCase(); - } - /** - * should we stop this event before firing off callbacks - * - * @param {Event} e - * @return {boolean} - */ - function _stop(e) { - var element = e.target || e.srcElement, - tag_name = element.tagName; + /** + * This is the main clustering function. It clusters and declusters on zoom or forced + * This function clusters on zoom, it can be called with a predefined zoom direction + * If out, check if we can form clusters, if in, check if we can open clusters. + * This function is only called from _zoom() + * + * @param {Number} zoomDirection | -1 / 0 / +1 for zoomOut / determineByZoom / zoomIn + * @param {Boolean} recursive | enabled or disable recursive calling of the opening of clusters + * @param {Boolean} force | enabled or disable forcing + * @param {Boolean} doNotStart | if true do not call start + * + */ + exports.updateClusters = function(zoomDirection,recursive,force,doNotStart) { + var isMovingBeforeClustering = this.moving; + var amountOfNodes = this.nodeIndices.length; - // if the element has the class "mousetrap" then no need to stop - if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) { - return false; - } + // on zoom out collapse the sector if the scale is at the level the sector was made + if (this.previousScale > this.scale && zoomDirection == 0) { + this._collapseSector(); + } - // stop for input, select, and textarea - return tag_name == 'INPUT' || tag_name == 'SELECT' || tag_name == 'TEXTAREA' || (element.contentEditable && element.contentEditable == 'true'); + // check if we zoom in or out + if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out + // forming clusters when forced pulls outliers in. When not forced, the edge length of the + // outer nodes determines if it is being clustered + this._formClusters(force); + } + else if (this.previousScale < this.scale || zoomDirection == 1) { // zoom in + if (force == true) { + // _openClusters checks for each node if the formationScale of the cluster is smaller than + // the current scale and if so, declusters. When forced, all clusters are reduced by one step + this._openClusters(recursive,force); + } + else { + // if a cluster takes up a set percentage of the active window + this._openClustersBySize(); + } } + this._updateNodeIndexList(); - /** - * checks if two arrays are equal - * - * @param {Array} modifiers1 - * @param {Array} modifiers2 - * @returns {boolean} - */ - function _modifiersMatch(modifiers1, modifiers2) { - return modifiers1.sort().join(',') === modifiers2.sort().join(','); + // if a cluster was NOT formed and the user zoomed out, we try clustering by hubs + if (this.nodeIndices.length == amountOfNodes && (this.previousScale > this.scale || zoomDirection == -1)) { + this._aggregateHubs(force); + this._updateNodeIndexList(); } - /** - * resets all sequence counters except for the ones passed in - * - * @param {Object} do_not_reset - * @returns void - */ - function _resetSequences(do_not_reset) { - do_not_reset = do_not_reset || {}; + // we now reduce chains. + if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out + this.handleChains(); + this._updateNodeIndexList(); + } - var active_sequences = false, - key; + this.previousScale = this.scale; - for (key in _sequence_levels) { - if (do_not_reset[key]) { - active_sequences = true; - continue; - } - _sequence_levels[key] = 0; - } + // rest of the update the index list, dynamic edges and labels + this._updateDynamicEdges(); + this.updateLabels(); - if (!active_sequences) { - _inside_sequence = false; - } + // if a cluster was formed, we increase the clusterSession + if (this.nodeIndices.length < amountOfNodes) { // this means a clustering operation has taken place + this.clusterSession += 1; + // if clusters have been made, we normalize the cluster level + this.normalizeClusterLevels(); } - /** - * finds all callbacks that match based on the keycode, modifiers, - * and action - * - * @param {string} character - * @param {Array} modifiers - * @param {string} action - * @param {boolean=} remove - should we remove any matches - * @param {string=} combination - * @returns {Array} - */ - function _getMatches(character, modifiers, action, remove, combination) { - var i, - callback, - matches = []; + if (doNotStart == false || doNotStart === undefined) { + // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded + if (this.moving != isMovingBeforeClustering) { + this.start(); + } + } - // if there are no events related to this keycode - if (!_callbacks[character]) { - return []; - } + this._updateCalculationNodes(); + }; - // if a modifier key is coming up on its own we should allow it - if (action == 'keyup' && _isModifier(character)) { - modifiers = [character]; - } + /** + * This function handles the chains. It is called on every updateClusters(). + */ + exports.handleChains = function() { + // after clustering we check how many chains there are + var chainPercentage = this._getChainFraction(); + if (chainPercentage > this.constants.clustering.chainThreshold) { + this._reduceAmountOfChains(1 - this.constants.clustering.chainThreshold / chainPercentage) - // loop through all callbacks for the key that was pressed - // and see if any of them match - for (i = 0; i < _callbacks[character].length; ++i) { - callback = _callbacks[character][i]; + } + }; - // if this is a sequence but it is not at the right level - // then move onto the next match - if (callback.seq && _sequence_levels[callback.seq] != callback.level) { - continue; - } + /** + * this functions starts clustering by hubs + * The minimum hub threshold is set globally + * + * @private + */ + exports._aggregateHubs = function(force) { + this._getHubSize(); + this._formClustersByHub(force,false); + }; - // if the action we are looking for doesn't match the action we got - // then we should keep going - if (action != callback.action) { - continue; - } - // if this is a keypress event that means that we need to only - // look at the character, otherwise check the modifiers as - // well - if (action == 'keypress' || _modifiersMatch(modifiers, callback.modifiers)) { + /** + * This function is fired by keypress. It forces hubs to form. + * + */ + exports.forceAggregateHubs = function(doNotStart) { + var isMovingBeforeClustering = this.moving; + var amountOfNodes = this.nodeIndices.length; - // remove is used so if you change your mind and call bind a - // second time with a new function the first one is overwritten - if (remove && callback.combo == combination) { - _callbacks[character].splice(i, 1); - } + this._aggregateHubs(true); - matches.push(callback); - } - } + // update the index list, dynamic edges and labels + this._updateNodeIndexList(); + this._updateDynamicEdges(); + this.updateLabels(); - return matches; + // if a cluster was formed, we increase the clusterSession + if (this.nodeIndices.length != amountOfNodes) { + this.clusterSession += 1; } - /** - * takes a key event and figures out what the modifiers are - * - * @param {Event} e - * @returns {Array} - */ - function _eventModifiers(e) { - var modifiers = []; + if (doNotStart == false || doNotStart === undefined) { + // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded + if (this.moving != isMovingBeforeClustering) { + this.start(); + } + } + }; - if (e.shiftKey) { - modifiers.push('shift'); + /** + * If a cluster takes up more than a set percentage of the screen, open the cluster + * + * @private + */ + exports._openClustersBySize = function() { + for (var nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + var node = this.nodes[nodeId]; + if (node.inView() == true) { + if ((node.width*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) || + (node.height*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) { + this.openCluster(node); + } } + } + } + }; - if (e.altKey) { - modifiers.push('alt'); - } - if (e.ctrlKey) { - modifiers.push('ctrl'); - } + /** + * This function loops over all nodes in the nodeIndices list. For each node it checks if it is a cluster and if it + * has to be opened based on the current zoom level. + * + * @private + */ + exports._openClusters = function(recursive,force) { + for (var i = 0; i < this.nodeIndices.length; i++) { + var node = this.nodes[this.nodeIndices[i]]; + this._expandClusterNode(node,recursive,force); + this._updateCalculationNodes(); + } + }; - if (e.metaKey) { - modifiers.push('meta'); - } + /** + * This function checks if a node has to be opened. This is done by checking the zoom level. + * If the node contains child nodes, this function is recursively called on the child nodes as well. + * This recursive behaviour is optional and can be set by the recursive argument. + * + * @param {Node} parentNode | to check for cluster and expand + * @param {Boolean} recursive | enabled or disable recursive calling + * @param {Boolean} force | enabled or disable forcing + * @param {Boolean} [openAll] | This will recursively force all nodes in the parent to be released + * @private + */ + exports._expandClusterNode = function(parentNode, recursive, force, openAll) { + // first check if node is a cluster + if (parentNode.clusterSize > 1) { + // this means that on a double tap event or a zoom event, the cluster fully unpacks if it is smaller than 20 + if (parentNode.clusterSize < this.constants.clustering.sectorThreshold) { + openAll = true; + } + recursive = openAll ? true : recursive; - return modifiers; - } + // if the last child has been added on a smaller scale than current scale decluster + if (parentNode.formationScale < this.scale || force == true) { + // we will check if any of the contained child nodes should be removed from the cluster + for (var containedNodeId in parentNode.containedNodes) { + if (parentNode.containedNodes.hasOwnProperty(containedNodeId)) { + var childNode = parentNode.containedNodes[containedNodeId]; - /** - * actually calls the callback function - * - * if your callback function returns false this will use the jquery - * convention - prevent default and stop propogation on the event - * - * @param {Function} callback - * @param {Event} e - * @returns void - */ - function _fireCallback(callback, e) { - if (callback(e) === false) { - if (e.preventDefault) { - e.preventDefault(); + // force expand will expand the largest cluster size clusters. Since we cluster from outside in, we assume that + // the largest cluster is the one that comes from outside + if (force == true) { + if (childNode.clusterSession == parentNode.clusterSessions[parentNode.clusterSessions.length-1] + || openAll) { + this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll); + } } - - if (e.stopPropagation) { - e.stopPropagation(); + else { + if (this._nodeInActiveArea(parentNode)) { + this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll); + } } - - e.returnValue = false; - e.cancelBubble = true; + } } + } } + }; - /** - * handles a character key event - * - * @param {string} character - * @param {Event} e - * @returns void - */ - function _handleCharacter(character, e) { + /** + * ONLY CALLED FROM _expandClusterNode + * + * This function will expel a child_node from a parent_node. This is to de-cluster the node. This function will remove + * the child node from the parent contained_node object and put it back into the global nodes object. + * The same holds for the edge that was connected to the child node. It is moved back into the global edges object. + * + * @param {Node} parentNode | the parent node + * @param {String} containedNodeId | child_node id as it is contained in the containedNodes object of the parent node + * @param {Boolean} recursive | This will also check if the child needs to be expanded. + * With force and recursive both true, the entire cluster is unpacked + * @param {Boolean} force | This will disregard the zoom level and will expel this child from the parent + * @param {Boolean} openAll | This will recursively force all nodes in the parent to be released + * @private + */ + exports._expelChildFromParent = function(parentNode, containedNodeId, recursive, force, openAll) { + var childNode = parentNode.containedNodes[containedNodeId]; - // if this event should not happen stop here - if (_stop(e)) { - return; - } + // if child node has been added on smaller scale than current, kick out + if (childNode.formationScale < this.scale || force == true) { + // unselect all selected items + this._unselectAll(); - var callbacks = _getMatches(character, _eventModifiers(e), e.type), - i, - do_not_reset = {}, - processed_sequence_callback = false; + // put the child node back in the global nodes object + this.nodes[containedNodeId] = childNode; - // loop through matching callbacks for this key event - for (i = 0; i < callbacks.length; ++i) { + // release the contained edges from this childNode back into the global edges + this._releaseContainedEdges(parentNode,childNode); - // fire for all sequence callbacks - // this is because if for example you have multiple sequences - // bound such as "g i" and "g t" they both need to fire the - // callback for matching g cause otherwise you can only ever - // match the first one - if (callbacks[i].seq) { - processed_sequence_callback = true; + // reconnect rerouted edges to the childNode + this._connectEdgeBackToChild(parentNode,childNode); - // keep a list of which sequences were matches for later - do_not_reset[callbacks[i].seq] = 1; - _fireCallback(callbacks[i].callback, e); - continue; - } + // validate all edges in dynamicEdges + this._validateEdges(parentNode); - // if there were no sequence matches but we are still here - // that means this is a regular match so we should fire that - if (!processed_sequence_callback && !_inside_sequence) { - _fireCallback(callbacks[i].callback, e); - } - } + // undo the changes from the clustering operation on the parent node + parentNode.mass -= childNode.mass; + parentNode.clusterSize -= childNode.clusterSize; + parentNode.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize); + parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length; - // if you are inside of a sequence and the key you are pressing - // is not a modifier key then we should reset all sequences - // that were not matched by this key event - if (e.type == _inside_sequence && !_isModifier(character)) { - _resetSequences(do_not_reset); + // place the child node near the parent, not at the exact same location to avoid chaos in the system + childNode.x = parentNode.x + parentNode.growthIndicator * (0.5 - Math.random()); + childNode.y = parentNode.y + parentNode.growthIndicator * (0.5 - Math.random()); + + // remove node from the list + delete parentNode.containedNodes[containedNodeId]; + + // check if there are other childs with this clusterSession in the parent. + var othersPresent = false; + for (var childNodeId in parentNode.containedNodes) { + if (parentNode.containedNodes.hasOwnProperty(childNodeId)) { + if (parentNode.containedNodes[childNodeId].clusterSession == childNode.clusterSession) { + othersPresent = true; + break; + } } - } + } + // if there are no others, remove the cluster session from the list + if (othersPresent == false) { + parentNode.clusterSessions.pop(); + } - /** - * handles a keydown event - * - * @param {Event} e - * @returns void - */ - function _handleKey(e) { + this._repositionBezierNodes(childNode); + // this._repositionBezierNodes(parentNode); - // normalize e.which for key events - // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion - e.which = typeof e.which == "number" ? e.which : e.keyCode; + // remove the clusterSession from the child node + childNode.clusterSession = 0; - var character = _characterFromEvent(e); + // recalculate the size of the node on the next time the node is rendered + parentNode.clearSizeCache(); - // no character found then stop - if (!character) { - return; - } + // restart the simulation to reorganise all nodes + this.moving = true; + } - if (e.type == 'keyup' && _ignore_next_keyup == character) { - _ignore_next_keyup = false; - return; - } + // check if a further expansion step is possible if recursivity is enabled + if (recursive == true) { + this._expandClusterNode(childNode,recursive,force,openAll); + } + }; - _handleCharacter(character, e); + + /** + * position the bezier nodes at the center of the edges + * + * @param node + * @private + */ + exports._repositionBezierNodes = function(node) { + for (var i = 0; i < node.dynamicEdges.length; i++) { + node.dynamicEdges[i].positionBezierNode(); } + }; - /** - * determines if the keycode specified is a modifier key or not - * - * @param {string} key - * @returns {boolean} - */ - function _isModifier(key) { - return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta'; + + /** + * This function checks if any nodes at the end of their trees have edges below a threshold length + * This function is called only from updateClusters() + * forceLevelCollapse ignores the length of the edge and collapses one level + * This means that a node with only one edge will be clustered with its connected node + * + * @private + * @param {Boolean} force + */ + exports._formClusters = function(force) { + if (force == false) { + this._formClustersByZoom(); + } + else { + this._forceClustersByZoom(); } + }; - /** - * called to set a 1 second timeout on the specified sequence - * - * this is so after each key press in the sequence you have 1 second - * to press the next key before you have to start over - * - * @returns void - */ - function _resetSequenceTimer() { - clearTimeout(_reset_timer); - _reset_timer = setTimeout(_resetSequences, 1000); + + /** + * This function handles the clustering by zooming out, this is based on a minimum edge distance + * + * @private + */ + exports._formClustersByZoom = function() { + var dx,dy,length, + minLength = this.constants.clustering.clusterEdgeThreshold/this.scale; + + // check if any edges are shorter than minLength and start the clustering + // the clustering favours the node with the larger mass + for (var edgeId in this.edges) { + if (this.edges.hasOwnProperty(edgeId)) { + var edge = this.edges[edgeId]; + if (edge.connected) { + if (edge.toId != edge.fromId) { + dx = (edge.to.x - edge.from.x); + dy = (edge.to.y - edge.from.y); + length = Math.sqrt(dx * dx + dy * dy); + + + if (length < minLength) { + // first check which node is larger + var parentNode = edge.from; + var childNode = edge.to; + if (edge.to.mass > edge.from.mass) { + parentNode = edge.to; + childNode = edge.from; + } + + if (childNode.dynamicEdgesLength == 1) { + this._addToCluster(parentNode,childNode,false); + } + else if (parentNode.dynamicEdgesLength == 1) { + this._addToCluster(childNode,parentNode,false); + } + } + } + } + } } + }; - /** - * reverses the map lookup so that we can look for specific keys - * to see what can and can't use keypress - * - * @return {Object} - */ - function _getReverseMap() { - if (!_REVERSE_MAP) { - _REVERSE_MAP = {}; - for (var key in _MAP) { + /** + * This function forces the network to cluster all nodes with only one connecting edge to their + * connected node. + * + * @private + */ + exports._forceClustersByZoom = function() { + for (var nodeId in this.nodes) { + // another node could have absorbed this child. + if (this.nodes.hasOwnProperty(nodeId)) { + var childNode = this.nodes[nodeId]; - // pull out the numeric keypad from here cause keypress should - // be able to detect the keys from the character - if (key > 95 && key < 112) { - continue; - } + // the edges can be swallowed by another decrease + if (childNode.dynamicEdgesLength == 1 && childNode.dynamicEdges.length != 0) { + var edge = childNode.dynamicEdges[0]; + var parentNode = (edge.toId == childNode.id) ? this.nodes[edge.fromId] : this.nodes[edge.toId]; - if (_MAP.hasOwnProperty(key)) { - _REVERSE_MAP[_MAP[key]] = key; - } + // group to the largest node + if (childNode.id != parentNode.id) { + if (parentNode.mass > childNode.mass) { + this._addToCluster(parentNode,childNode,true); } + else { + this._addToCluster(childNode,parentNode,true); + } + } } - return _REVERSE_MAP; + } } + }; - /** - * picks the best action based on the key combination - * - * @param {string} key - character for key - * @param {Array} modifiers - * @param {string=} action passed in - */ - function _pickBestAction(key, modifiers, action) { - // if no action was picked in we should try to pick the one - // that we think would work best for this key - if (!action) { - action = _getReverseMap()[key] ? 'keydown' : 'keypress'; + /** + * To keep the nodes of roughly equal size we normalize the cluster levels. + * This function clusters a node to its smallest connected neighbour. + * + * @param node + * @private + */ + exports._clusterToSmallestNeighbour = function(node) { + var smallestNeighbour = -1; + var smallestNeighbourNode = null; + for (var i = 0; i < node.dynamicEdges.length; i++) { + if (node.dynamicEdges[i] !== undefined) { + var neighbour = null; + if (node.dynamicEdges[i].fromId != node.id) { + neighbour = node.dynamicEdges[i].from; + } + else if (node.dynamicEdges[i].toId != node.id) { + neighbour = node.dynamicEdges[i].to; } - // modifier keys don't work as expected with keypress, - // switch to keydown - if (action == 'keypress' && modifiers.length) { - action = 'keydown'; + + if (neighbour != null && smallestNeighbour > neighbour.clusterSessions.length) { + smallestNeighbour = neighbour.clusterSessions.length; + smallestNeighbourNode = neighbour; } + } + } - return action; + if (neighbour != null && this.nodes[neighbour.id] !== undefined) { + this._addToCluster(neighbour, node, true); } + }; - /** - * binds a key sequence to an event - * - * @param {string} combo - combo specified in bind call - * @param {Array} keys - * @param {Function} callback - * @param {string=} action - * @returns void - */ - function _bindSequence(combo, keys, callback, action) { - // start off by adding a sequence level record for this combination - // and setting the level to 0 - _sequence_levels[combo] = 0; + /** + * This function forms clusters from hubs, it loops over all nodes + * + * @param {Boolean} force | Disregard zoom level + * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges + * @private + */ + exports._formClustersByHub = function(force, onlyEqual) { + // we loop over all nodes in the list + for (var nodeId in this.nodes) { + // we check if it is still available since it can be used by the clustering in this loop + if (this.nodes.hasOwnProperty(nodeId)) { + this._formClusterFromHub(this.nodes[nodeId],force,onlyEqual); + } + } + }; - // if there is no action pick the best one for the first key - // in the sequence - if (!action) { - action = _pickBestAction(keys[0], []); - } + /** + * This function forms a cluster from a specific preselected hub node + * + * @param {Node} hubNode | the node we will cluster as a hub + * @param {Boolean} force | Disregard zoom level + * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges + * @param {Number} [absorptionSizeOffset] | + * @private + */ + exports._formClusterFromHub = function(hubNode, force, onlyEqual, absorptionSizeOffset) { + if (absorptionSizeOffset === undefined) { + absorptionSizeOffset = 0; + } + // we decide if the node is a hub + if ((hubNode.dynamicEdgesLength >= this.hubThreshold && onlyEqual == false) || + (hubNode.dynamicEdgesLength == this.hubThreshold && onlyEqual == true)) { + // initialize variables + var dx,dy,length; + var minLength = this.constants.clustering.clusterEdgeThreshold/this.scale; + var allowCluster = false; - /** - * callback to increase the sequence level for this sequence and reset - * all other sequences that were active - * - * @param {Event} e - * @returns void - */ - var _increaseSequence = function(e) { - _inside_sequence = action; - ++_sequence_levels[combo]; - _resetSequenceTimer(); - }, + // we create a list of edges because the dynamicEdges change over the course of this loop + var edgesIdarray = []; + var amountOfInitialEdges = hubNode.dynamicEdges.length; + for (var j = 0; j < amountOfInitialEdges; j++) { + edgesIdarray.push(hubNode.dynamicEdges[j].id); + } - /** - * wraps the specified callback inside of another function in order - * to reset all sequence counters as soon as this sequence is done - * - * @param {Event} e - * @returns void - */ - _callbackAndReset = function(e) { - _fireCallback(callback, e); + // if the hub clustering is not forces, we check if one of the edges connected + // to a cluster is small enough based on the constants.clustering.clusterEdgeThreshold + if (force == false) { + allowCluster = false; + for (j = 0; j < amountOfInitialEdges; j++) { + var edge = this.edges[edgesIdarray[j]]; + if (edge !== undefined) { + if (edge.connected) { + if (edge.toId != edge.fromId) { + dx = (edge.to.x - edge.from.x); + dy = (edge.to.y - edge.from.y); + length = Math.sqrt(dx * dx + dy * dy); - // we should ignore the next key up if the action is key down - // or keypress. this is so if you finish a sequence and - // release the key the final key will not trigger a keyup - if (action !== 'keyup') { - _ignore_next_keyup = _characterFromEvent(e); + if (length < minLength) { + allowCluster = true; + break; } + } + } + } + } + } - // weird race condition if a sequence ends with the key - // another sequence begins with - setTimeout(_resetSequences, 10); - }, - i; - - // loop through keys one at a time and bind the appropriate callback - // function. for any key leading up to the final one it should - // increase the sequence. after the final, it should reset all sequences - for (i = 0; i < keys.length; ++i) { - _bindSingle(keys[i], i < keys.length - 1 ? _increaseSequence : _callbackAndReset, action, combo, i); + // start the clustering if allowed + if ((!force && allowCluster) || force) { + // we loop over all edges INITIALLY connected to this hub + for (j = 0; j < amountOfInitialEdges; j++) { + edge = this.edges[edgesIdarray[j]]; + // the edge can be clustered by this function in a previous loop + if (edge !== undefined) { + var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId]; + // we do not want hubs to merge with other hubs nor do we want to cluster itself. + if ((childNode.dynamicEdges.length <= (this.hubThreshold + absorptionSizeOffset)) && + (childNode.id != hubNode.id)) { + this._addToCluster(hubNode,childNode,force); + } + } } + } } + }; - /** - * binds a single keyboard combination - * - * @param {string} combination - * @param {Function} callback - * @param {string=} action - * @param {string=} sequence_name - name of sequence if part of sequence - * @param {number=} level - what part of the sequence the command is - * @returns void - */ - function _bindSingle(combination, callback, action, sequence_name, level) { - // make sure multiple spaces in a row become a single space - combination = combination.replace(/\s+/g, ' '); - var sequence = combination.split(' '), - i, - key, - keys, - modifiers = []; + /** + * This function adds the child node to the parent node, creating a cluster if it is not already. + * + * @param {Node} parentNode | this is the node that will house the child node + * @param {Node} childNode | this node will be deleted from the global this.nodes and stored in the parent node + * @param {Boolean} force | true will only update the remainingEdges at the very end of the clustering, ensuring single level collapse + * @private + */ + exports._addToCluster = function(parentNode, childNode, force) { + // join child node in the parent node + parentNode.containedNodes[childNode.id] = childNode; - // if this pattern is a sequence of keys then run through this method - // to reprocess each pattern one key at a time - if (sequence.length > 1) { - return _bindSequence(combination, sequence, callback, action); - } + // manage all the edges connected to the child and parent nodes + for (var i = 0; i < childNode.dynamicEdges.length; i++) { + var edge = childNode.dynamicEdges[i]; + if (edge.toId == parentNode.id || edge.fromId == parentNode.id) { // edge connected to parentNode + this._addToContainedEdges(parentNode,childNode,edge); + } + else { + this._connectEdgeToCluster(parentNode,childNode,edge); + } + } + // a contained node has no dynamic edges. + childNode.dynamicEdges = []; - // take the keys from this pattern and figure out what the actual - // pattern is all about - keys = combination === '+' ? ['+'] : combination.split('+'); + // remove circular edges from clusters + this._containCircularEdgesFromNode(parentNode,childNode); - for (i = 0; i < keys.length; ++i) { - key = keys[i]; - // normalize key names - if (_SPECIAL_ALIASES[key]) { - key = _SPECIAL_ALIASES[key]; - } + // remove the childNode from the global nodes object + delete this.nodes[childNode.id]; - // if this is not a keypress event then we should - // be smart about using shift keys - // this will only work for US keyboards however - if (action && action != 'keypress' && _SHIFT_MAP[key]) { - key = _SHIFT_MAP[key]; - modifiers.push('shift'); - } + // update the properties of the child and parent + var massBefore = parentNode.mass; + childNode.clusterSession = this.clusterSession; + parentNode.mass += childNode.mass; + parentNode.clusterSize += childNode.clusterSize; + parentNode.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize); - // if this key is a modifier then add it to the list of modifiers - if (_isModifier(key)) { - modifiers.push(key); - } - } + // keep track of the clustersessions so we can open the cluster up as it has been formed. + if (parentNode.clusterSessions[parentNode.clusterSessions.length - 1] != this.clusterSession) { + parentNode.clusterSessions.push(this.clusterSession); + } - // depending on what the key combination is - // we will try to pick the best event for it - action = _pickBestAction(key, modifiers, action); + // forced clusters only open from screen size and double tap + if (force == true) { + // parentNode.formationScale = Math.pow(1 - (1.0/11.0),this.clusterSession+3); + parentNode.formationScale = 0; + } + else { + parentNode.formationScale = this.scale; // The latest child has been added on this scale + } - // make sure to initialize array if this is the first time - // a callback is added for this key - if (!_callbacks[key]) { - _callbacks[key] = []; - } + // recalculate the size of the node on the next time the node is rendered + parentNode.clearSizeCache(); - // remove an existing match if there is one - _getMatches(key, modifiers, action, !sequence_name, combination); + // set the pop-out scale for the childnode + parentNode.containedNodes[childNode.id].formationScale = parentNode.formationScale; - // add this call back to the array - // if it is a sequence put it at the beginning - // if not put it at the end - // - // this is important because the way these are processed expects - // the sequence ones to come first - _callbacks[key][sequence_name ? 'unshift' : 'push']({ - callback: callback, - modifiers: modifiers, - action: action, - seq: sequence_name, - level: level, - combo: combination - }); - } - - /** - * binds multiple combinations to the same callback - * - * @param {Array} combinations - * @param {Function} callback - * @param {string|undefined} action - * @returns void - */ - function _bindMultiple(combinations, callback, action) { - for (var i = 0; i < combinations.length; ++i) { - _bindSingle(combinations[i], callback, action); - } - } - - // start! - _addEvent(document, 'keypress', _handleKey); - _addEvent(document, 'keydown', _handleKey); - _addEvent(document, 'keyup', _handleKey); - - var mousetrap = { - - /** - * binds an event to mousetrap - * - * can be a single key, a combination of keys separated with +, - * a comma separated list of keys, an array of keys, or - * a sequence of keys separated by spaces - * - * be sure to list the modifier keys first to make sure that the - * correct key ends up getting bound (the last key in the pattern) - * - * @param {string|Array} keys - * @param {Function} callback - * @param {string=} action - 'keypress', 'keydown', or 'keyup' - * @returns void - */ - bind: function(keys, callback, action) { - _bindMultiple(keys instanceof Array ? keys : [keys], callback, action); - _direct_map[keys + ':' + action] = callback; - return this; - }, - - /** - * unbinds an event to mousetrap - * - * the unbinding sets the callback function of the specified key combo - * to an empty function and deletes the corresponding key in the - * _direct_map dict. - * - * the keycombo+action has to be exactly the same as - * it was defined in the bind method - * - * TODO: actually remove this from the _callbacks dictionary instead - * of binding an empty function - * - * @param {string|Array} keys - * @param {string} action - * @returns void - */ - unbind: function(keys, action) { - if (_direct_map[keys + ':' + action]) { - delete _direct_map[keys + ':' + action]; - this.bind(keys, function() {}, action); - } - return this; - }, - - /** - * triggers an event that has already been bound - * - * @param {string} keys - * @param {string=} action - * @returns void - */ - trigger: function(keys, action) { - _direct_map[keys + ':' + action](); - return this; - }, - - /** - * resets the library back to its initial state. this is useful - * if you want to clear out the current keyboard shortcuts and bind - * new ones - for example if you switch to another page - * - * @returns void - */ - reset: function() { - _callbacks = {}; - _direct_map = {}; - return this; - } - }; - - module.exports = mousetrap; + // nullify the movement velocity of the child, this is to avoid hectic behaviour + childNode.clearVelocity(); + // the mass has altered, preservation of energy dictates the velocity to be updated + parentNode.updateVelocity(massBefore); + // restart the simulation to reorganise all nodes + this.moving = true; + }; -/***/ }, -/* 43 */ -/***/ function(module, exports, __webpack_require__) { /** - * Creation of the ClusterMixin var. - * - * This contains all the functions the Network object can use to employ clustering + * This function will apply the changes made to the remainingEdges during the formation of the clusters. + * This is a seperate function to allow for level-wise collapsing of the node barnesHutTree. + * It has to be called if a level is collapsed. It is called by _formClusters(). + * @private */ + exports._updateDynamicEdges = function() { + for (var i = 0; i < this.nodeIndices.length; i++) { + var node = this.nodes[this.nodeIndices[i]]; + node.dynamicEdgesLength = node.dynamicEdges.length; - /** - * This is only called in the constructor of the network object - * - */ - exports.startWithClustering = function() { - // cluster if the data set is big - this.clusterToFit(this.constants.clustering.initialMaxNodes, true); - - // updates the lables after clustering - this.updateLabels(); - - // this is called here because if clusterin is disabled, the start and stabilize are called in - // the setData function. - if (this.stabilize) { - this._stabilize(); - } - this.start(); + // this corrects for multiple edges pointing at the same other node + var correction = 0; + if (node.dynamicEdgesLength > 1) { + for (var j = 0; j < node.dynamicEdgesLength - 1; j++) { + var edgeToId = node.dynamicEdges[j].toId; + var edgeFromId = node.dynamicEdges[j].fromId; + for (var k = j+1; k < node.dynamicEdgesLength; k++) { + if ((node.dynamicEdges[k].toId == edgeToId && node.dynamicEdges[k].fromId == edgeFromId) || + (node.dynamicEdges[k].fromId == edgeToId && node.dynamicEdges[k].toId == edgeFromId)) { + correction += 1; + } + } + } + } + node.dynamicEdgesLength -= correction; + } }; + /** - * This function clusters until the initialMaxNodes has been reached + * This adds an edge from the childNode to the contained edges of the parent node * - * @param {Number} maxNumberOfNodes - * @param {Boolean} reposition + * @param parentNode | Node object + * @param childNode | Node object + * @param edge | Edge object + * @private */ - exports.clusterToFit = function(maxNumberOfNodes, reposition) { - var numberOfNodes = this.nodeIndices.length; + exports._addToContainedEdges = function(parentNode, childNode, edge) { + // create an array object if it does not yet exist for this childNode + if (!(parentNode.containedEdges.hasOwnProperty(childNode.id))) { + parentNode.containedEdges[childNode.id] = [] + } + // add this edge to the list + parentNode.containedEdges[childNode.id].push(edge); - var maxLevels = 50; - var level = 0; + // remove the edge from the global edges object + delete this.edges[edge.id]; - // we first cluster the hubs, then we pull in the outliers, repeat - while (numberOfNodes > maxNumberOfNodes && level < maxLevels) { - if (level % 3 == 0) { - this.forceAggregateHubs(true); - this.normalizeClusterLevels(); - } - else { - this.increaseClusterLevel(); // this also includes a cluster normalization + // remove the edge from the parent object + for (var i = 0; i < parentNode.dynamicEdges.length; i++) { + if (parentNode.dynamicEdges[i].id == edge.id) { + parentNode.dynamicEdges.splice(i,1); + break; } - - numberOfNodes = this.nodeIndices.length; - level += 1; - } - - // after the clustering we reposition the nodes to reduce the initial chaos - if (level > 0 && reposition == true) { - this.repositionNodes(); } - this._updateCalculationNodes(); }; /** - * This function can be called to open up a specific cluster. It is only called by - * It will unpack the cluster back one level. + * This function connects an edge that was connected to a child node to the parent node. + * It keeps track of which nodes it has been connected to with the originalId array. * - * @param node | Node object: cluster to open. + * @param {Node} parentNode | Node object + * @param {Node} childNode | Node object + * @param {Edge} edge | Edge object + * @private */ - exports.openCluster = function(node) { - var isMovingBeforeClustering = this.moving; - if (node.clusterSize > this.constants.clustering.sectorThreshold && this._nodeInActiveArea(node) && - !(this._sector() == "default" && this.nodeIndices.length == 1)) { - // this loads a new sector, loads the nodes and edges and nodeIndices of it. - this._addSector(node); - var level = 0; - - // we decluster until we reach a decent number of nodes - while ((this.nodeIndices.length < this.constants.clustering.initialMaxNodes) && (level < 10)) { - this.decreaseClusterLevel(); - level += 1; - } - + exports._connectEdgeToCluster = function(parentNode, childNode, edge) { + // handle circular edges + if (edge.toId == edge.fromId) { + this._addToContainedEdges(parentNode, childNode, edge); } else { - this._expandClusterNode(node,false,true); + if (edge.toId == childNode.id) { // edge connected to other node on the "to" side + edge.originalToId.push(childNode.id); + edge.to = parentNode; + edge.toId = parentNode.id; + } + else { // edge connected to other node with the "from" side - // update the index list, dynamic edges and labels - this._updateNodeIndexList(); - this._updateDynamicEdges(); - this._updateCalculationNodes(); - this.updateLabels(); - } + edge.originalFromId.push(childNode.id); + edge.from = parentNode; + edge.fromId = parentNode.id; + } - // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded - if (this.moving != isMovingBeforeClustering) { - this.start(); + this._addToReroutedEdges(parentNode,childNode,edge); } }; /** - * This calls the updateClustes with default arguments + * If a node is connected to itself, a circular edge is drawn. When clustering we want to contain + * these edges inside of the cluster. + * + * @param parentNode + * @param childNode + * @private */ - exports.updateClustersDefault = function() { - if (this.constants.clustering.enabled == true) { - this.updateClusters(0,false,false); + exports._containCircularEdgesFromNode = function(parentNode, childNode) { + // manage all the edges connected to the child and parent nodes + for (var i = 0; i < parentNode.dynamicEdges.length; i++) { + var edge = parentNode.dynamicEdges[i]; + // handle circular edges + if (edge.toId == edge.fromId) { + this._addToContainedEdges(parentNode, childNode, edge); + } } }; /** - * This function can be called to increase the cluster level. This means that the nodes with only one edge connection will - * be clustered with their connected node. This can be repeated as many times as needed. - * This can be called externally (by a keybind for instance) to reduce the complexity of big datasets. - */ - exports.increaseClusterLevel = function() { - this.updateClusters(-1,false,true); - }; - - - /** - * This function can be called to decrease the cluster level. This means that the nodes with only one edge connection will - * be unpacked if they are a cluster. This can be repeated as many times as needed. - * This can be called externally (by a key-bind for instance) to look into clusters without zooming. - */ - exports.decreaseClusterLevel = function() { - this.updateClusters(1,false,true); - }; - - - /** - * This is the main clustering function. It clusters and declusters on zoom or forced - * This function clusters on zoom, it can be called with a predefined zoom direction - * If out, check if we can form clusters, if in, check if we can open clusters. - * This function is only called from _zoom() - * - * @param {Number} zoomDirection | -1 / 0 / +1 for zoomOut / determineByZoom / zoomIn - * @param {Boolean} recursive | enabled or disable recursive calling of the opening of clusters - * @param {Boolean} force | enabled or disable forcing - * @param {Boolean} doNotStart | if true do not call start + * This adds an edge from the childNode to the rerouted edges of the parent node * + * @param parentNode | Node object + * @param childNode | Node object + * @param edge | Edge object + * @private */ - exports.updateClusters = function(zoomDirection,recursive,force,doNotStart) { - var isMovingBeforeClustering = this.moving; - var amountOfNodes = this.nodeIndices.length; - - // on zoom out collapse the sector if the scale is at the level the sector was made - if (this.previousScale > this.scale && zoomDirection == 0) { - this._collapseSector(); - } - - // check if we zoom in or out - if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out - // forming clusters when forced pulls outliers in. When not forced, the edge length of the - // outer nodes determines if it is being clustered - this._formClusters(force); - } - else if (this.previousScale < this.scale || zoomDirection == 1) { // zoom in - if (force == true) { - // _openClusters checks for each node if the formationScale of the cluster is smaller than - // the current scale and if so, declusters. When forced, all clusters are reduced by one step - this._openClusters(recursive,force); - } - else { - // if a cluster takes up a set percentage of the active window - this._openClustersBySize(); - } + exports._addToReroutedEdges = function(parentNode, childNode, edge) { + // create an array object if it does not yet exist for this childNode + // we store the edge in the rerouted edges so we can restore it when the cluster pops open + if (!(parentNode.reroutedEdges.hasOwnProperty(childNode.id))) { + parentNode.reroutedEdges[childNode.id] = []; } - this._updateNodeIndexList(); + parentNode.reroutedEdges[childNode.id].push(edge); - // if a cluster was NOT formed and the user zoomed out, we try clustering by hubs - if (this.nodeIndices.length == amountOfNodes && (this.previousScale > this.scale || zoomDirection == -1)) { - this._aggregateHubs(force); - this._updateNodeIndexList(); - } + // this edge becomes part of the dynamicEdges of the cluster node + parentNode.dynamicEdges.push(edge); + }; - // we now reduce chains. - if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out - this.handleChains(); - this._updateNodeIndexList(); - } - this.previousScale = this.scale; - // rest of the update the index list, dynamic edges and labels - this._updateDynamicEdges(); - this.updateLabels(); + /** + * This function connects an edge that was connected to a cluster node back to the child node. + * + * @param parentNode | Node object + * @param childNode | Node object + * @private + */ + exports._connectEdgeBackToChild = function(parentNode, childNode) { + if (parentNode.reroutedEdges.hasOwnProperty(childNode.id)) { + for (var i = 0; i < parentNode.reroutedEdges[childNode.id].length; i++) { + var edge = parentNode.reroutedEdges[childNode.id][i]; + if (edge.originalFromId[edge.originalFromId.length-1] == childNode.id) { + edge.originalFromId.pop(); + edge.fromId = childNode.id; + edge.from = childNode; + } + else { + edge.originalToId.pop(); + edge.toId = childNode.id; + edge.to = childNode; + } - // if a cluster was formed, we increase the clusterSession - if (this.nodeIndices.length < amountOfNodes) { // this means a clustering operation has taken place - this.clusterSession += 1; - // if clusters have been made, we normalize the cluster level - this.normalizeClusterLevels(); - } + // append this edge to the list of edges connecting to the childnode + childNode.dynamicEdges.push(edge); - if (doNotStart == false || doNotStart === undefined) { - // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded - if (this.moving != isMovingBeforeClustering) { - this.start(); + // remove the edge from the parent object + for (var j = 0; j < parentNode.dynamicEdges.length; j++) { + if (parentNode.dynamicEdges[j].id == edge.id) { + parentNode.dynamicEdges.splice(j,1); + break; + } + } } + // remove the entry from the rerouted edges + delete parentNode.reroutedEdges[childNode.id]; } - - this._updateCalculationNodes(); }; - /** - * This function handles the chains. It is called on every updateClusters(). - */ - exports.handleChains = function() { - // after clustering we check how many chains there are - var chainPercentage = this._getChainFraction(); - if (chainPercentage > this.constants.clustering.chainThreshold) { - this._reduceAmountOfChains(1 - this.constants.clustering.chainThreshold / chainPercentage) - - } - }; /** - * this functions starts clustering by hubs - * The minimum hub threshold is set globally + * When loops are clustered, an edge can be both in the rerouted array and the contained array. + * This function is called last to verify that all edges in dynamicEdges are in fact connected to the + * parentNode * + * @param parentNode | Node object * @private */ - exports._aggregateHubs = function(force) { - this._getHubSize(); - this._formClustersByHub(force,false); + exports._validateEdges = function(parentNode) { + for (var i = 0; i < parentNode.dynamicEdges.length; i++) { + var edge = parentNode.dynamicEdges[i]; + if (parentNode.id != edge.toId && parentNode.id != edge.fromId) { + parentNode.dynamicEdges.splice(i,1); + } + } }; /** - * This function is fired by keypress. It forces hubs to form. + * This function released the contained edges back into the global domain and puts them back into the + * dynamic edges of both parent and child. * + * @param {Node} parentNode | + * @param {Node} childNode | + * @private */ - exports.forceAggregateHubs = function(doNotStart) { - var isMovingBeforeClustering = this.moving; - var amountOfNodes = this.nodeIndices.length; - - this._aggregateHubs(true); + exports._releaseContainedEdges = function(parentNode, childNode) { + for (var i = 0; i < parentNode.containedEdges[childNode.id].length; i++) { + var edge = parentNode.containedEdges[childNode.id][i]; - // update the index list, dynamic edges and labels - this._updateNodeIndexList(); - this._updateDynamicEdges(); - this.updateLabels(); + // put the edge back in the global edges object + this.edges[edge.id] = edge; - // if a cluster was formed, we increase the clusterSession - if (this.nodeIndices.length != amountOfNodes) { - this.clusterSession += 1; + // put the edge back in the dynamic edges of the child and parent + childNode.dynamicEdges.push(edge); + parentNode.dynamicEdges.push(edge); } + // remove the entry from the contained edges + delete parentNode.containedEdges[childNode.id]; - if (doNotStart == false || doNotStart === undefined) { - // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded - if (this.moving != isMovingBeforeClustering) { - this.start(); - } - } }; + + + + // ------------------- UTILITY FUNCTIONS ---------------------------- // + + /** - * If a cluster takes up more than a set percentage of the screen, open the cluster - * - * @private + * This updates the node labels for all nodes (for debugging purposes) */ - exports._openClustersBySize = function() { - for (var nodeId in this.nodes) { + exports.updateLabels = function() { + var nodeId; + // update node labels + for (nodeId in this.nodes) { if (this.nodes.hasOwnProperty(nodeId)) { var node = this.nodes[nodeId]; - if (node.inView() == true) { - if ((node.width*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) || - (node.height*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) { - this.openCluster(node); + if (node.clusterSize > 1) { + node.label = "[".concat(String(node.clusterSize),"]"); + } + } + } + + // update node labels + for (nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + node = this.nodes[nodeId]; + if (node.clusterSize == 1) { + if (node.originalLabel !== undefined) { + node.label = node.originalLabel; + } + else { + node.label = String(node.id); } } } } + + // /* Debug Override */ + // for (nodeId in this.nodes) { + // if (this.nodes.hasOwnProperty(nodeId)) { + // node = this.nodes[nodeId]; + // node.label = String(node.level); + // } + // } + }; /** - * This function loops over all nodes in the nodeIndices list. For each node it checks if it is a cluster and if it - * has to be opened based on the current zoom level. - * - * @private + * We want to keep the cluster level distribution rather small. This means we do not want unclustered nodes + * if the rest of the nodes are already a few cluster levels in. + * To fix this we use this function. It determines the min and max cluster level and sends nodes that have not + * clustered enough to the clusterToSmallestNeighbours function. */ - exports._openClusters = function(recursive,force) { - for (var i = 0; i < this.nodeIndices.length; i++) { - var node = this.nodes[this.nodeIndices[i]]; - this._expandClusterNode(node,recursive,force); - this._updateCalculationNodes(); + exports.normalizeClusterLevels = function() { + var maxLevel = 0; + var minLevel = 1e9; + var clusterLevel = 0; + var nodeId; + + // we loop over all nodes in the list + for (nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + clusterLevel = this.nodes[nodeId].clusterSessions.length; + if (maxLevel < clusterLevel) {maxLevel = clusterLevel;} + if (minLevel > clusterLevel) {minLevel = clusterLevel;} + } } - }; - /** - * This function checks if a node has to be opened. This is done by checking the zoom level. - * If the node contains child nodes, this function is recursively called on the child nodes as well. - * This recursive behaviour is optional and can be set by the recursive argument. + if (maxLevel - minLevel > this.constants.clustering.clusterLevelDifference) { + var amountOfNodes = this.nodeIndices.length; + var targetLevel = maxLevel - this.constants.clustering.clusterLevelDifference; + // we loop over all nodes in the list + for (nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + if (this.nodes[nodeId].clusterSessions.length < targetLevel) { + this._clusterToSmallestNeighbour(this.nodes[nodeId]); + } + } + } + this._updateNodeIndexList(); + this._updateDynamicEdges(); + // if a cluster was formed, we increase the clusterSession + if (this.nodeIndices.length != amountOfNodes) { + this.clusterSession += 1; + } + } + }; + + + + /** + * This function determines if the cluster we want to decluster is in the active area + * this means around the zoom center * - * @param {Node} parentNode | to check for cluster and expand - * @param {Boolean} recursive | enabled or disable recursive calling - * @param {Boolean} force | enabled or disable forcing - * @param {Boolean} [openAll] | This will recursively force all nodes in the parent to be released + * @param {Node} node + * @returns {boolean} * @private */ - exports._expandClusterNode = function(parentNode, recursive, force, openAll) { - // first check if node is a cluster - if (parentNode.clusterSize > 1) { - // this means that on a double tap event or a zoom event, the cluster fully unpacks if it is smaller than 20 - if (parentNode.clusterSize < this.constants.clustering.sectorThreshold) { - openAll = true; - } - recursive = openAll ? true : recursive; + exports._nodeInActiveArea = function(node) { + return ( + Math.abs(node.x - this.areaCenter.x) <= this.constants.clustering.activeAreaBoxSize/this.scale + && + Math.abs(node.y - this.areaCenter.y) <= this.constants.clustering.activeAreaBoxSize/this.scale + ) + }; - // if the last child has been added on a smaller scale than current scale decluster - if (parentNode.formationScale < this.scale || force == true) { - // we will check if any of the contained child nodes should be removed from the cluster - for (var containedNodeId in parentNode.containedNodes) { - if (parentNode.containedNodes.hasOwnProperty(containedNodeId)) { - var childNode = parentNode.containedNodes[containedNodeId]; - // force expand will expand the largest cluster size clusters. Since we cluster from outside in, we assume that - // the largest cluster is the one that comes from outside - if (force == true) { - if (childNode.clusterSession == parentNode.clusterSessions[parentNode.clusterSessions.length-1] - || openAll) { - this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll); - } - } - else { - if (this._nodeInActiveArea(parentNode)) { - this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll); - } - } - } - } + /** + * This is an adaptation of the original repositioning function. This is called if the system is clustered initially + * It puts large clusters away from the center and randomizes the order. + * + */ + exports.repositionNodes = function() { + for (var i = 0; i < this.nodeIndices.length; i++) { + var node = this.nodes[this.nodeIndices[i]]; + if ((node.xFixed == false || node.yFixed == false)) { + var radius = 10 * 0.1*this.nodeIndices.length * Math.min(100,node.mass); + var angle = 2 * Math.PI * Math.random(); + if (node.xFixed == false) {node.x = radius * Math.cos(angle);} + if (node.yFixed == false) {node.y = radius * Math.sin(angle);} + this._repositionBezierNodes(node); } } }; + /** - * ONLY CALLED FROM _expandClusterNode - * - * This function will expel a child_node from a parent_node. This is to de-cluster the node. This function will remove - * the child node from the parent contained_node object and put it back into the global nodes object. - * The same holds for the edge that was connected to the child node. It is moved back into the global edges object. + * We determine how many connections denote an important hub. + * We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%) * - * @param {Node} parentNode | the parent node - * @param {String} containedNodeId | child_node id as it is contained in the containedNodes object of the parent node - * @param {Boolean} recursive | This will also check if the child needs to be expanded. - * With force and recursive both true, the entire cluster is unpacked - * @param {Boolean} force | This will disregard the zoom level and will expel this child from the parent - * @param {Boolean} openAll | This will recursively force all nodes in the parent to be released * @private */ - exports._expelChildFromParent = function(parentNode, containedNodeId, recursive, force, openAll) { - var childNode = parentNode.containedNodes[containedNodeId]; + exports._getHubSize = function() { + var average = 0; + var averageSquared = 0; + var hubCounter = 0; + var largestHub = 0; - // if child node has been added on smaller scale than current, kick out - if (childNode.formationScale < this.scale || force == true) { - // unselect all selected items - this._unselectAll(); + for (var i = 0; i < this.nodeIndices.length; i++) { - // put the child node back in the global nodes object - this.nodes[containedNodeId] = childNode; + var node = this.nodes[this.nodeIndices[i]]; + if (node.dynamicEdgesLength > largestHub) { + largestHub = node.dynamicEdgesLength; + } + average += node.dynamicEdgesLength; + averageSquared += Math.pow(node.dynamicEdgesLength,2); + hubCounter += 1; + } + average = average / hubCounter; + averageSquared = averageSquared / hubCounter; - // release the contained edges from this childNode back into the global edges - this._releaseContainedEdges(parentNode,childNode); + var variance = averageSquared - Math.pow(average,2); - // reconnect rerouted edges to the childNode - this._connectEdgeBackToChild(parentNode,childNode); + var standardDeviation = Math.sqrt(variance); - // validate all edges in dynamicEdges - this._validateEdges(parentNode); + this.hubThreshold = Math.floor(average + 2*standardDeviation); - // undo the changes from the clustering operation on the parent node - parentNode.mass -= childNode.mass; - parentNode.clusterSize -= childNode.clusterSize; - parentNode.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize); - parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length; + // always have at least one to cluster + if (this.hubThreshold > largestHub) { + this.hubThreshold = largestHub; + } - // place the child node near the parent, not at the exact same location to avoid chaos in the system - childNode.x = parentNode.x + parentNode.growthIndicator * (0.5 - Math.random()); - childNode.y = parentNode.y + parentNode.growthIndicator * (0.5 - Math.random()); + // console.log("average",average,"averageSQ",averageSquared,"var",variance,"std",standardDeviation); + // console.log("hubThreshold:",this.hubThreshold); + }; - // remove node from the list - delete parentNode.containedNodes[containedNodeId]; - // check if there are other childs with this clusterSession in the parent. - var othersPresent = false; - for (var childNodeId in parentNode.containedNodes) { - if (parentNode.containedNodes.hasOwnProperty(childNodeId)) { - if (parentNode.containedNodes[childNodeId].clusterSession == childNode.clusterSession) { - othersPresent = true; - break; + /** + * We reduce the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods + * with this amount we can cluster specifically on these chains. + * + * @param {Number} fraction | between 0 and 1, the percentage of chains to reduce + * @private + */ + exports._reduceAmountOfChains = function(fraction) { + this.hubThreshold = 2; + var reduceAmount = Math.floor(this.nodeIndices.length * fraction); + for (var nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) { + if (reduceAmount > 0) { + this._formClusterFromHub(this.nodes[nodeId],true,true,1); + reduceAmount -= 1; } } } - // if there are no others, remove the cluster session from the list - if (othersPresent == false) { - parentNode.clusterSessions.pop(); - } - - this._repositionBezierNodes(childNode); - // this._repositionBezierNodes(parentNode); - - // remove the clusterSession from the child node - childNode.clusterSession = 0; - - // recalculate the size of the node on the next time the node is rendered - parentNode.clearSizeCache(); - - // restart the simulation to reorganise all nodes - this.moving = true; } + }; - // check if a further expansion step is possible if recursivity is enabled - if (recursive == true) { - this._expandClusterNode(childNode,recursive,force,openAll); + /** + * We get the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods + * with this amount we can cluster specifically on these chains. + * + * @private + */ + exports._getChainFraction = function() { + var chains = 0; + var total = 0; + for (var nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) { + chains += 1; + } + total += 1; + } } + return chains/total; }; +/***/ }, +/* 43 */ +/***/ function(module, exports, __webpack_require__) { + + var util = __webpack_require__(1); + /** - * position the bezier nodes at the center of the edges + * Creation of the SectorMixin var. + * + * This contains all the functions the Network object can use to employ the sector system. + * The sector system is always used by Network, though the benefits only apply to the use of clustering. + * If clustering is not used, there is no overhead except for a duplicate object with references to nodes and edges. + */ + + /** + * This function is only called by the setData function of the Network object. + * This loads the global references into the active sector. This initializes the sector. * - * @param node * @private */ - exports._repositionBezierNodes = function(node) { - for (var i = 0; i < node.dynamicEdges.length; i++) { - node.dynamicEdges[i].positionBezierNode(); - } + exports._putDataInSector = function() { + this.sectors["active"][this._sector()].nodes = this.nodes; + this.sectors["active"][this._sector()].edges = this.edges; + this.sectors["active"][this._sector()].nodeIndices = this.nodeIndices; }; /** - * This function checks if any nodes at the end of their trees have edges below a threshold length - * This function is called only from updateClusters() - * forceLevelCollapse ignores the length of the edge and collapses one level - * This means that a node with only one edge will be clustered with its connected node + * /** + * This function sets the global references to nodes, edges and nodeIndices back to + * those of the supplied (active) sector. If a type is defined, do the specific type * + * @param {String} sectorId + * @param {String} [sectorType] | "active" or "frozen" * @private - * @param {Boolean} force */ - exports._formClusters = function(force) { - if (force == false) { - this._formClustersByZoom(); + exports._switchToSector = function(sectorId, sectorType) { + if (sectorType === undefined || sectorType == "active") { + this._switchToActiveSector(sectorId); } else { - this._forceClustersByZoom(); + this._switchToFrozenSector(sectorId); } }; /** - * This function handles the clustering by zooming out, this is based on a minimum edge distance + * This function sets the global references to nodes, edges and nodeIndices back to + * those of the supplied active sector. * + * @param sectorId * @private */ - exports._formClustersByZoom = function() { - var dx,dy,length, - minLength = this.constants.clustering.clusterEdgeThreshold/this.scale; - - // check if any edges are shorter than minLength and start the clustering - // the clustering favours the node with the larger mass - for (var edgeId in this.edges) { - if (this.edges.hasOwnProperty(edgeId)) { - var edge = this.edges[edgeId]; - if (edge.connected) { - if (edge.toId != edge.fromId) { - dx = (edge.to.x - edge.from.x); - dy = (edge.to.y - edge.from.y); - length = Math.sqrt(dx * dx + dy * dy); - + exports._switchToActiveSector = function(sectorId) { + this.nodeIndices = this.sectors["active"][sectorId]["nodeIndices"]; + this.nodes = this.sectors["active"][sectorId]["nodes"]; + this.edges = this.sectors["active"][sectorId]["edges"]; + }; - if (length < minLength) { - // first check which node is larger - var parentNode = edge.from; - var childNode = edge.to; - if (edge.to.mass > edge.from.mass) { - parentNode = edge.to; - childNode = edge.from; - } - if (childNode.dynamicEdgesLength == 1) { - this._addToCluster(parentNode,childNode,false); - } - else if (parentNode.dynamicEdgesLength == 1) { - this._addToCluster(childNode,parentNode,false); - } - } - } - } - } - } + /** + * This function sets the global references to nodes, edges and nodeIndices back to + * those of the supplied active sector. + * + * @private + */ + exports._switchToSupportSector = function() { + this.nodeIndices = this.sectors["support"]["nodeIndices"]; + this.nodes = this.sectors["support"]["nodes"]; + this.edges = this.sectors["support"]["edges"]; }; + /** - * This function forces the network to cluster all nodes with only one connecting edge to their - * connected node. + * This function sets the global references to nodes, edges and nodeIndices back to + * those of the supplied frozen sector. * + * @param sectorId * @private */ - exports._forceClustersByZoom = function() { - for (var nodeId in this.nodes) { - // another node could have absorbed this child. - if (this.nodes.hasOwnProperty(nodeId)) { - var childNode = this.nodes[nodeId]; + exports._switchToFrozenSector = function(sectorId) { + this.nodeIndices = this.sectors["frozen"][sectorId]["nodeIndices"]; + this.nodes = this.sectors["frozen"][sectorId]["nodes"]; + this.edges = this.sectors["frozen"][sectorId]["edges"]; + }; - // the edges can be swallowed by another decrease - if (childNode.dynamicEdgesLength == 1 && childNode.dynamicEdges.length != 0) { - var edge = childNode.dynamicEdges[0]; - var parentNode = (edge.toId == childNode.id) ? this.nodes[edge.fromId] : this.nodes[edge.toId]; - // group to the largest node - if (childNode.id != parentNode.id) { - if (parentNode.mass > childNode.mass) { - this._addToCluster(parentNode,childNode,true); - } - else { - this._addToCluster(childNode,parentNode,true); - } - } - } - } - } + /** + * This function sets the global references to nodes, edges and nodeIndices back to + * those of the currently active sector. + * + * @private + */ + exports._loadLatestSector = function() { + this._switchToSector(this._sector()); }; /** - * To keep the nodes of roughly equal size we normalize the cluster levels. - * This function clusters a node to its smallest connected neighbour. + * This function returns the currently active sector Id * - * @param node + * @returns {String} * @private */ - exports._clusterToSmallestNeighbour = function(node) { - var smallestNeighbour = -1; - var smallestNeighbourNode = null; - for (var i = 0; i < node.dynamicEdges.length; i++) { - if (node.dynamicEdges[i] !== undefined) { - var neighbour = null; - if (node.dynamicEdges[i].fromId != node.id) { - neighbour = node.dynamicEdges[i].from; - } - else if (node.dynamicEdges[i].toId != node.id) { - neighbour = node.dynamicEdges[i].to; - } + exports._sector = function() { + return this.activeSector[this.activeSector.length-1]; + }; - if (neighbour != null && smallestNeighbour > neighbour.clusterSessions.length) { - smallestNeighbour = neighbour.clusterSessions.length; - smallestNeighbourNode = neighbour; - } - } + /** + * This function returns the previously active sector Id + * + * @returns {String} + * @private + */ + exports._previousSector = function() { + if (this.activeSector.length > 1) { + return this.activeSector[this.activeSector.length-2]; } - - if (neighbour != null && this.nodes[neighbour.id] !== undefined) { - this._addToCluster(neighbour, node, true); + else { + throw new TypeError('there are not enough sectors in the this.activeSector array.'); } }; /** - * This function forms clusters from hubs, it loops over all nodes + * We add the active sector at the end of the this.activeSector array + * This ensures it is the currently active sector returned by _sector() and it reaches the top + * of the activeSector stack. When we reverse our steps we move from the end to the beginning of this stack. * - * @param {Boolean} force | Disregard zoom level - * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges + * @param newId * @private */ - exports._formClustersByHub = function(force, onlyEqual) { - // we loop over all nodes in the list - for (var nodeId in this.nodes) { - // we check if it is still available since it can be used by the clustering in this loop - if (this.nodes.hasOwnProperty(nodeId)) { - this._formClusterFromHub(this.nodes[nodeId],force,onlyEqual); - } - } + exports._setActiveSector = function(newId) { + this.activeSector.push(newId); }; + /** - * This function forms a cluster from a specific preselected hub node + * We remove the currently active sector id from the active sector stack. This happens when + * we reactivate the previously active sector * - * @param {Node} hubNode | the node we will cluster as a hub - * @param {Boolean} force | Disregard zoom level - * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges - * @param {Number} [absorptionSizeOffset] | * @private */ - exports._formClusterFromHub = function(hubNode, force, onlyEqual, absorptionSizeOffset) { - if (absorptionSizeOffset === undefined) { - absorptionSizeOffset = 0; - } - // we decide if the node is a hub - if ((hubNode.dynamicEdgesLength >= this.hubThreshold && onlyEqual == false) || - (hubNode.dynamicEdgesLength == this.hubThreshold && onlyEqual == true)) { - // initialize variables - var dx,dy,length; - var minLength = this.constants.clustering.clusterEdgeThreshold/this.scale; - var allowCluster = false; - - // we create a list of edges because the dynamicEdges change over the course of this loop - var edgesIdarray = []; - var amountOfInitialEdges = hubNode.dynamicEdges.length; - for (var j = 0; j < amountOfInitialEdges; j++) { - edgesIdarray.push(hubNode.dynamicEdges[j].id); - } + exports._forgetLastSector = function() { + this.activeSector.pop(); + }; - // if the hub clustering is not forces, we check if one of the edges connected - // to a cluster is small enough based on the constants.clustering.clusterEdgeThreshold - if (force == false) { - allowCluster = false; - for (j = 0; j < amountOfInitialEdges; j++) { - var edge = this.edges[edgesIdarray[j]]; - if (edge !== undefined) { - if (edge.connected) { - if (edge.toId != edge.fromId) { - dx = (edge.to.x - edge.from.x); - dy = (edge.to.y - edge.from.y); - length = Math.sqrt(dx * dx + dy * dy); - if (length < minLength) { - allowCluster = true; - break; - } - } - } - } - } - } + /** + * This function creates a new active sector with the supplied newId. This newId + * is the expanding node id. + * + * @param {String} newId | Id of the new active sector + * @private + */ + exports._createNewSector = function(newId) { + // create the new sector + this.sectors["active"][newId] = {"nodes":{}, + "edges":{}, + "nodeIndices":[], + "formationScale": this.scale, + "drawingNode": undefined}; - // start the clustering if allowed - if ((!force && allowCluster) || force) { - // we loop over all edges INITIALLY connected to this hub - for (j = 0; j < amountOfInitialEdges; j++) { - edge = this.edges[edgesIdarray[j]]; - // the edge can be clustered by this function in a previous loop - if (edge !== undefined) { - var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId]; - // we do not want hubs to merge with other hubs nor do we want to cluster itself. - if ((childNode.dynamicEdges.length <= (this.hubThreshold + absorptionSizeOffset)) && - (childNode.id != hubNode.id)) { - this._addToCluster(hubNode,childNode,force); - } + // create the new sector render node. This gives visual feedback that you are in a new sector. + this.sectors["active"][newId]['drawingNode'] = new Node( + {id:newId, + color: { + background: "#eaefef", + border: "495c5e" } - } - } - } + },{},{},this.constants); + this.sectors["active"][newId]['drawingNode'].clusterSize = 2; }; + /** + * This function removes the currently active sector. This is called when we create a new + * active sector. + * + * @param {String} sectorId | Id of the active sector that will be removed + * @private + */ + exports._deleteActiveSector = function(sectorId) { + delete this.sectors["active"][sectorId]; + }; + /** - * This function adds the child node to the parent node, creating a cluster if it is not already. + * This function removes the currently active sector. This is called when we reactivate + * the previously active sector. * - * @param {Node} parentNode | this is the node that will house the child node - * @param {Node} childNode | this node will be deleted from the global this.nodes and stored in the parent node - * @param {Boolean} force | true will only update the remainingEdges at the very end of the clustering, ensuring single level collapse + * @param {String} sectorId | Id of the active sector that will be removed * @private */ - exports._addToCluster = function(parentNode, childNode, force) { - // join child node in the parent node - parentNode.containedNodes[childNode.id] = childNode; - - // manage all the edges connected to the child and parent nodes - for (var i = 0; i < childNode.dynamicEdges.length; i++) { - var edge = childNode.dynamicEdges[i]; - if (edge.toId == parentNode.id || edge.fromId == parentNode.id) { // edge connected to parentNode - this._addToContainedEdges(parentNode,childNode,edge); - } - else { - this._connectEdgeToCluster(parentNode,childNode,edge); - } - } - // a contained node has no dynamic edges. - childNode.dynamicEdges = []; - - // remove circular edges from clusters - this._containCircularEdgesFromNode(parentNode,childNode); - - - // remove the childNode from the global nodes object - delete this.nodes[childNode.id]; - - // update the properties of the child and parent - var massBefore = parentNode.mass; - childNode.clusterSession = this.clusterSession; - parentNode.mass += childNode.mass; - parentNode.clusterSize += childNode.clusterSize; - parentNode.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize); - - // keep track of the clustersessions so we can open the cluster up as it has been formed. - if (parentNode.clusterSessions[parentNode.clusterSessions.length - 1] != this.clusterSession) { - parentNode.clusterSessions.push(this.clusterSession); - } - - // forced clusters only open from screen size and double tap - if (force == true) { - // parentNode.formationScale = Math.pow(1 - (1.0/11.0),this.clusterSession+3); - parentNode.formationScale = 0; - } - else { - parentNode.formationScale = this.scale; // The latest child has been added on this scale - } - - // recalculate the size of the node on the next time the node is rendered - parentNode.clearSizeCache(); - - // set the pop-out scale for the childnode - parentNode.containedNodes[childNode.id].formationScale = parentNode.formationScale; - - // nullify the movement velocity of the child, this is to avoid hectic behaviour - childNode.clearVelocity(); - - // the mass has altered, preservation of energy dictates the velocity to be updated - parentNode.updateVelocity(massBefore); - - // restart the simulation to reorganise all nodes - this.moving = true; - }; + exports._deleteFrozenSector = function(sectorId) { + delete this.sectors["frozen"][sectorId]; + }; /** - * This function will apply the changes made to the remainingEdges during the formation of the clusters. - * This is a seperate function to allow for level-wise collapsing of the node barnesHutTree. - * It has to be called if a level is collapsed. It is called by _formClusters(). + * Freezing an active sector means moving it from the "active" object to the "frozen" object. + * We copy the references, then delete the active entree. + * + * @param sectorId * @private */ - exports._updateDynamicEdges = function() { - for (var i = 0; i < this.nodeIndices.length; i++) { - var node = this.nodes[this.nodeIndices[i]]; - node.dynamicEdgesLength = node.dynamicEdges.length; + exports._freezeSector = function(sectorId) { + // we move the set references from the active to the frozen stack. + this.sectors["frozen"][sectorId] = this.sectors["active"][sectorId]; - // this corrects for multiple edges pointing at the same other node - var correction = 0; - if (node.dynamicEdgesLength > 1) { - for (var j = 0; j < node.dynamicEdgesLength - 1; j++) { - var edgeToId = node.dynamicEdges[j].toId; - var edgeFromId = node.dynamicEdges[j].fromId; - for (var k = j+1; k < node.dynamicEdgesLength; k++) { - if ((node.dynamicEdges[k].toId == edgeToId && node.dynamicEdges[k].fromId == edgeFromId) || - (node.dynamicEdges[k].fromId == edgeToId && node.dynamicEdges[k].toId == edgeFromId)) { - correction += 1; - } - } - } - } - node.dynamicEdgesLength -= correction; - } + // we have moved the sector data into the frozen set, we now remove it from the active set + this._deleteActiveSector(sectorId); }; /** - * This adds an edge from the childNode to the contained edges of the parent node + * This is the reverse operation of _freezeSector. Activating means moving the sector from the "frozen" + * object to the "active" object. * - * @param parentNode | Node object - * @param childNode | Node object - * @param edge | Edge object + * @param sectorId * @private */ - exports._addToContainedEdges = function(parentNode, childNode, edge) { - // create an array object if it does not yet exist for this childNode - if (!(parentNode.containedEdges.hasOwnProperty(childNode.id))) { - parentNode.containedEdges[childNode.id] = [] - } - // add this edge to the list - parentNode.containedEdges[childNode.id].push(edge); - - // remove the edge from the global edges object - delete this.edges[edge.id]; + exports._activateSector = function(sectorId) { + // we move the set references from the frozen to the active stack. + this.sectors["active"][sectorId] = this.sectors["frozen"][sectorId]; - // remove the edge from the parent object - for (var i = 0; i < parentNode.dynamicEdges.length; i++) { - if (parentNode.dynamicEdges[i].id == edge.id) { - parentNode.dynamicEdges.splice(i,1); - break; - } - } + // we have moved the sector data into the active set, we now remove it from the frozen stack + this._deleteFrozenSector(sectorId); }; + /** - * This function connects an edge that was connected to a child node to the parent node. - * It keeps track of which nodes it has been connected to with the originalId array. + * This function merges the data from the currently active sector with a frozen sector. This is used + * in the process of reverting back to the previously active sector. + * The data that is placed in the frozen (the previously active) sector is the node that has been removed from it + * upon the creation of a new active sector. * - * @param {Node} parentNode | Node object - * @param {Node} childNode | Node object - * @param {Edge} edge | Edge object + * @param sectorId * @private */ - exports._connectEdgeToCluster = function(parentNode, childNode, edge) { - // handle circular edges - if (edge.toId == edge.fromId) { - this._addToContainedEdges(parentNode, childNode, edge); - } - else { - if (edge.toId == childNode.id) { // edge connected to other node on the "to" side - edge.originalToId.push(childNode.id); - edge.to = parentNode; - edge.toId = parentNode.id; + exports._mergeThisWithFrozen = function(sectorId) { + // copy all nodes + for (var nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + this.sectors["frozen"][sectorId]["nodes"][nodeId] = this.nodes[nodeId]; } - else { // edge connected to other node with the "from" side + } - edge.originalFromId.push(childNode.id); - edge.from = parentNode; - edge.fromId = parentNode.id; + // copy all edges (if not fully clustered, else there are no edges) + for (var edgeId in this.edges) { + if (this.edges.hasOwnProperty(edgeId)) { + this.sectors["frozen"][sectorId]["edges"][edgeId] = this.edges[edgeId]; } + } - this._addToReroutedEdges(parentNode,childNode,edge); + // merge the nodeIndices + for (var i = 0; i < this.nodeIndices.length; i++) { + this.sectors["frozen"][sectorId]["nodeIndices"].push(this.nodeIndices[i]); } }; /** - * If a node is connected to itself, a circular edge is drawn. When clustering we want to contain - * these edges inside of the cluster. + * This clusters the sector to one cluster. It was a single cluster before this process started so + * we revert to that state. The clusterToFit function with a maximum size of 1 node does this. * - * @param parentNode - * @param childNode * @private */ - exports._containCircularEdgesFromNode = function(parentNode, childNode) { - // manage all the edges connected to the child and parent nodes - for (var i = 0; i < parentNode.dynamicEdges.length; i++) { - var edge = parentNode.dynamicEdges[i]; - // handle circular edges - if (edge.toId == edge.fromId) { - this._addToContainedEdges(parentNode, childNode, edge); - } - } + exports._collapseThisToSingleCluster = function() { + this.clusterToFit(1,false); }; /** - * This adds an edge from the childNode to the rerouted edges of the parent node + * We create a new active sector from the node that we want to open. * - * @param parentNode | Node object - * @param childNode | Node object - * @param edge | Edge object + * @param node * @private */ - exports._addToReroutedEdges = function(parentNode, childNode, edge) { - // create an array object if it does not yet exist for this childNode - // we store the edge in the rerouted edges so we can restore it when the cluster pops open - if (!(parentNode.reroutedEdges.hasOwnProperty(childNode.id))) { - parentNode.reroutedEdges[childNode.id] = []; - } - parentNode.reroutedEdges[childNode.id].push(edge); + exports._addSector = function(node) { + // this is the currently active sector + var sector = this._sector(); - // this edge becomes part of the dynamicEdges of the cluster node - parentNode.dynamicEdges.push(edge); - }; + // // this should allow me to select nodes from a frozen set. + // if (this.sectors['active'][sector]["nodes"].hasOwnProperty(node.id)) { + // console.log("the node is part of the active sector"); + // } + // else { + // console.log("I dont know what the fuck happened!!"); + // } + // when we switch to a new sector, we remove the node that will be expanded from the current nodes list. + delete this.nodes[node.id]; + var unqiueIdentifier = util.randomUUID(); - /** - * This function connects an edge that was connected to a cluster node back to the child node. - * - * @param parentNode | Node object - * @param childNode | Node object - * @private - */ - exports._connectEdgeBackToChild = function(parentNode, childNode) { - if (parentNode.reroutedEdges.hasOwnProperty(childNode.id)) { - for (var i = 0; i < parentNode.reroutedEdges[childNode.id].length; i++) { - var edge = parentNode.reroutedEdges[childNode.id][i]; - if (edge.originalFromId[edge.originalFromId.length-1] == childNode.id) { - edge.originalFromId.pop(); - edge.fromId = childNode.id; - edge.from = childNode; - } - else { - edge.originalToId.pop(); - edge.toId = childNode.id; - edge.to = childNode; - } + // we fully freeze the currently active sector + this._freezeSector(sector); - // append this edge to the list of edges connecting to the childnode - childNode.dynamicEdges.push(edge); + // we create a new active sector. This sector has the Id of the node to ensure uniqueness + this._createNewSector(unqiueIdentifier); - // remove the edge from the parent object - for (var j = 0; j < parentNode.dynamicEdges.length; j++) { - if (parentNode.dynamicEdges[j].id == edge.id) { - parentNode.dynamicEdges.splice(j,1); - break; - } - } - } - // remove the entry from the rerouted edges - delete parentNode.reroutedEdges[childNode.id]; - } + // we add the active sector to the sectors array to be able to revert these steps later on + this._setActiveSector(unqiueIdentifier); + + // we redirect the global references to the new sector's references. this._sector() now returns unqiueIdentifier + this._switchToSector(this._sector()); + + // finally we add the node we removed from our previous active sector to the new active sector + this.nodes[node.id] = node; }; /** - * When loops are clustered, an edge can be both in the rerouted array and the contained array. - * This function is called last to verify that all edges in dynamicEdges are in fact connected to the - * parentNode + * We close the sector that is currently open and revert back to the one before. + * If the active sector is the "default" sector, nothing happens. * - * @param parentNode | Node object * @private */ - exports._validateEdges = function(parentNode) { - for (var i = 0; i < parentNode.dynamicEdges.length; i++) { - var edge = parentNode.dynamicEdges[i]; - if (parentNode.id != edge.toId && parentNode.id != edge.fromId) { - parentNode.dynamicEdges.splice(i,1); - } - } - }; + exports._collapseSector = function() { + // the currently active sector + var sector = this._sector(); + // we cannot collapse the default sector + if (sector != "default") { + if ((this.nodeIndices.length == 1) || + (this.sectors["active"][sector]["drawingNode"].width*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) || + (this.sectors["active"][sector]["drawingNode"].height*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) { + var previousSector = this._previousSector(); - /** - * This function released the contained edges back into the global domain and puts them back into the - * dynamic edges of both parent and child. - * - * @param {Node} parentNode | - * @param {Node} childNode | - * @private - */ - exports._releaseContainedEdges = function(parentNode, childNode) { - for (var i = 0; i < parentNode.containedEdges[childNode.id].length; i++) { - var edge = parentNode.containedEdges[childNode.id][i]; + // we collapse the sector back to a single cluster + this._collapseThisToSingleCluster(); - // put the edge back in the global edges object - this.edges[edge.id] = edge; + // we move the remaining nodes, edges and nodeIndices to the previous sector. + // This previous sector is the one we will reactivate + this._mergeThisWithFrozen(previousSector); - // put the edge back in the dynamic edges of the child and parent - childNode.dynamicEdges.push(edge); - parentNode.dynamicEdges.push(edge); - } - // remove the entry from the contained edges - delete parentNode.containedEdges[childNode.id]; + // the previously active (frozen) sector now has all the data from the currently active sector. + // we can now delete the active sector. + this._deleteActiveSector(sector); - }; + // we activate the previously active (and currently frozen) sector. + this._activateSector(previousSector); + // we load the references from the newly active sector into the global references + this._switchToSector(previousSector); + // we forget the previously active sector because we reverted to the one before + this._forgetLastSector(); + // finally, we update the node index list. + this._updateNodeIndexList(); - // ------------------- UTILITY FUNCTIONS ---------------------------- // + // we refresh the list with calulation nodes and calculation node indices. + this._updateCalculationNodes(); + } + } + }; /** - * This updates the node labels for all nodes (for debugging purposes) + * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation(). + * + * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors + * | we dont pass the function itself because then the "this" is the window object + * | instead of the Network object + * @param {*} [argument] | Optional: arguments to pass to the runFunction + * @private */ - exports.updateLabels = function() { - var nodeId; - // update node labels - for (nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - var node = this.nodes[nodeId]; - if (node.clusterSize > 1) { - node.label = "[".concat(String(node.clusterSize),"]"); + exports._doInAllActiveSectors = function(runFunction,argument) { + if (argument === undefined) { + for (var sector in this.sectors["active"]) { + if (this.sectors["active"].hasOwnProperty(sector)) { + // switch the global references to those of this sector + this._switchToActiveSector(sector); + this[runFunction](); } } } - - // update node labels - for (nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - node = this.nodes[nodeId]; - if (node.clusterSize == 1) { - if (node.originalLabel !== undefined) { - node.label = node.originalLabel; + else { + for (var sector in this.sectors["active"]) { + if (this.sectors["active"].hasOwnProperty(sector)) { + // switch the global references to those of this sector + this._switchToActiveSector(sector); + var args = Array.prototype.splice.call(arguments, 1); + if (args.length > 1) { + this[runFunction](args[0],args[1]); } else { - node.label = String(node.id); + this[runFunction](argument); } } } } - - // /* Debug Override */ - // for (nodeId in this.nodes) { - // if (this.nodes.hasOwnProperty(nodeId)) { - // node = this.nodes[nodeId]; - // node.label = String(node.level); - // } - // } - + // we revert the global references back to our active sector + this._loadLatestSector(); }; /** - * We want to keep the cluster level distribution rather small. This means we do not want unclustered nodes - * if the rest of the nodes are already a few cluster levels in. - * To fix this we use this function. It determines the min and max cluster level and sends nodes that have not - * clustered enough to the clusterToSmallestNeighbours function. + * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation(). + * + * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors + * | we dont pass the function itself because then the "this" is the window object + * | instead of the Network object + * @param {*} [argument] | Optional: arguments to pass to the runFunction + * @private */ - exports.normalizeClusterLevels = function() { - var maxLevel = 0; - var minLevel = 1e9; - var clusterLevel = 0; - var nodeId; - - // we loop over all nodes in the list - for (nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - clusterLevel = this.nodes[nodeId].clusterSessions.length; - if (maxLevel < clusterLevel) {maxLevel = clusterLevel;} - if (minLevel > clusterLevel) {minLevel = clusterLevel;} - } + exports._doInSupportSector = function(runFunction,argument) { + if (argument === undefined) { + this._switchToSupportSector(); + this[runFunction](); } - - if (maxLevel - minLevel > this.constants.clustering.clusterLevelDifference) { - var amountOfNodes = this.nodeIndices.length; - var targetLevel = maxLevel - this.constants.clustering.clusterLevelDifference; - // we loop over all nodes in the list - for (nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - if (this.nodes[nodeId].clusterSessions.length < targetLevel) { - this._clusterToSmallestNeighbour(this.nodes[nodeId]); - } - } + else { + this._switchToSupportSector(); + var args = Array.prototype.splice.call(arguments, 1); + if (args.length > 1) { + this[runFunction](args[0],args[1]); } - this._updateNodeIndexList(); - this._updateDynamicEdges(); - // if a cluster was formed, we increase the clusterSession - if (this.nodeIndices.length != amountOfNodes) { - this.clusterSession += 1; + else { + this[runFunction](argument); } } + // we revert the global references back to our active sector + this._loadLatestSector(); }; - /** - * This function determines if the cluster we want to decluster is in the active area - * this means around the zoom center + * This runs a function in all frozen sectors. This is used in the _redraw(). * - * @param {Node} node - * @returns {boolean} + * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors + * | we don't pass the function itself because then the "this" is the window object + * | instead of the Network object + * @param {*} [argument] | Optional: arguments to pass to the runFunction * @private */ - exports._nodeInActiveArea = function(node) { - return ( - Math.abs(node.x - this.areaCenter.x) <= this.constants.clustering.activeAreaBoxSize/this.scale - && - Math.abs(node.y - this.areaCenter.y) <= this.constants.clustering.activeAreaBoxSize/this.scale - ) + exports._doInAllFrozenSectors = function(runFunction,argument) { + if (argument === undefined) { + for (var sector in this.sectors["frozen"]) { + if (this.sectors["frozen"].hasOwnProperty(sector)) { + // switch the global references to those of this sector + this._switchToFrozenSector(sector); + this[runFunction](); + } + } + } + else { + for (var sector in this.sectors["frozen"]) { + if (this.sectors["frozen"].hasOwnProperty(sector)) { + // switch the global references to those of this sector + this._switchToFrozenSector(sector); + var args = Array.prototype.splice.call(arguments, 1); + if (args.length > 1) { + this[runFunction](args[0],args[1]); + } + else { + this[runFunction](argument); + } + } + } + } + this._loadLatestSector(); }; /** - * This is an adaptation of the original repositioning function. This is called if the system is clustered initially - * It puts large clusters away from the center and randomizes the order. + * This runs a function in all sectors. This is used in the _redraw(). * + * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors + * | we don't pass the function itself because then the "this" is the window object + * | instead of the Network object + * @param {*} [argument] | Optional: arguments to pass to the runFunction + * @private */ - exports.repositionNodes = function() { - for (var i = 0; i < this.nodeIndices.length; i++) { - var node = this.nodes[this.nodeIndices[i]]; - if ((node.xFixed == false || node.yFixed == false)) { - var radius = 10 * 0.1*this.nodeIndices.length * Math.min(100,node.mass); - var angle = 2 * Math.PI * Math.random(); - if (node.xFixed == false) {node.x = radius * Math.cos(angle);} - if (node.yFixed == false) {node.y = radius * Math.sin(angle);} - this._repositionBezierNodes(node); + exports._doInAllSectors = function(runFunction,argument) { + var args = Array.prototype.splice.call(arguments, 1); + if (argument === undefined) { + this._doInAllActiveSectors(runFunction); + this._doInAllFrozenSectors(runFunction); + } + else { + if (args.length > 1) { + this._doInAllActiveSectors(runFunction,args[0],args[1]); + this._doInAllFrozenSectors(runFunction,args[0],args[1]); + } + else { + this._doInAllActiveSectors(runFunction,argument); + this._doInAllFrozenSectors(runFunction,argument); } } }; /** - * We determine how many connections denote an important hub. - * We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%) + * This clears the nodeIndices list. We cannot use this.nodeIndices = [] because we would break the link with the + * active sector. Thus we clear the nodeIndices in the active sector, then reconnect the this.nodeIndices to it. * * @private */ - exports._getHubSize = function() { - var average = 0; - var averageSquared = 0; - var hubCounter = 0; - var largestHub = 0; - - for (var i = 0; i < this.nodeIndices.length; i++) { - - var node = this.nodes[this.nodeIndices[i]]; - if (node.dynamicEdgesLength > largestHub) { - largestHub = node.dynamicEdgesLength; - } - average += node.dynamicEdgesLength; - averageSquared += Math.pow(node.dynamicEdgesLength,2); - hubCounter += 1; - } - average = average / hubCounter; - averageSquared = averageSquared / hubCounter; - - var variance = averageSquared - Math.pow(average,2); - - var standardDeviation = Math.sqrt(variance); - - this.hubThreshold = Math.floor(average + 2*standardDeviation); - - // always have at least one to cluster - if (this.hubThreshold > largestHub) { - this.hubThreshold = largestHub; - } - - // console.log("average",average,"averageSQ",averageSquared,"var",variance,"std",standardDeviation); - // console.log("hubThreshold:",this.hubThreshold); - }; + exports._clearNodeIndexList = function() { + var sector = this._sector(); + this.sectors["active"][sector]["nodeIndices"] = []; + this.nodeIndices = this.sectors["active"][sector]["nodeIndices"]; + }; /** - * We reduce the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods - * with this amount we can cluster specifically on these chains. + * Draw the encompassing sector node * - * @param {Number} fraction | between 0 and 1, the percentage of chains to reduce + * @param ctx + * @param sectorType * @private */ - exports._reduceAmountOfChains = function(fraction) { - this.hubThreshold = 2; - var reduceAmount = Math.floor(this.nodeIndices.length * fraction); - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) { - if (reduceAmount > 0) { - this._formClusterFromHub(this.nodes[nodeId],true,true,1); - reduceAmount -= 1; + exports._drawSectorNodes = function(ctx,sectorType) { + var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node; + for (var sector in this.sectors[sectorType]) { + if (this.sectors[sectorType].hasOwnProperty(sector)) { + if (this.sectors[sectorType][sector]["drawingNode"] !== undefined) { + + this._switchToSector(sector,sectorType); + + minY = 1e9; maxY = -1e9; minX = 1e9; maxX = -1e9; + for (var nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + node = this.nodes[nodeId]; + node.resize(ctx); + if (minX > node.x - 0.5 * node.width) {minX = node.x - 0.5 * node.width;} + if (maxX < node.x + 0.5 * node.width) {maxX = node.x + 0.5 * node.width;} + if (minY > node.y - 0.5 * node.height) {minY = node.y - 0.5 * node.height;} + if (maxY < node.y + 0.5 * node.height) {maxY = node.y + 0.5 * node.height;} + } } + node = this.sectors[sectorType][sector]["drawingNode"]; + node.x = 0.5 * (maxX + minX); + node.y = 0.5 * (maxY + minY); + node.width = 2 * (node.x - minX); + node.height = 2 * (node.y - minY); + node.radius = Math.sqrt(Math.pow(0.5*node.width,2) + Math.pow(0.5*node.height,2)); + node.setScale(this.scale); + node._drawCircle(ctx); } } } }; - /** - * We get the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods - * with this amount we can cluster specifically on these chains. - * - * @private - */ - exports._getChainFraction = function() { - var chains = 0; - var total = 0; - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) { - chains += 1; - } - total += 1; - } - } - return chains/total; + exports._drawAllSectorNodes = function(ctx) { + this._drawSectorNodes(ctx,"frozen"); + this._drawSectorNodes(ctx,"active"); + this._loadLatestSector(); }; @@ -22611,4489 +22602,4759 @@ return /******/ (function(modules) { // webpackBootstrap /* 44 */ /***/ function(module, exports, __webpack_require__) { - var util = __webpack_require__(1); + var Node = __webpack_require__(30); /** - * Creation of the SectorMixin var. + * This function can be called from the _doInAllSectors function * - * This contains all the functions the Network object can use to employ the sector system. - * The sector system is always used by Network, though the benefits only apply to the use of clustering. - * If clustering is not used, there is no overhead except for a duplicate object with references to nodes and edges. + * @param object + * @param overlappingNodes + * @private */ + exports._getNodesOverlappingWith = function(object, overlappingNodes) { + var nodes = this.nodes; + for (var nodeId in nodes) { + if (nodes.hasOwnProperty(nodeId)) { + if (nodes[nodeId].isOverlappingWith(object)) { + overlappingNodes.push(nodeId); + } + } + } + }; /** - * This function is only called by the setData function of the Network object. - * This loads the global references into the active sector. This initializes the sector. - * + * retrieve all nodes overlapping with given object + * @param {Object} object An object with parameters left, top, right, bottom + * @return {Number[]} An array with id's of the overlapping nodes * @private */ - exports._putDataInSector = function() { - this.sectors["active"][this._sector()].nodes = this.nodes; - this.sectors["active"][this._sector()].edges = this.edges; - this.sectors["active"][this._sector()].nodeIndices = this.nodeIndices; + exports._getAllNodesOverlappingWith = function (object) { + var overlappingNodes = []; + this._doInAllActiveSectors("_getNodesOverlappingWith",object,overlappingNodes); + return overlappingNodes; }; /** - * /** - * This function sets the global references to nodes, edges and nodeIndices back to - * those of the supplied (active) sector. If a type is defined, do the specific type + * Return a position object in canvasspace from a single point in screenspace * - * @param {String} sectorId - * @param {String} [sectorType] | "active" or "frozen" + * @param pointer + * @returns {{left: number, top: number, right: number, bottom: number}} * @private */ - exports._switchToSector = function(sectorId, sectorType) { - if (sectorType === undefined || sectorType == "active") { - this._switchToActiveSector(sectorId); - } - else { - this._switchToFrozenSector(sectorId); - } + exports._pointerToPositionObject = function(pointer) { + var x = this._XconvertDOMtoCanvas(pointer.x); + var y = this._YconvertDOMtoCanvas(pointer.y); + + return { + left: x, + top: y, + right: x, + bottom: y + }; }; /** - * This function sets the global references to nodes, edges and nodeIndices back to - * those of the supplied active sector. + * Get the top node at the a specific point (like a click) * - * @param sectorId + * @param {{x: Number, y: Number}} pointer + * @return {Node | null} node * @private */ - exports._switchToActiveSector = function(sectorId) { - this.nodeIndices = this.sectors["active"][sectorId]["nodeIndices"]; - this.nodes = this.sectors["active"][sectorId]["nodes"]; - this.edges = this.sectors["active"][sectorId]["edges"]; + exports._getNodeAt = function (pointer) { + // we first check if this is an navigation controls element + var positionObject = this._pointerToPositionObject(pointer); + var overlappingNodes = this._getAllNodesOverlappingWith(positionObject); + + // if there are overlapping nodes, select the last one, this is the + // one which is drawn on top of the others + if (overlappingNodes.length > 0) { + return this.nodes[overlappingNodes[overlappingNodes.length - 1]]; + } + else { + return null; + } }; /** - * This function sets the global references to nodes, edges and nodeIndices back to - * those of the supplied active sector. - * + * retrieve all edges overlapping with given object, selector is around center + * @param {Object} object An object with parameters left, top, right, bottom + * @return {Number[]} An array with id's of the overlapping nodes * @private */ - exports._switchToSupportSector = function() { - this.nodeIndices = this.sectors["support"]["nodeIndices"]; - this.nodes = this.sectors["support"]["nodes"]; - this.edges = this.sectors["support"]["edges"]; + exports._getEdgesOverlappingWith = function (object, overlappingEdges) { + var edges = this.edges; + for (var edgeId in edges) { + if (edges.hasOwnProperty(edgeId)) { + if (edges[edgeId].isOverlappingWith(object)) { + overlappingEdges.push(edgeId); + } + } + } }; /** - * This function sets the global references to nodes, edges and nodeIndices back to - * those of the supplied frozen sector. - * - * @param sectorId + * retrieve all nodes overlapping with given object + * @param {Object} object An object with parameters left, top, right, bottom + * @return {Number[]} An array with id's of the overlapping nodes * @private */ - exports._switchToFrozenSector = function(sectorId) { - this.nodeIndices = this.sectors["frozen"][sectorId]["nodeIndices"]; - this.nodes = this.sectors["frozen"][sectorId]["nodes"]; - this.edges = this.sectors["frozen"][sectorId]["edges"]; + exports._getAllEdgesOverlappingWith = function (object) { + var overlappingEdges = []; + this._doInAllActiveSectors("_getEdgesOverlappingWith",object,overlappingEdges); + return overlappingEdges; }; - /** - * This function sets the global references to nodes, edges and nodeIndices back to - * those of the currently active sector. + * Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call + * _getNodeAt and _getEdgesAt, then priortize the selection to user preferences. * + * @param pointer + * @returns {null} * @private */ - exports._loadLatestSector = function() { - this._switchToSector(this._sector()); + exports._getEdgeAt = function(pointer) { + var positionObject = this._pointerToPositionObject(pointer); + var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject); + + if (overlappingEdges.length > 0) { + return this.edges[overlappingEdges[overlappingEdges.length - 1]]; + } + else { + return null; + } }; /** - * This function returns the currently active sector Id + * Add object to the selection array. * - * @returns {String} + * @param obj * @private */ - exports._sector = function() { - return this.activeSector[this.activeSector.length-1]; + exports._addToSelection = function(obj) { + if (obj instanceof Node) { + this.selectionObj.nodes[obj.id] = obj; + } + else { + this.selectionObj.edges[obj.id] = obj; + } }; - /** - * This function returns the previously active sector Id + * Add object to the selection array. * - * @returns {String} + * @param obj * @private */ - exports._previousSector = function() { - if (this.activeSector.length > 1) { - return this.activeSector[this.activeSector.length-2]; + exports._addToHover = function(obj) { + if (obj instanceof Node) { + this.hoverObj.nodes[obj.id] = obj; } else { - throw new TypeError('there are not enough sectors in the this.activeSector array.'); + this.hoverObj.edges[obj.id] = obj; } }; /** - * We add the active sector at the end of the this.activeSector array - * This ensures it is the currently active sector returned by _sector() and it reaches the top - * of the activeSector stack. When we reverse our steps we move from the end to the beginning of this stack. + * Remove a single option from selection. * - * @param newId + * @param {Object} obj * @private */ - exports._setActiveSector = function(newId) { - this.activeSector.push(newId); - }; - + exports._removeFromSelection = function(obj) { + if (obj instanceof Node) { + delete this.selectionObj.nodes[obj.id]; + } + else { + delete this.selectionObj.edges[obj.id]; + } + }; /** - * We remove the currently active sector id from the active sector stack. This happens when - * we reactivate the previously active sector + * Unselect all. The selectionObj is useful for this. * + * @param {Boolean} [doNotTrigger] | ignore trigger * @private */ - exports._forgetLastSector = function() { - this.activeSector.pop(); - }; + exports._unselectAll = function(doNotTrigger) { + if (doNotTrigger === undefined) { + doNotTrigger = false; + } + for(var nodeId in this.selectionObj.nodes) { + if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { + this.selectionObj.nodes[nodeId].unselect(); + } + } + for(var edgeId in this.selectionObj.edges) { + if(this.selectionObj.edges.hasOwnProperty(edgeId)) { + this.selectionObj.edges[edgeId].unselect(); + } + } + + this.selectionObj = {nodes:{},edges:{}}; + if (doNotTrigger == false) { + this.emit('select', this.getSelection()); + } + }; /** - * This function creates a new active sector with the supplied newId. This newId - * is the expanding node id. + * Unselect all clusters. The selectionObj is useful for this. * - * @param {String} newId | Id of the new active sector + * @param {Boolean} [doNotTrigger] | ignore trigger * @private */ - exports._createNewSector = function(newId) { - // create the new sector - this.sectors["active"][newId] = {"nodes":{}, - "edges":{}, - "nodeIndices":[], - "formationScale": this.scale, - "drawingNode": undefined}; + exports._unselectClusters = function(doNotTrigger) { + if (doNotTrigger === undefined) { + doNotTrigger = false; + } - // create the new sector render node. This gives visual feedback that you are in a new sector. - this.sectors["active"][newId]['drawingNode'] = new Node( - {id:newId, - color: { - background: "#eaefef", - border: "495c5e" - } - },{},{},this.constants); - this.sectors["active"][newId]['drawingNode'].clusterSize = 2; + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + if (this.selectionObj.nodes[nodeId].clusterSize > 1) { + this.selectionObj.nodes[nodeId].unselect(); + this._removeFromSelection(this.selectionObj.nodes[nodeId]); + } + } + } + + if (doNotTrigger == false) { + this.emit('select', this.getSelection()); + } }; /** - * This function removes the currently active sector. This is called when we create a new - * active sector. + * return the number of selected nodes * - * @param {String} sectorId | Id of the active sector that will be removed + * @returns {number} * @private */ - exports._deleteActiveSector = function(sectorId) { - delete this.sectors["active"][sectorId]; + exports._getSelectedNodeCount = function() { + var count = 0; + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + count += 1; + } + } + return count; }; - /** - * This function removes the currently active sector. This is called when we reactivate - * the previously active sector. + * return the selected node * - * @param {String} sectorId | Id of the active sector that will be removed + * @returns {number} * @private */ - exports._deleteFrozenSector = function(sectorId) { - delete this.sectors["frozen"][sectorId]; + exports._getSelectedNode = function() { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + return this.selectionObj.nodes[nodeId]; + } + } + return null; }; - /** - * Freezing an active sector means moving it from the "active" object to the "frozen" object. - * We copy the references, then delete the active entree. + * return the selected edge * - * @param sectorId + * @returns {number} * @private */ - exports._freezeSector = function(sectorId) { - // we move the set references from the active to the frozen stack. - this.sectors["frozen"][sectorId] = this.sectors["active"][sectorId]; - - // we have moved the sector data into the frozen set, we now remove it from the active set - this._deleteActiveSector(sectorId); + exports._getSelectedEdge = function() { + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + return this.selectionObj.edges[edgeId]; + } + } + return null; }; /** - * This is the reverse operation of _freezeSector. Activating means moving the sector from the "frozen" - * object to the "active" object. + * return the number of selected edges * - * @param sectorId + * @returns {number} * @private */ - exports._activateSector = function(sectorId) { - // we move the set references from the frozen to the active stack. - this.sectors["active"][sectorId] = this.sectors["frozen"][sectorId]; - - // we have moved the sector data into the active set, we now remove it from the frozen stack - this._deleteFrozenSector(sectorId); + exports._getSelectedEdgeCount = function() { + var count = 0; + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + count += 1; + } + } + return count; }; /** - * This function merges the data from the currently active sector with a frozen sector. This is used - * in the process of reverting back to the previously active sector. - * The data that is placed in the frozen (the previously active) sector is the node that has been removed from it - * upon the creation of a new active sector. + * return the number of selected objects. * - * @param sectorId + * @returns {number} * @private */ - exports._mergeThisWithFrozen = function(sectorId) { - // copy all nodes - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - this.sectors["frozen"][sectorId]["nodes"][nodeId] = this.nodes[nodeId]; + exports._getSelectedObjectCount = function() { + var count = 0; + for(var nodeId in this.selectionObj.nodes) { + if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { + count += 1; } } - - // copy all edges (if not fully clustered, else there are no edges) - for (var edgeId in this.edges) { - if (this.edges.hasOwnProperty(edgeId)) { - this.sectors["frozen"][sectorId]["edges"][edgeId] = this.edges[edgeId]; + for(var edgeId in this.selectionObj.edges) { + if(this.selectionObj.edges.hasOwnProperty(edgeId)) { + count += 1; } } + return count; + }; - // merge the nodeIndices - for (var i = 0; i < this.nodeIndices.length; i++) { - this.sectors["frozen"][sectorId]["nodeIndices"].push(this.nodeIndices[i]); + /** + * Check if anything is selected + * + * @returns {boolean} + * @private + */ + exports._selectionIsEmpty = function() { + for(var nodeId in this.selectionObj.nodes) { + if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { + return false; + } + } + for(var edgeId in this.selectionObj.edges) { + if(this.selectionObj.edges.hasOwnProperty(edgeId)) { + return false; + } } + return true; }; /** - * This clusters the sector to one cluster. It was a single cluster before this process started so - * we revert to that state. The clusterToFit function with a maximum size of 1 node does this. + * check if one of the selected nodes is a cluster. * + * @returns {boolean} * @private */ - exports._collapseThisToSingleCluster = function() { - this.clusterToFit(1,false); + exports._clusterInSelection = function() { + for(var nodeId in this.selectionObj.nodes) { + if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { + if (this.selectionObj.nodes[nodeId].clusterSize > 1) { + return true; + } + } + } + return false; }; + /** + * select the edges connected to the node that is being selected + * + * @param {Node} node + * @private + */ + exports._selectConnectedEdges = function(node) { + for (var i = 0; i < node.dynamicEdges.length; i++) { + var edge = node.dynamicEdges[i]; + edge.select(); + this._addToSelection(edge); + } + }; /** - * We create a new active sector from the node that we want to open. + * select the edges connected to the node that is being selected * - * @param node + * @param {Node} node * @private */ - exports._addSector = function(node) { - // this is the currently active sector - var sector = this._sector(); + exports._hoverConnectedEdges = function(node) { + for (var i = 0; i < node.dynamicEdges.length; i++) { + var edge = node.dynamicEdges[i]; + edge.hover = true; + this._addToHover(edge); + } + }; - // // this should allow me to select nodes from a frozen set. - // if (this.sectors['active'][sector]["nodes"].hasOwnProperty(node.id)) { - // console.log("the node is part of the active sector"); - // } - // else { - // console.log("I dont know what the fuck happened!!"); - // } - // when we switch to a new sector, we remove the node that will be expanded from the current nodes list. - delete this.nodes[node.id]; + /** + * unselect the edges connected to the node that is being selected + * + * @param {Node} node + * @private + */ + exports._unselectConnectedEdges = function(node) { + for (var i = 0; i < node.dynamicEdges.length; i++) { + var edge = node.dynamicEdges[i]; + edge.unselect(); + this._removeFromSelection(edge); + } + }; - var unqiueIdentifier = util.randomUUID(); - // we fully freeze the currently active sector - this._freezeSector(sector); - - // we create a new active sector. This sector has the Id of the node to ensure uniqueness - this._createNewSector(unqiueIdentifier); - - // we add the active sector to the sectors array to be able to revert these steps later on - this._setActiveSector(unqiueIdentifier); - - // we redirect the global references to the new sector's references. this._sector() now returns unqiueIdentifier - this._switchToSector(this._sector()); - - // finally we add the node we removed from our previous active sector to the new active sector - this.nodes[node.id] = node; - }; /** - * We close the sector that is currently open and revert back to the one before. - * If the active sector is the "default" sector, nothing happens. + * This is called when someone clicks on a node. either select or deselect it. + * If there is an existing selection and we don't want to append to it, clear the existing selection * + * @param {Node || Edge} object + * @param {Boolean} append + * @param {Boolean} [doNotTrigger] | ignore trigger * @private */ - exports._collapseSector = function() { - // the currently active sector - var sector = this._sector(); - - // we cannot collapse the default sector - if (sector != "default") { - if ((this.nodeIndices.length == 1) || - (this.sectors["active"][sector]["drawingNode"].width*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) || - (this.sectors["active"][sector]["drawingNode"].height*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) { - var previousSector = this._previousSector(); - - // we collapse the sector back to a single cluster - this._collapseThisToSingleCluster(); - - // we move the remaining nodes, edges and nodeIndices to the previous sector. - // This previous sector is the one we will reactivate - this._mergeThisWithFrozen(previousSector); - - // the previously active (frozen) sector now has all the data from the currently active sector. - // we can now delete the active sector. - this._deleteActiveSector(sector); + exports._selectObject = function(object, append, doNotTrigger, highlightEdges) { + if (doNotTrigger === undefined) { + doNotTrigger = false; + } + if (highlightEdges === undefined) { + highlightEdges = true; + } - // we activate the previously active (and currently frozen) sector. - this._activateSector(previousSector); + if (this._selectionIsEmpty() == false && append == false && this.forceAppendSelection == false) { + this._unselectAll(true); + } - // we load the references from the newly active sector into the global references - this._switchToSector(previousSector); + if (object.selected == false) { + object.select(); + this._addToSelection(object); + if (object instanceof Node && this.blockConnectingEdgeSelection == false && highlightEdges == true) { + this._selectConnectedEdges(object); + } + } + else { + object.unselect(); + this._removeFromSelection(object); + } - // we forget the previously active sector because we reverted to the one before - this._forgetLastSector(); + if (doNotTrigger == false) { + this.emit('select', this.getSelection()); + } + }; - // finally, we update the node index list. - this._updateNodeIndexList(); - // we refresh the list with calulation nodes and calculation node indices. - this._updateCalculationNodes(); - } + /** + * This is called when someone clicks on a node. either select or deselect it. + * If there is an existing selection and we don't want to append to it, clear the existing selection + * + * @param {Node || Edge} object + * @private + */ + exports._blurObject = function(object) { + if (object.hover == true) { + object.hover = false; + this.emit("blurNode",{node:object.id}); } }; - /** - * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation(). + * This is called when someone clicks on a node. either select or deselect it. + * If there is an existing selection and we don't want to append to it, clear the existing selection * - * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors - * | we dont pass the function itself because then the "this" is the window object - * | instead of the Network object - * @param {*} [argument] | Optional: arguments to pass to the runFunction + * @param {Node || Edge} object * @private */ - exports._doInAllActiveSectors = function(runFunction,argument) { - if (argument === undefined) { - for (var sector in this.sectors["active"]) { - if (this.sectors["active"].hasOwnProperty(sector)) { - // switch the global references to those of this sector - this._switchToActiveSector(sector); - this[runFunction](); - } + exports._hoverObject = function(object) { + if (object.hover == false) { + object.hover = true; + this._addToHover(object); + if (object instanceof Node) { + this.emit("hoverNode",{node:object.id}); } } - else { - for (var sector in this.sectors["active"]) { - if (this.sectors["active"].hasOwnProperty(sector)) { - // switch the global references to those of this sector - this._switchToActiveSector(sector); - var args = Array.prototype.splice.call(arguments, 1); - if (args.length > 1) { - this[runFunction](args[0],args[1]); - } - else { - this[runFunction](argument); - } - } - } + if (object instanceof Node) { + this._hoverConnectedEdges(object); } - // we revert the global references back to our active sector - this._loadLatestSector(); }; /** - * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation(). + * handles the selection part of the touch, only for navigation controls elements; + * Touch is triggered before tap, also before hold. Hold triggers after a while. + * This is the most responsive solution * - * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors - * | we dont pass the function itself because then the "this" is the window object - * | instead of the Network object - * @param {*} [argument] | Optional: arguments to pass to the runFunction + * @param {Object} pointer * @private */ - exports._doInSupportSector = function(runFunction,argument) { - if (argument === undefined) { - this._switchToSupportSector(); - this[runFunction](); + exports._handleTouch = function(pointer) { + }; + + + /** + * handles the selection part of the tap; + * + * @param {Object} pointer + * @private + */ + exports._handleTap = function(pointer) { + var node = this._getNodeAt(pointer); + if (node != null) { + this._selectObject(node,false); } else { - this._switchToSupportSector(); - var args = Array.prototype.splice.call(arguments, 1); - if (args.length > 1) { - this[runFunction](args[0],args[1]); + var edge = this._getEdgeAt(pointer); + if (edge != null) { + this._selectObject(edge,false); } else { - this[runFunction](argument); + this._unselectAll(); } } - // we revert the global references back to our active sector - this._loadLatestSector(); + this.emit("click", this.getSelection()); + this._redraw(); }; /** - * This runs a function in all frozen sectors. This is used in the _redraw(). + * handles the selection part of the double tap and opens a cluster if needed * - * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors - * | we don't pass the function itself because then the "this" is the window object - * | instead of the Network object - * @param {*} [argument] | Optional: arguments to pass to the runFunction + * @param {Object} pointer * @private */ - exports._doInAllFrozenSectors = function(runFunction,argument) { - if (argument === undefined) { - for (var sector in this.sectors["frozen"]) { - if (this.sectors["frozen"].hasOwnProperty(sector)) { - // switch the global references to those of this sector - this._switchToFrozenSector(sector); - this[runFunction](); - } - } - } - else { - for (var sector in this.sectors["frozen"]) { - if (this.sectors["frozen"].hasOwnProperty(sector)) { - // switch the global references to those of this sector - this._switchToFrozenSector(sector); - var args = Array.prototype.splice.call(arguments, 1); - if (args.length > 1) { - this[runFunction](args[0],args[1]); - } - else { - this[runFunction](argument); - } - } - } + exports._handleDoubleTap = function(pointer) { + var node = this._getNodeAt(pointer); + if (node != null && node !== undefined) { + // we reset the areaCenter here so the opening of the node will occur + this.areaCenter = {"x" : this._XconvertDOMtoCanvas(pointer.x), + "y" : this._YconvertDOMtoCanvas(pointer.y)}; + this.openCluster(node); } - this._loadLatestSector(); + this.emit("doubleClick", this.getSelection()); }; /** - * This runs a function in all sectors. This is used in the _redraw(). + * Handle the onHold selection part * - * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors - * | we don't pass the function itself because then the "this" is the window object - * | instead of the Network object - * @param {*} [argument] | Optional: arguments to pass to the runFunction + * @param pointer * @private */ - exports._doInAllSectors = function(runFunction,argument) { - var args = Array.prototype.splice.call(arguments, 1); - if (argument === undefined) { - this._doInAllActiveSectors(runFunction); - this._doInAllFrozenSectors(runFunction); + exports._handleOnHold = function(pointer) { + var node = this._getNodeAt(pointer); + if (node != null) { + this._selectObject(node,true); } else { - if (args.length > 1) { - this._doInAllActiveSectors(runFunction,args[0],args[1]); - this._doInAllFrozenSectors(runFunction,args[0],args[1]); - } - else { - this._doInAllActiveSectors(runFunction,argument); - this._doInAllFrozenSectors(runFunction,argument); + var edge = this._getEdgeAt(pointer); + if (edge != null) { + this._selectObject(edge,true); } } + this._redraw(); }; /** - * This clears the nodeIndices list. We cannot use this.nodeIndices = [] because we would break the link with the - * active sector. Thus we clear the nodeIndices in the active sector, then reconnect the this.nodeIndices to it. + * handle the onRelease event. These functions are here for the navigation controls module. * - * @private + * @private */ - exports._clearNodeIndexList = function() { - var sector = this._sector(); - this.sectors["active"][sector]["nodeIndices"] = []; - this.nodeIndices = this.sectors["active"][sector]["nodeIndices"]; + exports._handleOnRelease = function(pointer) { + }; + /** - * Draw the encompassing sector node * - * @param ctx - * @param sectorType - * @private + * retrieve the currently selected objects + * @return {{nodes: Array., edges: Array.}} selection */ - exports._drawSectorNodes = function(ctx,sectorType) { - var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node; - for (var sector in this.sectors[sectorType]) { - if (this.sectors[sectorType].hasOwnProperty(sector)) { - if (this.sectors[sectorType][sector]["drawingNode"] !== undefined) { - - this._switchToSector(sector,sectorType); + exports.getSelection = function() { + var nodeIds = this.getSelectedNodes(); + var edgeIds = this.getSelectedEdges(); + return {nodes:nodeIds, edges:edgeIds}; + }; - minY = 1e9; maxY = -1e9; minX = 1e9; maxX = -1e9; - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - node = this.nodes[nodeId]; - node.resize(ctx); - if (minX > node.x - 0.5 * node.width) {minX = node.x - 0.5 * node.width;} - if (maxX < node.x + 0.5 * node.width) {maxX = node.x + 0.5 * node.width;} - if (minY > node.y - 0.5 * node.height) {minY = node.y - 0.5 * node.height;} - if (maxY < node.y + 0.5 * node.height) {maxY = node.y + 0.5 * node.height;} - } - } - node = this.sectors[sectorType][sector]["drawingNode"]; - node.x = 0.5 * (maxX + minX); - node.y = 0.5 * (maxY + minY); - node.width = 2 * (node.x - minX); - node.height = 2 * (node.y - minY); - node.radius = Math.sqrt(Math.pow(0.5*node.width,2) + Math.pow(0.5*node.height,2)); - node.setScale(this.scale); - node._drawCircle(ctx); - } + /** + * + * retrieve the currently selected nodes + * @return {String[]} selection An array with the ids of the + * selected nodes. + */ + exports.getSelectedNodes = function() { + var idArray = []; + for(var nodeId in this.selectionObj.nodes) { + if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { + idArray.push(nodeId); } } + return idArray }; - exports._drawAllSectorNodes = function(ctx) { - this._drawSectorNodes(ctx,"frozen"); - this._drawSectorNodes(ctx,"active"); - this._loadLatestSector(); - }; - - -/***/ }, -/* 45 */ -/***/ function(module, exports, __webpack_require__) { - - var Node = __webpack_require__(30); - /** - * This function can be called from the _doInAllSectors function * - * @param object - * @param overlappingNodes - * @private + * retrieve the currently selected edges + * @return {Array} selection An array with the ids of the + * selected nodes. */ - exports._getNodesOverlappingWith = function(object, overlappingNodes) { - var nodes = this.nodes; - for (var nodeId in nodes) { - if (nodes.hasOwnProperty(nodeId)) { - if (nodes[nodeId].isOverlappingWith(object)) { - overlappingNodes.push(nodeId); - } + exports.getSelectedEdges = function() { + var idArray = []; + for(var edgeId in this.selectionObj.edges) { + if(this.selectionObj.edges.hasOwnProperty(edgeId)) { + idArray.push(edgeId); } } + return idArray; }; + /** - * retrieve all nodes overlapping with given object - * @param {Object} object An object with parameters left, top, right, bottom - * @return {Number[]} An array with id's of the overlapping nodes - * @private + * select zero or more nodes + * @param {Number[] | String[]} selection An array with the ids of the + * selected nodes. */ - exports._getAllNodesOverlappingWith = function (object) { - var overlappingNodes = []; - this._doInAllActiveSectors("_getNodesOverlappingWith",object,overlappingNodes); - return overlappingNodes; + exports.setSelection = function(selection) { + var i, iMax, id; + + if (!selection || (selection.length == undefined)) + throw 'Selection must be an array with ids'; + + // first unselect any selected node + this._unselectAll(true); + + for (i = 0, iMax = selection.length; i < iMax; i++) { + id = selection[i]; + + var node = this.nodes[id]; + if (!node) { + throw new RangeError('Node with id "' + id + '" not found'); + } + this._selectObject(node,true,true); + } + + console.log("setSelection is deprecated. Please use selectNodes instead.") + + this.redraw(); }; /** - * Return a position object in canvasspace from a single point in screenspace - * - * @param pointer - * @returns {{left: number, top: number, right: number, bottom: number}} - * @private + * select zero or more nodes with the option to highlight edges + * @param {Number[] | String[]} selection An array with the ids of the + * selected nodes. + * @param {boolean} [highlightEdges] */ - exports._pointerToPositionObject = function(pointer) { - var x = this._XconvertDOMtoCanvas(pointer.x); - var y = this._YconvertDOMtoCanvas(pointer.y); + exports.selectNodes = function(selection, highlightEdges) { + var i, iMax, id; - return { - left: x, - top: y, - right: x, - bottom: y - }; + if (!selection || (selection.length == undefined)) + throw 'Selection must be an array with ids'; + + // first unselect any selected node + this._unselectAll(true); + + for (i = 0, iMax = selection.length; i < iMax; i++) { + id = selection[i]; + + var node = this.nodes[id]; + if (!node) { + throw new RangeError('Node with id "' + id + '" not found'); + } + this._selectObject(node,true,true,highlightEdges); + } + this.redraw(); }; /** - * Get the top node at the a specific point (like a click) - * - * @param {{x: Number, y: Number}} pointer - * @return {Node | null} node - * @private + * select zero or more edges + * @param {Number[] | String[]} selection An array with the ids of the + * selected nodes. */ - exports._getNodeAt = function (pointer) { - // we first check if this is an navigation controls element - var positionObject = this._pointerToPositionObject(pointer); - var overlappingNodes = this._getAllNodesOverlappingWith(positionObject); + exports.selectEdges = function(selection) { + var i, iMax, id; - // if there are overlapping nodes, select the last one, this is the - // one which is drawn on top of the others - if (overlappingNodes.length > 0) { - return this.nodes[overlappingNodes[overlappingNodes.length - 1]]; - } - else { - return null; + if (!selection || (selection.length == undefined)) + throw 'Selection must be an array with ids'; + + // first unselect any selected node + this._unselectAll(true); + + for (i = 0, iMax = selection.length; i < iMax; i++) { + id = selection[i]; + + var edge = this.edges[id]; + if (!edge) { + throw new RangeError('Edge with id "' + id + '" not found'); + } + this._selectObject(edge,true,true,highlightEdges); } + this.redraw(); }; - /** - * retrieve all edges overlapping with given object, selector is around center - * @param {Object} object An object with parameters left, top, right, bottom - * @return {Number[]} An array with id's of the overlapping nodes + * Validate the selection: remove ids of nodes which no longer exist * @private */ - exports._getEdgesOverlappingWith = function (object, overlappingEdges) { - var edges = this.edges; - for (var edgeId in edges) { - if (edges.hasOwnProperty(edgeId)) { - if (edges[edgeId].isOverlappingWith(object)) { - overlappingEdges.push(edgeId); + exports._updateSelection = function () { + for(var nodeId in this.selectionObj.nodes) { + if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { + if (!this.nodes.hasOwnProperty(nodeId)) { + delete this.selectionObj.nodes[nodeId]; + } + } + } + for(var edgeId in this.selectionObj.edges) { + if(this.selectionObj.edges.hasOwnProperty(edgeId)) { + if (!this.edges.hasOwnProperty(edgeId)) { + delete this.selectionObj.edges[edgeId]; } } } }; - /** - * retrieve all nodes overlapping with given object - * @param {Object} object An object with parameters left, top, right, bottom - * @return {Number[]} An array with id's of the overlapping nodes - * @private - */ - exports._getAllEdgesOverlappingWith = function (object) { - var overlappingEdges = []; - this._doInAllActiveSectors("_getEdgesOverlappingWith",object,overlappingEdges); - return overlappingEdges; - }; +/***/ }, +/* 45 */ +/***/ function(module, exports, __webpack_require__) { + + var util = __webpack_require__(1); + var Node = __webpack_require__(30); + var Edge = __webpack_require__(27); /** - * Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call - * _getNodeAt and _getEdgesAt, then priortize the selection to user preferences. + * clears the toolbar div element of children * - * @param pointer - * @returns {null} * @private */ - exports._getEdgeAt = function(pointer) { - var positionObject = this._pointerToPositionObject(pointer); - var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject); - - if (overlappingEdges.length > 0) { - return this.edges[overlappingEdges[overlappingEdges.length - 1]]; - } - else { - return null; + exports._clearManipulatorBar = function() { + while (this.manipulationDiv.hasChildNodes()) { + this.manipulationDiv.removeChild(this.manipulationDiv.firstChild); } }; - /** - * Add object to the selection array. + * Manipulation UI temporarily overloads certain functions to extend or replace them. To be able to restore + * these functions to their original functionality, we saved them in this.cachedFunctions. + * This function restores these functions to their original function. * - * @param obj * @private */ - exports._addToSelection = function(obj) { - if (obj instanceof Node) { - this.selectionObj.nodes[obj.id] = obj; - } - else { - this.selectionObj.edges[obj.id] = obj; + exports._restoreOverloadedFunctions = function() { + for (var functionName in this.cachedFunctions) { + if (this.cachedFunctions.hasOwnProperty(functionName)) { + this[functionName] = this.cachedFunctions[functionName]; + } } }; /** - * Add object to the selection array. + * Enable or disable edit-mode. * - * @param obj * @private */ - exports._addToHover = function(obj) { - if (obj instanceof Node) { - this.hoverObj.nodes[obj.id] = obj; + exports._toggleEditMode = function() { + this.editMode = !this.editMode; + var toolbar = document.getElementById("network-manipulationDiv"); + var closeDiv = document.getElementById("network-manipulation-closeDiv"); + var editModeDiv = document.getElementById("network-manipulation-editMode"); + if (this.editMode == true) { + toolbar.style.display="block"; + closeDiv.style.display="block"; + editModeDiv.style.display="none"; + closeDiv.onclick = this._toggleEditMode.bind(this); } else { - this.hoverObj.edges[obj.id] = obj; + toolbar.style.display="none"; + closeDiv.style.display="none"; + editModeDiv.style.display="block"; + closeDiv.onclick = null; } + this._createManipulatorBar() }; - /** - * Remove a single option from selection. + * main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar. * - * @param {Object} obj * @private */ - exports._removeFromSelection = function(obj) { - if (obj instanceof Node) { - delete this.selectionObj.nodes[obj.id]; - } - else { - delete this.selectionObj.edges[obj.id]; + exports._createManipulatorBar = function() { + // remove bound functions + if (this.boundFunction) { + this.off('select', this.boundFunction); } - }; - /** - * Unselect all. The selectionObj is useful for this. - * - * @param {Boolean} [doNotTrigger] | ignore trigger - * @private - */ - exports._unselectAll = function(doNotTrigger) { - if (doNotTrigger === undefined) { - doNotTrigger = false; + if (this.edgeBeingEdited !== undefined) { + this.edgeBeingEdited._disableControlNodes(); + this.edgeBeingEdited = undefined; + this.selectedControlNode = null; + this.controlNodesActive = false; } - for(var nodeId in this.selectionObj.nodes) { - if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { - this.selectionObj.nodes[nodeId].unselect(); + + // restore overloaded functions + this._restoreOverloadedFunctions(); + + // resume calculation + this.freezeSimulation = false; + + // reset global variables + this.blockConnectingEdgeSelection = false; + this.forceAppendSelection = false; + + if (this.editMode == true) { + while (this.manipulationDiv.hasChildNodes()) { + this.manipulationDiv.removeChild(this.manipulationDiv.firstChild); } - } - for(var edgeId in this.selectionObj.edges) { - if(this.selectionObj.edges.hasOwnProperty(edgeId)) { - this.selectionObj.edges[edgeId].unselect(); + // add the icons to the manipulator div + this.manipulationDiv.innerHTML = "" + + "" + + ""+this.constants.labels['add'] +"" + + "
" + + "" + + ""+this.constants.labels['link'] +""; + if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) { + this.manipulationDiv.innerHTML += "" + + "
" + + "" + + ""+this.constants.labels['editNode'] +""; + } + else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) { + this.manipulationDiv.innerHTML += "" + + "
" + + "" + + ""+this.constants.labels['editEdge'] +""; + } + if (this._selectionIsEmpty() == false) { + this.manipulationDiv.innerHTML += "" + + "
" + + "" + + ""+this.constants.labels['del'] +""; } - } - this.selectionObj = {nodes:{},edges:{}}; - if (doNotTrigger == false) { - this.emit('select', this.getSelection()); + // bind the icons + var addNodeButton = document.getElementById("network-manipulate-addNode"); + addNodeButton.onclick = this._createAddNodeToolbar.bind(this); + var addEdgeButton = document.getElementById("network-manipulate-connectNode"); + addEdgeButton.onclick = this._createAddEdgeToolbar.bind(this); + if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) { + var editButton = document.getElementById("network-manipulate-editNode"); + editButton.onclick = this._editNode.bind(this); + } + else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) { + var editButton = document.getElementById("network-manipulate-editEdge"); + editButton.onclick = this._createEditEdgeToolbar.bind(this); + } + if (this._selectionIsEmpty() == false) { + var deleteButton = document.getElementById("network-manipulate-delete"); + deleteButton.onclick = this._deleteSelected.bind(this); + } + var closeDiv = document.getElementById("network-manipulation-closeDiv"); + closeDiv.onclick = this._toggleEditMode.bind(this); + + this.boundFunction = this._createManipulatorBar.bind(this); + this.on('select', this.boundFunction); + } + else { + this.editModeDiv.innerHTML = "" + + "" + + "" + this.constants.labels['edit'] + ""; + var editModeButton = document.getElementById("network-manipulate-editModeButton"); + editModeButton.onclick = this._toggleEditMode.bind(this); } }; + + /** - * Unselect all clusters. The selectionObj is useful for this. + * Create the toolbar for adding Nodes * - * @param {Boolean} [doNotTrigger] | ignore trigger * @private */ - exports._unselectClusters = function(doNotTrigger) { - if (doNotTrigger === undefined) { - doNotTrigger = false; + exports._createAddNodeToolbar = function() { + // clear the toolbar + this._clearManipulatorBar(); + if (this.boundFunction) { + this.off('select', this.boundFunction); } - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - if (this.selectionObj.nodes[nodeId].clusterSize > 1) { - this.selectionObj.nodes[nodeId].unselect(); - this._removeFromSelection(this.selectionObj.nodes[nodeId]); - } - } - } + // create the toolbar contents + this.manipulationDiv.innerHTML = "" + + "" + + "" + this.constants.labels['back'] + " " + + "
" + + "" + + "" + this.constants.labels['addDescription'] + ""; - if (doNotTrigger == false) { - this.emit('select', this.getSelection()); - } + // bind the icon + var backButton = document.getElementById("network-manipulate-back"); + backButton.onclick = this._createManipulatorBar.bind(this); + + // we use the boundFunction so we can reference it when we unbind it from the "select" event. + this.boundFunction = this._addNode.bind(this); + this.on('select', this.boundFunction); }; /** - * return the number of selected nodes + * create the toolbar to connect nodes * - * @returns {number} * @private */ - exports._getSelectedNodeCount = function() { - var count = 0; - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - count += 1; - } - } - return count; - }; + exports._createAddEdgeToolbar = function() { + // clear the toolbar + this._clearManipulatorBar(); + this._unselectAll(true); + this.freezeSimulation = true; - /** - * return the selected node - * - * @returns {number} - * @private - */ - exports._getSelectedNode = function() { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - return this.selectionObj.nodes[nodeId]; - } + if (this.boundFunction) { + this.off('select', this.boundFunction); } - return null; - }; - /** - * return the selected edge - * - * @returns {number} - * @private - */ - exports._getSelectedEdge = function() { - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - return this.selectionObj.edges[edgeId]; - } - } - return null; - }; + this._unselectAll(); + this.forceAppendSelection = false; + this.blockConnectingEdgeSelection = true; + + this.manipulationDiv.innerHTML = "" + + "" + + "" + this.constants.labels['back'] + " " + + "
" + + "" + + "" + this.constants.labels['linkDescription'] + ""; + + // bind the icon + var backButton = document.getElementById("network-manipulate-back"); + backButton.onclick = this._createManipulatorBar.bind(this); + // we use the boundFunction so we can reference it when we unbind it from the "select" event. + this.boundFunction = this._handleConnect.bind(this); + this.on('select', this.boundFunction); + + // temporarily overload functions + this.cachedFunctions["_handleTouch"] = this._handleTouch; + this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease; + this._handleTouch = this._handleConnect; + this._handleOnRelease = this._finishConnect; + + // redraw to show the unselect + this._redraw(); + }; /** - * return the number of selected edges + * create the toolbar to edit edges * - * @returns {number} * @private */ - exports._getSelectedEdgeCount = function() { - var count = 0; - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - count += 1; - } + exports._createEditEdgeToolbar = function() { + // clear the toolbar + this._clearManipulatorBar(); + this.controlNodesActive = true; + + if (this.boundFunction) { + this.off('select', this.boundFunction); } - return count; - }; + + this.edgeBeingEdited = this._getSelectedEdge(); + this.edgeBeingEdited._enableControlNodes(); + + this.manipulationDiv.innerHTML = "" + + "" + + "" + this.constants.labels['back'] + " " + + "
" + + "" + + "" + this.constants.labels['editEdgeDescription'] + ""; + + // bind the icon + var backButton = document.getElementById("network-manipulate-back"); + backButton.onclick = this._createManipulatorBar.bind(this); + + // temporarily overload functions + this.cachedFunctions["_handleTouch"] = this._handleTouch; + this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease; + this.cachedFunctions["_handleTap"] = this._handleTap; + this.cachedFunctions["_handleDragStart"] = this._handleDragStart; + this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag; + this._handleTouch = this._selectControlNode; + this._handleTap = function () {}; + this._handleOnDrag = this._controlNodeDrag; + this._handleDragStart = function () {} + this._handleOnRelease = this._releaseControlNode; + + // redraw to show the unselect + this._redraw(); + }; + + + /** - * return the number of selected objects. + * the function bound to the selection event. It checks if you want to connect a cluster and changes the description + * to walk the user through the process. * - * @returns {number} * @private */ - exports._getSelectedObjectCount = function() { - var count = 0; - for(var nodeId in this.selectionObj.nodes) { - if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { - count += 1; - } - } - for(var edgeId in this.selectionObj.edges) { - if(this.selectionObj.edges.hasOwnProperty(edgeId)) { - count += 1; - } + exports._selectControlNode = function(pointer) { + this.edgeBeingEdited.controlNodes.from.unselect(); + this.edgeBeingEdited.controlNodes.to.unselect(); + this.selectedControlNode = this.edgeBeingEdited._getSelectedControlNode(this._XconvertDOMtoCanvas(pointer.x),this._YconvertDOMtoCanvas(pointer.y)); + if (this.selectedControlNode !== null) { + this.selectedControlNode.select(); + this.freezeSimulation = true; } - return count; + this._redraw(); }; /** - * Check if anything is selected + * the function bound to the selection event. It checks if you want to connect a cluster and changes the description + * to walk the user through the process. * - * @returns {boolean} * @private */ - exports._selectionIsEmpty = function() { - for(var nodeId in this.selectionObj.nodes) { - if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { - return false; - } + exports._controlNodeDrag = function(event) { + var pointer = this._getPointer(event.gesture.center); + if (this.selectedControlNode !== null && this.selectedControlNode !== undefined) { + this.selectedControlNode.x = this._XconvertDOMtoCanvas(pointer.x); + this.selectedControlNode.y = this._YconvertDOMtoCanvas(pointer.y); } - for(var edgeId in this.selectionObj.edges) { - if(this.selectionObj.edges.hasOwnProperty(edgeId)) { - return false; + this._redraw(); + }; + + exports._releaseControlNode = function(pointer) { + var newNode = this._getNodeAt(pointer); + if (newNode != null) { + if (this.edgeBeingEdited.controlNodes.from.selected == true) { + this._editEdge(newNode.id, this.edgeBeingEdited.to.id); + this.edgeBeingEdited.controlNodes.from.unselect(); + } + if (this.edgeBeingEdited.controlNodes.to.selected == true) { + this._editEdge(this.edgeBeingEdited.from.id, newNode.id); + this.edgeBeingEdited.controlNodes.to.unselect(); } } - return true; + else { + this.edgeBeingEdited._restoreControlNodes(); + } + this.freezeSimulation = false; + this._redraw(); }; - /** - * check if one of the selected nodes is a cluster. + * the function bound to the selection event. It checks if you want to connect a cluster and changes the description + * to walk the user through the process. * - * @returns {boolean} * @private */ - exports._clusterInSelection = function() { - for(var nodeId in this.selectionObj.nodes) { - if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { - if (this.selectionObj.nodes[nodeId].clusterSize > 1) { - return true; + exports._handleConnect = function(pointer) { + if (this._getSelectedNodeCount() == 0) { + var node = this._getNodeAt(pointer); + if (node != null) { + if (node.clusterSize > 1) { + alert("Cannot create edges to a cluster.") + } + else { + this._selectObject(node,false); + // create a node the temporary line can look at + this.sectors['support']['nodes']['targetNode'] = new Node({id:'targetNode'},{},{},this.constants); + this.sectors['support']['nodes']['targetNode'].x = node.x; + this.sectors['support']['nodes']['targetNode'].y = node.y; + this.sectors['support']['nodes']['targetViaNode'] = new Node({id:'targetViaNode'},{},{},this.constants); + this.sectors['support']['nodes']['targetViaNode'].x = node.x; + this.sectors['support']['nodes']['targetViaNode'].y = node.y; + this.sectors['support']['nodes']['targetViaNode'].parentEdgeId = "connectionEdge"; + + // create a temporary edge + this.edges['connectionEdge'] = new Edge({id:"connectionEdge",from:node.id,to:this.sectors['support']['nodes']['targetNode'].id}, this, this.constants); + this.edges['connectionEdge'].from = node; + this.edges['connectionEdge'].connected = true; + this.edges['connectionEdge'].smooth = true; + this.edges['connectionEdge'].selected = true; + this.edges['connectionEdge'].to = this.sectors['support']['nodes']['targetNode']; + this.edges['connectionEdge'].via = this.sectors['support']['nodes']['targetViaNode']; + + this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag; + this._handleOnDrag = function(event) { + var pointer = this._getPointer(event.gesture.center); + this.sectors['support']['nodes']['targetNode'].x = this._XconvertDOMtoCanvas(pointer.x); + this.sectors['support']['nodes']['targetNode'].y = this._YconvertDOMtoCanvas(pointer.y); + this.sectors['support']['nodes']['targetViaNode'].x = 0.5 * (this._XconvertDOMtoCanvas(pointer.x) + this.edges['connectionEdge'].from.x); + this.sectors['support']['nodes']['targetViaNode'].y = this._YconvertDOMtoCanvas(pointer.y); + }; + + this.moving = true; + this.start(); } } } - return false; }; - /** - * select the edges connected to the node that is being selected - * - * @param {Node} node - * @private - */ - exports._selectConnectedEdges = function(node) { - for (var i = 0; i < node.dynamicEdges.length; i++) { - var edge = node.dynamicEdges[i]; - edge.select(); - this._addToSelection(edge); + exports._finishConnect = function(pointer) { + if (this._getSelectedNodeCount() == 1) { + + // restore the drag function + this._handleOnDrag = this.cachedFunctions["_handleOnDrag"]; + delete this.cachedFunctions["_handleOnDrag"]; + + // remember the edge id + var connectFromId = this.edges['connectionEdge'].fromId; + + // remove the temporary nodes and edge + delete this.edges['connectionEdge']; + delete this.sectors['support']['nodes']['targetNode']; + delete this.sectors['support']['nodes']['targetViaNode']; + + var node = this._getNodeAt(pointer); + if (node != null) { + if (node.clusterSize > 1) { + alert("Cannot create edges to a cluster.") + } + else { + this._createEdge(connectFromId,node.id); + this._createManipulatorBar(); + } + } + this._unselectAll(); } }; + /** - * select the edges connected to the node that is being selected - * - * @param {Node} node - * @private + * Adds a node on the specified location */ - exports._hoverConnectedEdges = function(node) { - for (var i = 0; i < node.dynamicEdges.length; i++) { - var edge = node.dynamicEdges[i]; - edge.hover = true; - this._addToHover(edge); + exports._addNode = function() { + if (this._selectionIsEmpty() && this.editMode == true) { + var positionObject = this._pointerToPositionObject(this.pointerPosition); + var defaultData = {id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",allowedToMoveX:true,allowedToMoveY:true}; + if (this.triggerFunctions.add) { + if (this.triggerFunctions.add.length == 2) { + var me = this; + this.triggerFunctions.add(defaultData, function(finalizedData) { + me.nodesData.add(finalizedData); + me._createManipulatorBar(); + me.moving = true; + me.start(); + }); + } + else { + alert(this.constants.labels['addError']); + this._createManipulatorBar(); + this.moving = true; + this.start(); + } + } + else { + this.nodesData.add(defaultData); + this._createManipulatorBar(); + this.moving = true; + this.start(); + } } }; /** - * unselect the edges connected to the node that is being selected + * connect two nodes with a new edge. * - * @param {Node} node * @private */ - exports._unselectConnectedEdges = function(node) { - for (var i = 0; i < node.dynamicEdges.length; i++) { - var edge = node.dynamicEdges[i]; - edge.unselect(); - this._removeFromSelection(edge); + exports._createEdge = function(sourceNodeId,targetNodeId) { + if (this.editMode == true) { + var defaultData = {from:sourceNodeId, to:targetNodeId}; + if (this.triggerFunctions.connect) { + if (this.triggerFunctions.connect.length == 2) { + var me = this; + this.triggerFunctions.connect(defaultData, function(finalizedData) { + me.edgesData.add(finalizedData); + me.moving = true; + me.start(); + }); + } + else { + alert(this.constants.labels["linkError"]); + this.moving = true; + this.start(); + } + } + else { + this.edgesData.add(defaultData); + this.moving = true; + this.start(); + } } }; - - - /** - * This is called when someone clicks on a node. either select or deselect it. - * If there is an existing selection and we don't want to append to it, clear the existing selection + * connect two nodes with a new edge. * - * @param {Node || Edge} object - * @param {Boolean} append - * @param {Boolean} [doNotTrigger] | ignore trigger * @private */ - exports._selectObject = function(object, append, doNotTrigger, highlightEdges) { - if (doNotTrigger === undefined) { - doNotTrigger = false; - } - if (highlightEdges === undefined) { - highlightEdges = true; - } - - if (this._selectionIsEmpty() == false && append == false && this.forceAppendSelection == false) { - this._unselectAll(true); + exports._editEdge = function(sourceNodeId,targetNodeId) { + if (this.editMode == true) { + var defaultData = {id: this.edgeBeingEdited.id, from:sourceNodeId, to:targetNodeId}; + if (this.triggerFunctions.editEdge) { + if (this.triggerFunctions.editEdge.length == 2) { + var me = this; + this.triggerFunctions.editEdge(defaultData, function(finalizedData) { + me.edgesData.update(finalizedData); + me.moving = true; + me.start(); + }); + } + else { + alert(this.constants.labels["linkError"]); + this.moving = true; + this.start(); + } + } + else { + this.edgesData.update(defaultData); + this.moving = true; + this.start(); + } } + }; - if (object.selected == false) { - object.select(); - this._addToSelection(object); - if (object instanceof Node && this.blockConnectingEdgeSelection == false && highlightEdges == true) { - this._selectConnectedEdges(object); + /** + * Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color. + * + * @private + */ + exports._editNode = function() { + if (this.triggerFunctions.edit && this.editMode == true) { + var node = this._getSelectedNode(); + var data = {id:node.id, + label: node.label, + group: node.group, + shape: node.shape, + color: { + background:node.color.background, + border:node.color.border, + highlight: { + background:node.color.highlight.background, + border:node.color.highlight.border + } + }}; + if (this.triggerFunctions.edit.length == 2) { + var me = this; + this.triggerFunctions.edit(data, function (finalizedData) { + me.nodesData.update(finalizedData); + me._createManipulatorBar(); + me.moving = true; + me.start(); + }); + } + else { + alert(this.constants.labels["editError"]); } } else { - object.unselect(); - this._removeFromSelection(object); - } - - if (doNotTrigger == false) { - this.emit('select', this.getSelection()); + alert(this.constants.labels["editBoundError"]); } }; + + /** - * This is called when someone clicks on a node. either select or deselect it. - * If there is an existing selection and we don't want to append to it, clear the existing selection + * delete everything in the selection * - * @param {Node || Edge} object * @private */ - exports._blurObject = function(object) { - if (object.hover == true) { - object.hover = false; - this.emit("blurNode",{node:object.id}); + exports._deleteSelected = function() { + if (!this._selectionIsEmpty() && this.editMode == true) { + if (!this._clusterInSelection()) { + var selectedNodes = this.getSelectedNodes(); + var selectedEdges = this.getSelectedEdges(); + if (this.triggerFunctions.del) { + var me = this; + var data = {nodes: selectedNodes, edges: selectedEdges}; + if (this.triggerFunctions.del.length = 2) { + this.triggerFunctions.del(data, function (finalizedData) { + me.edgesData.remove(finalizedData.edges); + me.nodesData.remove(finalizedData.nodes); + me._unselectAll(); + me.moving = true; + me.start(); + }); + } + else { + alert(this.constants.labels["deleteError"]) + } + } + else { + this.edgesData.remove(selectedEdges); + this.nodesData.remove(selectedNodes); + this._unselectAll(); + this.moving = true; + this.start(); + } + } + else { + alert(this.constants.labels["deleteClusterError"]); + } + } + }; + + +/***/ }, +/* 46 */ +/***/ function(module, exports, __webpack_require__) { + + exports._cleanNavigation = function() { + // clean up previous navigation items + var wrapper = document.getElementById('network-navigation_wrapper'); + if (wrapper != null) { + this.containerElement.removeChild(wrapper); } + document.onmouseup = null; }; /** - * This is called when someone clicks on a node. either select or deselect it. - * If there is an existing selection and we don't want to append to it, clear the existing selection + * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation + * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent + * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false. + * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas. * - * @param {Node || Edge} object * @private */ - exports._hoverObject = function(object) { - if (object.hover == false) { - object.hover = true; - this._addToHover(object); - if (object instanceof Node) { - this.emit("hoverNode",{node:object.id}); - } - } - if (object instanceof Node) { - this._hoverConnectedEdges(object); + exports._loadNavigationElements = function() { + this._cleanNavigation(); + + this.navigationDivs = {}; + var navigationDivs = ['up','down','left','right','zoomIn','zoomOut','zoomExtends']; + var navigationDivActions = ['_moveUp','_moveDown','_moveLeft','_moveRight','_zoomIn','_zoomOut','zoomExtent']; + + this.navigationDivs['wrapper'] = document.createElement('div'); + this.navigationDivs['wrapper'].id = "network-navigation_wrapper"; + this.navigationDivs['wrapper'].style.position = "absolute"; + this.navigationDivs['wrapper'].style.width = this.frame.canvas.clientWidth + "px"; + this.navigationDivs['wrapper'].style.height = this.frame.canvas.clientHeight + "px"; + this.containerElement.insertBefore(this.navigationDivs['wrapper'],this.frame); + + for (var i = 0; i < navigationDivs.length; i++) { + this.navigationDivs[navigationDivs[i]] = document.createElement('div'); + this.navigationDivs[navigationDivs[i]].id = "network-navigation_" + navigationDivs[i]; + this.navigationDivs[navigationDivs[i]].className = "network-navigation " + navigationDivs[i]; + this.navigationDivs['wrapper'].appendChild(this.navigationDivs[navigationDivs[i]]); + this.navigationDivs[navigationDivs[i]].onmousedown = this[navigationDivActions[i]].bind(this); } - }; + document.onmouseup = this._stopMovement.bind(this); + }; /** - * handles the selection part of the touch, only for navigation controls elements; - * Touch is triggered before tap, also before hold. Hold triggers after a while. - * This is the most responsive solution + * this stops all movement induced by the navigation buttons * - * @param {Object} pointer * @private */ - exports._handleTouch = function(pointer) { + exports._stopMovement = function() { + this._xStopMoving(); + this._yStopMoving(); + this._stopZoom(); }; /** - * handles the selection part of the tap; + * stops the actions performed by page up and down etc. * - * @param {Object} pointer + * @param event * @private */ - exports._handleTap = function(pointer) { - var node = this._getNodeAt(pointer); - if (node != null) { - this._selectObject(node,false); - } - else { - var edge = this._getEdgeAt(pointer); - if (edge != null) { - this._selectObject(edge,false); - } - else { - this._unselectAll(); + exports._preventDefault = function(event) { + if (event !== undefined) { + if (event.preventDefault) { + event.preventDefault(); + } else { + event.returnValue = false; } } - this.emit("click", this.getSelection()); - this._redraw(); }; /** - * handles the selection part of the double tap and opens a cluster if needed + * move the screen up + * By using the increments, instead of adding a fixed number to the translation, we keep fluent and + * instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently + * To avoid this behaviour, we do the translation in the start loop. * - * @param {Object} pointer * @private */ - exports._handleDoubleTap = function(pointer) { - var node = this._getNodeAt(pointer); - if (node != null && node !== undefined) { - // we reset the areaCenter here so the opening of the node will occur - this.areaCenter = {"x" : this._XconvertDOMtoCanvas(pointer.x), - "y" : this._YconvertDOMtoCanvas(pointer.y)}; - this.openCluster(node); + exports._moveUp = function(event) { + this.yIncrement = this.constants.keyboard.speed.y; + this.start(); // if there is no node movement, the calculation wont be done + this._preventDefault(event); + if (this.navigationDivs) { + this.navigationDivs['up'].className += " active"; } - this.emit("doubleClick", this.getSelection()); }; /** - * Handle the onHold selection part - * - * @param pointer + * move the screen down * @private */ - exports._handleOnHold = function(pointer) { - var node = this._getNodeAt(pointer); - if (node != null) { - this._selectObject(node,true); - } - else { - var edge = this._getEdgeAt(pointer); - if (edge != null) { - this._selectObject(edge,true); - } + exports._moveDown = function(event) { + this.yIncrement = -this.constants.keyboard.speed.y; + this.start(); // if there is no node movement, the calculation wont be done + this._preventDefault(event); + if (this.navigationDivs) { + this.navigationDivs['down'].className += " active"; } - this._redraw(); }; /** - * handle the onRelease event. These functions are here for the navigation controls module. - * - * @private + * move the screen left + * @private */ - exports._handleOnRelease = function(pointer) { - - }; - + exports._moveLeft = function(event) { + this.xIncrement = this.constants.keyboard.speed.x; + this.start(); // if there is no node movement, the calculation wont be done + this._preventDefault(event); + if (this.navigationDivs) { + this.navigationDivs['left'].className += " active"; + } + }; /** - * - * retrieve the currently selected objects - * @return {{nodes: Array., edges: Array.}} selection + * move the screen right + * @private */ - exports.getSelection = function() { - var nodeIds = this.getSelectedNodes(); - var edgeIds = this.getSelectedEdges(); - return {nodes:nodeIds, edges:edgeIds}; + exports._moveRight = function(event) { + this.xIncrement = -this.constants.keyboard.speed.y; + this.start(); // if there is no node movement, the calculation wont be done + this._preventDefault(event); + if (this.navigationDivs) { + this.navigationDivs['right'].className += " active"; + } }; + /** - * - * retrieve the currently selected nodes - * @return {String[]} selection An array with the ids of the - * selected nodes. + * Zoom in, using the same method as the movement. + * @private */ - exports.getSelectedNodes = function() { - var idArray = []; - for(var nodeId in this.selectionObj.nodes) { - if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { - idArray.push(nodeId); - } + exports._zoomIn = function(event) { + this.zoomIncrement = this.constants.keyboard.speed.zoom; + this.start(); // if there is no node movement, the calculation wont be done + this._preventDefault(event); + if (this.navigationDivs) { + this.navigationDivs['zoomIn'].className += " active"; } - return idArray }; + /** - * - * retrieve the currently selected edges - * @return {Array} selection An array with the ids of the - * selected nodes. + * Zoom out + * @private */ - exports.getSelectedEdges = function() { - var idArray = []; - for(var edgeId in this.selectionObj.edges) { - if(this.selectionObj.edges.hasOwnProperty(edgeId)) { - idArray.push(edgeId); - } + exports._zoomOut = function() { + this.zoomIncrement = -this.constants.keyboard.speed.zoom; + this.start(); // if there is no node movement, the calculation wont be done + this._preventDefault(event); + if (this.navigationDivs) { + this.navigationDivs['zoomOut'].className += " active"; } - return idArray; }; /** - * select zero or more nodes - * @param {Number[] | String[]} selection An array with the ids of the - * selected nodes. + * Stop zooming and unhighlight the zoom controls + * @private */ - exports.setSelection = function(selection) { - var i, iMax, id; - - if (!selection || (selection.length == undefined)) - throw 'Selection must be an array with ids'; - - // first unselect any selected node - this._unselectAll(true); - - for (i = 0, iMax = selection.length; i < iMax; i++) { - id = selection[i]; - - var node = this.nodes[id]; - if (!node) { - throw new RangeError('Node with id "' + id + '" not found'); - } - this._selectObject(node,true,true); + exports._stopZoom = function() { + this.zoomIncrement = 0; + if (this.navigationDivs) { + this.navigationDivs['zoomIn'].className = this.navigationDivs['zoomIn'].className.replace(" active",""); + this.navigationDivs['zoomOut'].className = this.navigationDivs['zoomOut'].className.replace(" active",""); } - - console.log("setSelection is deprecated. Please use selectNodes instead.") - - this.redraw(); }; /** - * select zero or more nodes with the option to highlight edges - * @param {Number[] | String[]} selection An array with the ids of the - * selected nodes. - * @param {boolean} [highlightEdges] + * Stop moving in the Y direction and unHighlight the up and down + * @private */ - exports.selectNodes = function(selection, highlightEdges) { - var i, iMax, id; - - if (!selection || (selection.length == undefined)) - throw 'Selection must be an array with ids'; - - // first unselect any selected node - this._unselectAll(true); - - for (i = 0, iMax = selection.length; i < iMax; i++) { - id = selection[i]; - - var node = this.nodes[id]; - if (!node) { - throw new RangeError('Node with id "' + id + '" not found'); - } - this._selectObject(node,true,true,highlightEdges); + exports._yStopMoving = function() { + this.yIncrement = 0; + if (this.navigationDivs) { + this.navigationDivs['up'].className = this.navigationDivs['up'].className.replace(" active",""); + this.navigationDivs['down'].className = this.navigationDivs['down'].className.replace(" active",""); } - this.redraw(); }; /** - * select zero or more edges - * @param {Number[] | String[]} selection An array with the ids of the - * selected nodes. + * Stop moving in the X direction and unHighlight left and right. + * @private */ - exports.selectEdges = function(selection) { - var i, iMax, id; - - if (!selection || (selection.length == undefined)) - throw 'Selection must be an array with ids'; + exports._xStopMoving = function() { + this.xIncrement = 0; + if (this.navigationDivs) { + this.navigationDivs['left'].className = this.navigationDivs['left'].className.replace(" active",""); + this.navigationDivs['right'].className = this.navigationDivs['right'].className.replace(" active",""); + } + }; - // first unselect any selected node - this._unselectAll(true); - for (i = 0, iMax = selection.length; i < iMax; i++) { - id = selection[i]; +/***/ }, +/* 47 */ +/***/ function(module, exports, __webpack_require__) { - var edge = this.edges[id]; - if (!edge) { - throw new RangeError('Edge with id "' + id + '" not found'); + exports._resetLevels = function() { + for (var nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + var node = this.nodes[nodeId]; + if (node.preassignedLevel == false) { + node.level = -1; + } } - this._selectObject(edge,true,true,highlightEdges); } - this.redraw(); }; /** - * Validate the selection: remove ids of nodes which no longer exist + * This is the main function to layout the nodes in a hierarchical way. + * It checks if the node details are supplied correctly + * * @private */ - exports._updateSelection = function () { - for(var nodeId in this.selectionObj.nodes) { - if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { - if (!this.nodes.hasOwnProperty(nodeId)) { - delete this.selectionObj.nodes[nodeId]; + exports._setupHierarchicalLayout = function() { + if (this.constants.hierarchicalLayout.enabled == true && this.nodeIndices.length > 0) { + if (this.constants.hierarchicalLayout.direction == "RL" || this.constants.hierarchicalLayout.direction == "DU") { + this.constants.hierarchicalLayout.levelSeparation *= -1; + } + else { + this.constants.hierarchicalLayout.levelSeparation = Math.abs(this.constants.hierarchicalLayout.levelSeparation); + } + + if (this.constants.hierarchicalLayout.direction == "RL" || this.constants.hierarchicalLayout.direction == "LR") { + if (this.constants.smoothCurves.enabled == true) { + this.constants.smoothCurves.type = "vertical"; } } - } - for(var edgeId in this.selectionObj.edges) { - if(this.selectionObj.edges.hasOwnProperty(edgeId)) { - if (!this.edges.hasOwnProperty(edgeId)) { - delete this.selectionObj.edges[edgeId]; + else { + if (this.constants.smoothCurves.enabled == true) { + this.constants.smoothCurves.type = "horizontal"; } } - } - }; + // get the size of the largest hubs and check if the user has defined a level for a node. + var hubsize = 0; + var node, nodeId; + var definedLevel = false; + var undefinedLevel = false; + for (nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + node = this.nodes[nodeId]; + if (node.level != -1) { + definedLevel = true; + } + else { + undefinedLevel = true; + } + if (hubsize < node.edges.length) { + hubsize = node.edges.length; + } + } + } -/***/ }, -/* 46 */ -/***/ function(module, exports, __webpack_require__) { + // if the user defined some levels but not all, alert and run without hierarchical layout + if (undefinedLevel == true && definedLevel == true) { + alert("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes."); + this.zoomExtent(true,this.constants.clustering.enabled); + if (!this.constants.clustering.enabled) { + this.start(); + } + } + else { + // setup the system to use hierarchical method. + this._changeConstants(); - var util = __webpack_require__(1); - var Node = __webpack_require__(30); - var Edge = __webpack_require__(27); + // define levels if undefined by the users. Based on hubsize + if (undefinedLevel == true) { + this._determineLevels(hubsize); + } + // check the distribution of the nodes per level. + var distribution = this._getDistribution(); - /** - * clears the toolbar div element of children - * - * @private - */ - exports._clearManipulatorBar = function() { - while (this.manipulationDiv.hasChildNodes()) { - this.manipulationDiv.removeChild(this.manipulationDiv.firstChild); - } - }; + // place the nodes on the canvas. This also stablilizes the system. + this._placeNodesByHierarchy(distribution); - /** - * Manipulation UI temporarily overloads certain functions to extend or replace them. To be able to restore - * these functions to their original functionality, we saved them in this.cachedFunctions. - * This function restores these functions to their original function. - * - * @private - */ - exports._restoreOverloadedFunctions = function() { - for (var functionName in this.cachedFunctions) { - if (this.cachedFunctions.hasOwnProperty(functionName)) { - this[functionName] = this.cachedFunctions[functionName]; + // start the simulation. + this.start(); } } }; + /** - * Enable or disable edit-mode. + * This function places the nodes on the canvas based on the hierarchial distribution. * + * @param {Object} distribution | obtained by the function this._getDistribution() * @private */ - exports._toggleEditMode = function() { - this.editMode = !this.editMode; - var toolbar = document.getElementById("network-manipulationDiv"); - var closeDiv = document.getElementById("network-manipulation-closeDiv"); - var editModeDiv = document.getElementById("network-manipulation-editMode"); - if (this.editMode == true) { - toolbar.style.display="block"; - closeDiv.style.display="block"; - editModeDiv.style.display="none"; - closeDiv.onclick = this._toggleEditMode.bind(this); - } - else { - toolbar.style.display="none"; - closeDiv.style.display="none"; - editModeDiv.style.display="block"; - closeDiv.onclick = null; + exports._placeNodesByHierarchy = function(distribution) { + var nodeId, node; + + // start placing all the level 0 nodes first. Then recursively position their branches. + for (nodeId in distribution[0].nodes) { + if (distribution[0].nodes.hasOwnProperty(nodeId)) { + node = distribution[0].nodes[nodeId]; + if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { + if (node.xFixed) { + node.x = distribution[0].minPos; + node.xFixed = false; + + distribution[0].minPos += distribution[0].nodeSpacing; + } + } + else { + if (node.yFixed) { + node.y = distribution[0].minPos; + node.yFixed = false; + + distribution[0].minPos += distribution[0].nodeSpacing; + } + } + this._placeBranchNodes(node.edges,node.id,distribution,node.level); + } } - this._createManipulatorBar() + + // stabilize the system after positioning. This function calls zoomExtent. + this._stabilize(); }; + /** - * main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar. + * This function get the distribution of levels based on hubsize * + * @returns {Object} * @private */ - exports._createManipulatorBar = function() { - // remove bound functions - if (this.boundFunction) { - this.off('select', this.boundFunction); - } - if (this.edgeBeingEdited !== undefined) { - this.edgeBeingEdited._disableControlNodes(); - this.edgeBeingEdited = undefined; - this.selectedControlNode = null; + exports._getDistribution = function() { + var distribution = {}; + var nodeId, node, level; + + // we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time. + // the fix of X is removed after the x value has been set. + for (nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + node = this.nodes[nodeId]; + node.xFixed = true; + node.yFixed = true; + if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { + node.y = this.constants.hierarchicalLayout.levelSeparation*node.level; + } + else { + node.x = this.constants.hierarchicalLayout.levelSeparation*node.level; + } + if (!distribution.hasOwnProperty(node.level)) { + distribution[node.level] = {amount: 0, nodes: {}, minPos:0, nodeSpacing:0}; + } + distribution[node.level].amount += 1; + distribution[node.level].nodes[node.id] = node; + } } - // restore overloaded functions - this._restoreOverloadedFunctions(); + // determine the largest amount of nodes of all levels + var maxCount = 0; + for (level in distribution) { + if (distribution.hasOwnProperty(level)) { + if (maxCount < distribution[level].amount) { + maxCount = distribution[level].amount; + } + } + } - // resume calculation - this.freezeSimulation = false; + // set the initial position and spacing of each nodes accordingly + for (level in distribution) { + if (distribution.hasOwnProperty(level)) { + distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing; + distribution[level].nodeSpacing /= (distribution[level].amount + 1); + distribution[level].minPos = distribution[level].nodeSpacing - (0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing); + } + } - // reset global variables - this.blockConnectingEdgeSelection = false; - this.forceAppendSelection = false; + return distribution; + }; - if (this.editMode == true) { - while (this.manipulationDiv.hasChildNodes()) { - this.manipulationDiv.removeChild(this.manipulationDiv.firstChild); - } - // add the icons to the manipulator div - this.manipulationDiv.innerHTML = "" + - "" + - ""+this.constants.labels['add'] +"" + - "
" + - "" + - ""+this.constants.labels['link'] +""; - if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) { - this.manipulationDiv.innerHTML += "" + - "
" + - "" + - ""+this.constants.labels['editNode'] +""; - } - else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) { - this.manipulationDiv.innerHTML += "" + - "
" + - "" + - ""+this.constants.labels['editEdge'] +""; - } - if (this._selectionIsEmpty() == false) { - this.manipulationDiv.innerHTML += "" + - "
" + - "" + - ""+this.constants.labels['del'] +""; - } + /** + * this function allocates nodes in levels based on the recursive branching from the largest hubs. + * + * @param hubsize + * @private + */ + exports._determineLevels = function(hubsize) { + var nodeId, node; - // bind the icons - var addNodeButton = document.getElementById("network-manipulate-addNode"); - addNodeButton.onclick = this._createAddNodeToolbar.bind(this); - var addEdgeButton = document.getElementById("network-manipulate-connectNode"); - addEdgeButton.onclick = this._createAddEdgeToolbar.bind(this); - if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) { - var editButton = document.getElementById("network-manipulate-editNode"); - editButton.onclick = this._editNode.bind(this); - } - else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) { - var editButton = document.getElementById("network-manipulate-editEdge"); - editButton.onclick = this._createEditEdgeToolbar.bind(this); - } - if (this._selectionIsEmpty() == false) { - var deleteButton = document.getElementById("network-manipulate-delete"); - deleteButton.onclick = this._deleteSelected.bind(this); + // determine hubs + for (nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + node = this.nodes[nodeId]; + if (node.edges.length == hubsize) { + node.level = 0; + } } - var closeDiv = document.getElementById("network-manipulation-closeDiv"); - closeDiv.onclick = this._toggleEditMode.bind(this); - - this.boundFunction = this._createManipulatorBar.bind(this); - this.on('select', this.boundFunction); } - else { - this.editModeDiv.innerHTML = "" + - "" + - "" + this.constants.labels['edit'] + ""; - var editModeButton = document.getElementById("network-manipulate-editModeButton"); - editModeButton.onclick = this._toggleEditMode.bind(this); + + // branch from hubs + for (nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + node = this.nodes[nodeId]; + if (node.level == 0) { + this._setLevel(1,node.edges,node.id); + } + } } }; - /** - * Create the toolbar for adding Nodes + * Since hierarchical layout does not support: + * - smooth curves (based on the physics), + * - clustering (based on dynamic node counts) + * + * We disable both features so there will be no problems. * * @private */ - exports._createAddNodeToolbar = function() { - // clear the toolbar - this._clearManipulatorBar(); - if (this.boundFunction) { - this.off('select', this.boundFunction); + exports._changeConstants = function() { + this.constants.clustering.enabled = false; + this.constants.physics.barnesHut.enabled = false; + this.constants.physics.hierarchicalRepulsion.enabled = true; + this._loadSelectedForceSolver(); + if (this.constants.smoothCurves.enabled == true) { + this.constants.smoothCurves.dynamic = false; } - - // create the toolbar contents - this.manipulationDiv.innerHTML = "" + - "" + - "" + this.constants.labels['back'] + " " + - "
" + - "" + - "" + this.constants.labels['addDescription'] + ""; - - // bind the icon - var backButton = document.getElementById("network-manipulate-back"); - backButton.onclick = this._createManipulatorBar.bind(this); - - // we use the boundFunction so we can reference it when we unbind it from the "select" event. - this.boundFunction = this._addNode.bind(this); - this.on('select', this.boundFunction); + this._configureSmoothCurves(); }; /** - * create the toolbar to connect nodes + * This is a recursively called function to enumerate the branches from the largest hubs and place the nodes + * on a X position that ensures there will be no overlap. * + * @param edges + * @param parentId + * @param distribution + * @param parentLevel * @private */ - exports._createAddEdgeToolbar = function() { - // clear the toolbar - this._clearManipulatorBar(); - this._unselectAll(true); - this.freezeSimulation = true; - - if (this.boundFunction) { - this.off('select', this.boundFunction); - } - - this._unselectAll(); - this.forceAppendSelection = false; - this.blockConnectingEdgeSelection = true; + exports._placeBranchNodes = function(edges, parentId, distribution, parentLevel) { + for (var i = 0; i < edges.length; i++) { + var childNode = null; + if (edges[i].toId == parentId) { + childNode = edges[i].from; + } + else { + childNode = edges[i].to; + } - this.manipulationDiv.innerHTML = "" + - "" + - "" + this.constants.labels['back'] + " " + - "
" + - "" + - "" + this.constants.labels['linkDescription'] + ""; - - // bind the icon - var backButton = document.getElementById("network-manipulate-back"); - backButton.onclick = this._createManipulatorBar.bind(this); - - // we use the boundFunction so we can reference it when we unbind it from the "select" event. - this.boundFunction = this._handleConnect.bind(this); - this.on('select', this.boundFunction); - - // temporarily overload functions - this.cachedFunctions["_handleTouch"] = this._handleTouch; - this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease; - this._handleTouch = this._handleConnect; - this._handleOnRelease = this._finishConnect; - - // redraw to show the unselect - this._redraw(); - }; - - /** - * create the toolbar to edit edges - * - * @private - */ - exports._createEditEdgeToolbar = function() { - // clear the toolbar - this._clearManipulatorBar(); + // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here. + var nodeMoved = false; + if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { + if (childNode.xFixed && childNode.level > parentLevel) { + childNode.xFixed = false; + childNode.x = distribution[childNode.level].minPos; + nodeMoved = true; + } + } + else { + if (childNode.yFixed && childNode.level > parentLevel) { + childNode.yFixed = false; + childNode.y = distribution[childNode.level].minPos; + nodeMoved = true; + } + } - if (this.boundFunction) { - this.off('select', this.boundFunction); + if (nodeMoved == true) { + distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing; + if (childNode.edges.length > 1) { + this._placeBranchNodes(childNode.edges,childNode.id,distribution,childNode.level); + } + } } - - this.edgeBeingEdited = this._getSelectedEdge(); - this.edgeBeingEdited._enableControlNodes(); - - this.manipulationDiv.innerHTML = "" + - "" + - "" + this.constants.labels['back'] + " " + - "
" + - "" + - "" + this.constants.labels['editEdgeDescription'] + ""; - - // bind the icon - var backButton = document.getElementById("network-manipulate-back"); - backButton.onclick = this._createManipulatorBar.bind(this); - - // temporarily overload functions - this.cachedFunctions["_handleTouch"] = this._handleTouch; - this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease; - this.cachedFunctions["_handleTap"] = this._handleTap; - this.cachedFunctions["_handleDragStart"] = this._handleDragStart; - this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag; - this._handleTouch = this._selectControlNode; - this._handleTap = function () {}; - this._handleOnDrag = this._controlNodeDrag; - this._handleDragStart = function () {} - this._handleOnRelease = this._releaseControlNode; - - // redraw to show the unselect - this._redraw(); }; - - - - /** - * the function bound to the selection event. It checks if you want to connect a cluster and changes the description - * to walk the user through the process. - * - * @private - */ - exports._selectControlNode = function(pointer) { - this.edgeBeingEdited.controlNodes.from.unselect(); - this.edgeBeingEdited.controlNodes.to.unselect(); - this.selectedControlNode = this.edgeBeingEdited._getSelectedControlNode(this._XconvertDOMtoCanvas(pointer.x),this._YconvertDOMtoCanvas(pointer.y)); - if (this.selectedControlNode !== null) { - this.selectedControlNode.select(); - this.freezeSimulation = true; - } - this._redraw(); - }; - /** - * the function bound to the selection event. It checks if you want to connect a cluster and changes the description - * to walk the user through the process. + * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level. * + * @param level + * @param edges + * @param parentId * @private */ - exports._controlNodeDrag = function(event) { - var pointer = this._getPointer(event.gesture.center); - if (this.selectedControlNode !== null && this.selectedControlNode !== undefined) { - this.selectedControlNode.x = this._XconvertDOMtoCanvas(pointer.x); - this.selectedControlNode.y = this._YconvertDOMtoCanvas(pointer.y); - } - this._redraw(); - }; - - exports._releaseControlNode = function(pointer) { - var newNode = this._getNodeAt(pointer); - if (newNode != null) { - if (this.edgeBeingEdited.controlNodes.from.selected == true) { - this._editEdge(newNode.id, this.edgeBeingEdited.to.id); - this.edgeBeingEdited.controlNodes.from.unselect(); + exports._setLevel = function(level, edges, parentId) { + for (var i = 0; i < edges.length; i++) { + var childNode = null; + if (edges[i].toId == parentId) { + childNode = edges[i].from; } - if (this.edgeBeingEdited.controlNodes.to.selected == true) { - this._editEdge(this.edgeBeingEdited.from.id, newNode.id); - this.edgeBeingEdited.controlNodes.to.unselect(); + else { + childNode = edges[i].to; + } + if (childNode.level == -1 || childNode.level > level) { + childNode.level = level; + if (edges.length > 1) { + this._setLevel(level+1, childNode.edges, childNode.id); + } } } - else { - this.edgeBeingEdited._restoreControlNodes(); - } - this.freezeSimulation = false; - this._redraw(); }; + /** - * the function bound to the selection event. It checks if you want to connect a cluster and changes the description - * to walk the user through the process. + * Unfix nodes * * @private */ - exports._handleConnect = function(pointer) { - if (this._getSelectedNodeCount() == 0) { - var node = this._getNodeAt(pointer); - if (node != null) { - if (node.clusterSize > 1) { - alert("Cannot create edges to a cluster.") - } - else { - this._selectObject(node,false); - // create a node the temporary line can look at - this.sectors['support']['nodes']['targetNode'] = new Node({id:'targetNode'},{},{},this.constants); - this.sectors['support']['nodes']['targetNode'].x = node.x; - this.sectors['support']['nodes']['targetNode'].y = node.y; - this.sectors['support']['nodes']['targetViaNode'] = new Node({id:'targetViaNode'},{},{},this.constants); - this.sectors['support']['nodes']['targetViaNode'].x = node.x; - this.sectors['support']['nodes']['targetViaNode'].y = node.y; - this.sectors['support']['nodes']['targetViaNode'].parentEdgeId = "connectionEdge"; - - // create a temporary edge - this.edges['connectionEdge'] = new Edge({id:"connectionEdge",from:node.id,to:this.sectors['support']['nodes']['targetNode'].id}, this, this.constants); - this.edges['connectionEdge'].from = node; - this.edges['connectionEdge'].connected = true; - this.edges['connectionEdge'].smooth = true; - this.edges['connectionEdge'].selected = true; - this.edges['connectionEdge'].to = this.sectors['support']['nodes']['targetNode']; - this.edges['connectionEdge'].via = this.sectors['support']['nodes']['targetViaNode']; - - this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag; - this._handleOnDrag = function(event) { - var pointer = this._getPointer(event.gesture.center); - this.sectors['support']['nodes']['targetNode'].x = this._XconvertDOMtoCanvas(pointer.x); - this.sectors['support']['nodes']['targetNode'].y = this._YconvertDOMtoCanvas(pointer.y); - this.sectors['support']['nodes']['targetViaNode'].x = 0.5 * (this._XconvertDOMtoCanvas(pointer.x) + this.edges['connectionEdge'].from.x); - this.sectors['support']['nodes']['targetViaNode'].y = this._YconvertDOMtoCanvas(pointer.y); - }; - - this.moving = true; - this.start(); - } + exports._restoreNodes = function() { + for (var nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + this.nodes[nodeId].xFixed = false; + this.nodes[nodeId].yFixed = false; } } }; - exports._finishConnect = function(pointer) { - if (this._getSelectedNodeCount() == 1) { - - // restore the drag function - this._handleOnDrag = this.cachedFunctions["_handleOnDrag"]; - delete this.cachedFunctions["_handleOnDrag"]; - - // remember the edge id - var connectFromId = this.edges['connectionEdge'].fromId; - - // remove the temporary nodes and edge - delete this.edges['connectionEdge']; - delete this.sectors['support']['nodes']['targetNode']; - delete this.sectors['support']['nodes']['targetViaNode']; - - var node = this._getNodeAt(pointer); - if (node != null) { - if (node.clusterSize > 1) { - alert("Cannot create edges to a cluster.") - } - else { - this._createEdge(connectFromId,node.id); - this._createManipulatorBar(); - } - } - this._unselectAll(); - } - }; +/***/ }, +/* 48 */ +/***/ function(module, exports, __webpack_require__) { /** - * Adds a node on the specified location + * Copyright 2012 Craig Campbell + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Mousetrap is a simple keyboard shortcut library for Javascript with + * no external dependencies + * + * @version 1.1.2 + * @url craig.is/killing/mice */ - exports._addNode = function() { - if (this._selectionIsEmpty() && this.editMode == true) { - var positionObject = this._pointerToPositionObject(this.pointerPosition); - var defaultData = {id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",allowedToMoveX:true,allowedToMoveY:true}; - if (this.triggerFunctions.add) { - if (this.triggerFunctions.add.length == 2) { - var me = this; - this.triggerFunctions.add(defaultData, function(finalizedData) { - me.nodesData.add(finalizedData); - me._createManipulatorBar(); - me.moving = true; - me.start(); - }); - } - else { - alert(this.constants.labels['addError']); - this._createManipulatorBar(); - this.moving = true; - this.start(); - } - } - else { - this.nodesData.add(defaultData); - this._createManipulatorBar(); - this.moving = true; - this.start(); - } - } - }; + /** + * mapping of special keycodes to their corresponding keys + * + * everything in this dictionary cannot use keypress events + * so it has to be here to map to the correct keycodes for + * keyup/keydown events + * + * @type {Object} + */ + var _MAP = { + 8: 'backspace', + 9: 'tab', + 13: 'enter', + 16: 'shift', + 17: 'ctrl', + 18: 'alt', + 20: 'capslock', + 27: 'esc', + 32: 'space', + 33: 'pageup', + 34: 'pagedown', + 35: 'end', + 36: 'home', + 37: 'left', + 38: 'up', + 39: 'right', + 40: 'down', + 45: 'ins', + 46: 'del', + 91: 'meta', + 93: 'meta', + 224: 'meta' + }, - /** - * connect two nodes with a new edge. - * - * @private - */ - exports._createEdge = function(sourceNodeId,targetNodeId) { - if (this.editMode == true) { - var defaultData = {from:sourceNodeId, to:targetNodeId}; - if (this.triggerFunctions.connect) { - if (this.triggerFunctions.connect.length == 2) { - var me = this; - this.triggerFunctions.connect(defaultData, function(finalizedData) { - me.edgesData.add(finalizedData); - me.moving = true; - me.start(); - }); - } - else { - alert(this.constants.labels["linkError"]); - this.moving = true; - this.start(); - } - } - else { - this.edgesData.add(defaultData); - this.moving = true; - this.start(); - } - } - }; + /** + * mapping for special characters so they can support + * + * this dictionary is only used incase you want to bind a + * keyup or keydown event to one of these keys + * + * @type {Object} + */ + _KEYCODE_MAP = { + 106: '*', + 107: '+', + 109: '-', + 110: '.', + 111 : '/', + 186: ';', + 187: '=', + 188: ',', + 189: '-', + 190: '.', + 191: '/', + 192: '`', + 219: '[', + 220: '\\', + 221: ']', + 222: '\'' + }, - /** - * connect two nodes with a new edge. - * - * @private - */ - exports._editEdge = function(sourceNodeId,targetNodeId) { - if (this.editMode == true) { - var defaultData = {id: this.edgeBeingEdited.id, from:sourceNodeId, to:targetNodeId}; - if (this.triggerFunctions.editEdge) { - if (this.triggerFunctions.editEdge.length == 2) { - var me = this; - this.triggerFunctions.editEdge(defaultData, function(finalizedData) { - me.edgesData.update(finalizedData); - me.moving = true; - me.start(); - }); - } - else { - alert(this.constants.labels["linkError"]); - this.moving = true; - this.start(); - } - } - else { - this.edgesData.update(defaultData); - this.moving = true; - this.start(); - } - } - }; + /** + * this is a mapping of keys that require shift on a US keypad + * back to the non shift equivelents + * + * this is so you can use keyup events with these keys + * + * note that this will only work reliably on US keyboards + * + * @type {Object} + */ + _SHIFT_MAP = { + '~': '`', + '!': '1', + '@': '2', + '#': '3', + '$': '4', + '%': '5', + '^': '6', + '&': '7', + '*': '8', + '(': '9', + ')': '0', + '_': '-', + '+': '=', + ':': ';', + '\"': '\'', + '<': ',', + '>': '.', + '?': '/', + '|': '\\' + }, - /** - * Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color. - * - * @private - */ - exports._editNode = function() { - if (this.triggerFunctions.edit && this.editMode == true) { - var node = this._getSelectedNode(); - var data = {id:node.id, - label: node.label, - group: node.group, - shape: node.shape, - color: { - background:node.color.background, - border:node.color.border, - highlight: { - background:node.color.highlight.background, - border:node.color.highlight.border - } - }}; - if (this.triggerFunctions.edit.length == 2) { - var me = this; - this.triggerFunctions.edit(data, function (finalizedData) { - me.nodesData.update(finalizedData); - me._createManipulatorBar(); - me.moving = true; - me.start(); - }); - } - else { - alert(this.constants.labels["editError"]); - } - } - else { - alert(this.constants.labels["editBoundError"]); - } - }; + /** + * this is a list of special strings you can use to map + * to modifier keys when you specify your keyboard shortcuts + * + * @type {Object} + */ + _SPECIAL_ALIASES = { + 'option': 'alt', + 'command': 'meta', + 'return': 'enter', + 'escape': 'esc' + }, + /** + * variable to store the flipped version of _MAP from above + * needed to check if we should use keypress or not when no action + * is specified + * + * @type {Object|undefined} + */ + _REVERSE_MAP, + /** + * a list of all the callbacks setup via Mousetrap.bind() + * + * @type {Object} + */ + _callbacks = {}, + /** + * direct map of string combinations to callbacks used for trigger() + * + * @type {Object} + */ + _direct_map = {}, - /** - * delete everything in the selection - * - * @private - */ - exports._deleteSelected = function() { - if (!this._selectionIsEmpty() && this.editMode == true) { - if (!this._clusterInSelection()) { - var selectedNodes = this.getSelectedNodes(); - var selectedEdges = this.getSelectedEdges(); - if (this.triggerFunctions.del) { - var me = this; - var data = {nodes: selectedNodes, edges: selectedEdges}; - if (this.triggerFunctions.del.length = 2) { - this.triggerFunctions.del(data, function (finalizedData) { - me.edgesData.remove(finalizedData.edges); - me.nodesData.remove(finalizedData.nodes); - me._unselectAll(); - me.moving = true; - me.start(); - }); - } - else { - alert(this.constants.labels["deleteError"]) - } - } - else { - this.edgesData.remove(selectedEdges); - this.nodesData.remove(selectedNodes); - this._unselectAll(); - this.moving = true; - this.start(); - } - } - else { - alert(this.constants.labels["deleteClusterError"]); - } + /** + * keeps track of what level each sequence is at since multiple + * sequences can start out with the same sequence + * + * @type {Object} + */ + _sequence_levels = {}, + + /** + * variable to store the setTimeout call + * + * @type {null|number} + */ + _reset_timer, + + /** + * temporary state where we will ignore the next keyup + * + * @type {boolean|string} + */ + _ignore_next_keyup = false, + + /** + * are we currently inside of a sequence? + * type of action ("keyup" or "keydown" or "keypress") or false + * + * @type {boolean|string} + */ + _inside_sequence = false; + + /** + * loop through the f keys, f1 to f19 and add them to the map + * programatically + */ + for (var i = 1; i < 20; ++i) { + _MAP[111 + i] = 'f' + i; } - }; + /** + * loop through to map numbers on the numeric keypad + */ + for (i = 0; i <= 9; ++i) { + _MAP[i + 96] = i; + } -/***/ }, -/* 47 */ -/***/ function(module, exports, __webpack_require__) { + /** + * cross browser add event method + * + * @param {Element|HTMLDocument} object + * @param {string} type + * @param {Function} callback + * @returns void + */ + function _addEvent(object, type, callback) { + if (object.addEventListener) { + return object.addEventListener(type, callback, false); + } - exports._cleanNavigation = function() { - // clean up previous navigation items - var wrapper = document.getElementById('network-navigation_wrapper'); - if (wrapper != null) { - this.containerElement.removeChild(wrapper); + object.attachEvent('on' + type, callback); } - document.onmouseup = null; - }; - /** - * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation - * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent - * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false. - * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas. - * - * @private - */ - exports._loadNavigationElements = function() { - this._cleanNavigation(); + /** + * takes the event and returns the key character + * + * @param {Event} e + * @return {string} + */ + function _characterFromEvent(e) { - this.navigationDivs = {}; - var navigationDivs = ['up','down','left','right','zoomIn','zoomOut','zoomExtends']; - var navigationDivActions = ['_moveUp','_moveDown','_moveLeft','_moveRight','_zoomIn','_zoomOut','zoomExtent']; + // for keypress events we should return the character as is + if (e.type == 'keypress') { + return String.fromCharCode(e.which); + } - this.navigationDivs['wrapper'] = document.createElement('div'); - this.navigationDivs['wrapper'].id = "network-navigation_wrapper"; - this.navigationDivs['wrapper'].style.position = "absolute"; - this.navigationDivs['wrapper'].style.width = this.frame.canvas.clientWidth + "px"; - this.navigationDivs['wrapper'].style.height = this.frame.canvas.clientHeight + "px"; - this.containerElement.insertBefore(this.navigationDivs['wrapper'],this.frame); + // for non keypress events the special maps are needed + if (_MAP[e.which]) { + return _MAP[e.which]; + } - for (var i = 0; i < navigationDivs.length; i++) { - this.navigationDivs[navigationDivs[i]] = document.createElement('div'); - this.navigationDivs[navigationDivs[i]].id = "network-navigation_" + navigationDivs[i]; - this.navigationDivs[navigationDivs[i]].className = "network-navigation " + navigationDivs[i]; - this.navigationDivs['wrapper'].appendChild(this.navigationDivs[navigationDivs[i]]); - this.navigationDivs[navigationDivs[i]].onmousedown = this[navigationDivActions[i]].bind(this); + if (_KEYCODE_MAP[e.which]) { + return _KEYCODE_MAP[e.which]; + } + + // if it is not in the special map + return String.fromCharCode(e.which).toLowerCase(); } - document.onmouseup = this._stopMovement.bind(this); - }; + /** + * should we stop this event before firing off callbacks + * + * @param {Event} e + * @return {boolean} + */ + function _stop(e) { + var element = e.target || e.srcElement, + tag_name = element.tagName; - /** - * this stops all movement induced by the navigation buttons - * - * @private - */ - exports._stopMovement = function() { - this._xStopMoving(); - this._yStopMoving(); - this._stopZoom(); - }; + // if the element has the class "mousetrap" then no need to stop + if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) { + return false; + } + // stop for input, select, and textarea + return tag_name == 'INPUT' || tag_name == 'SELECT' || tag_name == 'TEXTAREA' || (element.contentEditable && element.contentEditable == 'true'); + } - /** - * stops the actions performed by page up and down etc. - * - * @param event - * @private - */ - exports._preventDefault = function(event) { - if (event !== undefined) { - if (event.preventDefault) { - event.preventDefault(); - } else { - event.returnValue = false; - } + /** + * checks if two arrays are equal + * + * @param {Array} modifiers1 + * @param {Array} modifiers2 + * @returns {boolean} + */ + function _modifiersMatch(modifiers1, modifiers2) { + return modifiers1.sort().join(',') === modifiers2.sort().join(','); } - }; + /** + * resets all sequence counters except for the ones passed in + * + * @param {Object} do_not_reset + * @returns void + */ + function _resetSequences(do_not_reset) { + do_not_reset = do_not_reset || {}; - /** - * move the screen up - * By using the increments, instead of adding a fixed number to the translation, we keep fluent and - * instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently - * To avoid this behaviour, we do the translation in the start loop. - * - * @private - */ - exports._moveUp = function(event) { - this.yIncrement = this.constants.keyboard.speed.y; - this.start(); // if there is no node movement, the calculation wont be done - this._preventDefault(event); - if (this.navigationDivs) { - this.navigationDivs['up'].className += " active"; - } - }; + var active_sequences = false, + key; + for (key in _sequence_levels) { + if (do_not_reset[key]) { + active_sequences = true; + continue; + } + _sequence_levels[key] = 0; + } - /** - * move the screen down - * @private - */ - exports._moveDown = function(event) { - this.yIncrement = -this.constants.keyboard.speed.y; - this.start(); // if there is no node movement, the calculation wont be done - this._preventDefault(event); - if (this.navigationDivs) { - this.navigationDivs['down'].className += " active"; + if (!active_sequences) { + _inside_sequence = false; + } } - }; + /** + * finds all callbacks that match based on the keycode, modifiers, + * and action + * + * @param {string} character + * @param {Array} modifiers + * @param {string} action + * @param {boolean=} remove - should we remove any matches + * @param {string=} combination + * @returns {Array} + */ + function _getMatches(character, modifiers, action, remove, combination) { + var i, + callback, + matches = []; + + // if there are no events related to this keycode + if (!_callbacks[character]) { + return []; + } - /** - * move the screen left - * @private - */ - exports._moveLeft = function(event) { - this.xIncrement = this.constants.keyboard.speed.x; - this.start(); // if there is no node movement, the calculation wont be done - this._preventDefault(event); - if (this.navigationDivs) { - this.navigationDivs['left'].className += " active"; - } - }; + // if a modifier key is coming up on its own we should allow it + if (action == 'keyup' && _isModifier(character)) { + modifiers = [character]; + } + // loop through all callbacks for the key that was pressed + // and see if any of them match + for (i = 0; i < _callbacks[character].length; ++i) { + callback = _callbacks[character][i]; - /** - * move the screen right - * @private - */ - exports._moveRight = function(event) { - this.xIncrement = -this.constants.keyboard.speed.y; - this.start(); // if there is no node movement, the calculation wont be done - this._preventDefault(event); - if (this.navigationDivs) { - this.navigationDivs['right'].className += " active"; - } - }; + // if this is a sequence but it is not at the right level + // then move onto the next match + if (callback.seq && _sequence_levels[callback.seq] != callback.level) { + continue; + } + // if the action we are looking for doesn't match the action we got + // then we should keep going + if (action != callback.action) { + continue; + } - /** - * Zoom in, using the same method as the movement. - * @private - */ - exports._zoomIn = function(event) { - this.zoomIncrement = this.constants.keyboard.speed.zoom; - this.start(); // if there is no node movement, the calculation wont be done - this._preventDefault(event); - if (this.navigationDivs) { - this.navigationDivs['zoomIn'].className += " active"; - } - }; + // if this is a keypress event that means that we need to only + // look at the character, otherwise check the modifiers as + // well + if (action == 'keypress' || _modifiersMatch(modifiers, callback.modifiers)) { + // remove is used so if you change your mind and call bind a + // second time with a new function the first one is overwritten + if (remove && callback.combo == combination) { + _callbacks[character].splice(i, 1); + } - /** - * Zoom out - * @private - */ - exports._zoomOut = function() { - this.zoomIncrement = -this.constants.keyboard.speed.zoom; - this.start(); // if there is no node movement, the calculation wont be done - this._preventDefault(event); - if (this.navigationDivs) { - this.navigationDivs['zoomOut'].className += " active"; + matches.push(callback); + } + } + + return matches; } - }; + /** + * takes a key event and figures out what the modifiers are + * + * @param {Event} e + * @returns {Array} + */ + function _eventModifiers(e) { + var modifiers = []; - /** - * Stop zooming and unhighlight the zoom controls - * @private - */ - exports._stopZoom = function() { - this.zoomIncrement = 0; - if (this.navigationDivs) { - this.navigationDivs['zoomIn'].className = this.navigationDivs['zoomIn'].className.replace(" active",""); - this.navigationDivs['zoomOut'].className = this.navigationDivs['zoomOut'].className.replace(" active",""); - } - }; + if (e.shiftKey) { + modifiers.push('shift'); + } + if (e.altKey) { + modifiers.push('alt'); + } - /** - * Stop moving in the Y direction and unHighlight the up and down - * @private - */ - exports._yStopMoving = function() { - this.yIncrement = 0; - if (this.navigationDivs) { - this.navigationDivs['up'].className = this.navigationDivs['up'].className.replace(" active",""); - this.navigationDivs['down'].className = this.navigationDivs['down'].className.replace(" active",""); - } - }; + if (e.ctrlKey) { + modifiers.push('ctrl'); + } + if (e.metaKey) { + modifiers.push('meta'); + } - /** - * Stop moving in the X direction and unHighlight left and right. - * @private - */ - exports._xStopMoving = function() { - this.xIncrement = 0; - if (this.navigationDivs) { - this.navigationDivs['left'].className = this.navigationDivs['left'].className.replace(" active",""); - this.navigationDivs['right'].className = this.navigationDivs['right'].className.replace(" active",""); + return modifiers; } - }; + /** + * actually calls the callback function + * + * if your callback function returns false this will use the jquery + * convention - prevent default and stop propogation on the event + * + * @param {Function} callback + * @param {Event} e + * @returns void + */ + function _fireCallback(callback, e) { + if (callback(e) === false) { + if (e.preventDefault) { + e.preventDefault(); + } -/***/ }, -/* 48 */ -/***/ function(module, exports, __webpack_require__) { + if (e.stopPropagation) { + e.stopPropagation(); + } - exports._resetLevels = function() { - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - var node = this.nodes[nodeId]; - if (node.preassignedLevel == false) { - node.level = -1; + e.returnValue = false; + e.cancelBubble = true; } - } } - }; - /** - * This is the main function to layout the nodes in a hierarchical way. - * It checks if the node details are supplied correctly - * - * @private - */ - exports._setupHierarchicalLayout = function() { - if (this.constants.hierarchicalLayout.enabled == true && this.nodeIndices.length > 0) { - if (this.constants.hierarchicalLayout.direction == "RL" || this.constants.hierarchicalLayout.direction == "DU") { - this.constants.hierarchicalLayout.levelSeparation *= -1; - } - else { - this.constants.hierarchicalLayout.levelSeparation = Math.abs(this.constants.hierarchicalLayout.levelSeparation); - } - // get the size of the largest hubs and check if the user has defined a level for a node. - var hubsize = 0; - var node, nodeId; - var definedLevel = false; - var undefinedLevel = false; + /** + * handles a character key event + * + * @param {string} character + * @param {Event} e + * @returns void + */ + function _handleCharacter(character, e) { - for (nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - node = this.nodes[nodeId]; - if (node.level != -1) { - definedLevel = true; - } - else { - undefinedLevel = true; - } - if (hubsize < node.edges.length) { - hubsize = node.edges.length; - } + // if this event should not happen stop here + if (_stop(e)) { + return; } - } - // if the user defined some levels but not all, alert and run without hierarchical layout - if (undefinedLevel == true && definedLevel == true) { - alert("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes."); - this.zoomExtent(true,this.constants.clustering.enabled); - if (!this.constants.clustering.enabled) { - this.start(); - } - } - else { - // setup the system to use hierarchical method. - this._changeConstants(); + var callbacks = _getMatches(character, _eventModifiers(e), e.type), + i, + do_not_reset = {}, + processed_sequence_callback = false; - // define levels if undefined by the users. Based on hubsize - if (undefinedLevel == true) { - this._determineLevels(hubsize); + // loop through matching callbacks for this key event + for (i = 0; i < callbacks.length; ++i) { + + // fire for all sequence callbacks + // this is because if for example you have multiple sequences + // bound such as "g i" and "g t" they both need to fire the + // callback for matching g cause otherwise you can only ever + // match the first one + if (callbacks[i].seq) { + processed_sequence_callback = true; + + // keep a list of which sequences were matches for later + do_not_reset[callbacks[i].seq] = 1; + _fireCallback(callbacks[i].callback, e); + continue; + } + + // if there were no sequence matches but we are still here + // that means this is a regular match so we should fire that + if (!processed_sequence_callback && !_inside_sequence) { + _fireCallback(callbacks[i].callback, e); + } } - // check the distribution of the nodes per level. - var distribution = this._getDistribution(); - // place the nodes on the canvas. This also stablilizes the system. - this._placeNodesByHierarchy(distribution); - - // start the simulation. - this.start(); - } + // if you are inside of a sequence and the key you are pressing + // is not a modifier key then we should reset all sequences + // that were not matched by this key event + if (e.type == _inside_sequence && !_isModifier(character)) { + _resetSequences(do_not_reset); + } } - }; + /** + * handles a keydown event + * + * @param {Event} e + * @returns void + */ + function _handleKey(e) { - /** - * This function places the nodes on the canvas based on the hierarchial distribution. - * - * @param {Object} distribution | obtained by the function this._getDistribution() - * @private - */ - exports._placeNodesByHierarchy = function(distribution) { - var nodeId, node; + // normalize e.which for key events + // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion + e.which = typeof e.which == "number" ? e.which : e.keyCode; - // start placing all the level 0 nodes first. Then recursively position their branches. - for (nodeId in distribution[0].nodes) { - if (distribution[0].nodes.hasOwnProperty(nodeId)) { - node = distribution[0].nodes[nodeId]; - if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { - if (node.xFixed) { - node.x = distribution[0].minPos; - node.xFixed = false; + var character = _characterFromEvent(e); - distribution[0].minPos += distribution[0].nodeSpacing; - } + // no character found then stop + if (!character) { + return; } - else { - if (node.yFixed) { - node.y = distribution[0].minPos; - node.yFixed = false; - distribution[0].minPos += distribution[0].nodeSpacing; - } + if (e.type == 'keyup' && _ignore_next_keyup == character) { + _ignore_next_keyup = false; + return; } - this._placeBranchNodes(node.edges,node.id,distribution,node.level); - } + + _handleCharacter(character, e); } - // stabilize the system after positioning. This function calls zoomExtent. - this._stabilize(); - }; + /** + * determines if the keycode specified is a modifier key or not + * + * @param {string} key + * @returns {boolean} + */ + function _isModifier(key) { + return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta'; + } + /** + * called to set a 1 second timeout on the specified sequence + * + * this is so after each key press in the sequence you have 1 second + * to press the next key before you have to start over + * + * @returns void + */ + function _resetSequenceTimer() { + clearTimeout(_reset_timer); + _reset_timer = setTimeout(_resetSequences, 1000); + } - /** - * This function get the distribution of levels based on hubsize - * - * @returns {Object} - * @private - */ - exports._getDistribution = function() { - var distribution = {}; - var nodeId, node, level; + /** + * reverses the map lookup so that we can look for specific keys + * to see what can and can't use keypress + * + * @return {Object} + */ + function _getReverseMap() { + if (!_REVERSE_MAP) { + _REVERSE_MAP = {}; + for (var key in _MAP) { - // we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time. - // the fix of X is removed after the x value has been set. - for (nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - node = this.nodes[nodeId]; - node.xFixed = true; - node.yFixed = true; - if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { - node.y = this.constants.hierarchicalLayout.levelSeparation*node.level; + // pull out the numeric keypad from here cause keypress should + // be able to detect the keys from the character + if (key > 95 && key < 112) { + continue; + } + + if (_MAP.hasOwnProperty(key)) { + _REVERSE_MAP[_MAP[key]] = key; + } + } } - else { - node.x = this.constants.hierarchicalLayout.levelSeparation*node.level; + return _REVERSE_MAP; + } + + /** + * picks the best action based on the key combination + * + * @param {string} key - character for key + * @param {Array} modifiers + * @param {string=} action passed in + */ + function _pickBestAction(key, modifiers, action) { + + // if no action was picked in we should try to pick the one + // that we think would work best for this key + if (!action) { + action = _getReverseMap()[key] ? 'keydown' : 'keypress'; } - if (!distribution.hasOwnProperty(node.level)) { - distribution[node.level] = {amount: 0, nodes: {}, minPos:0, nodeSpacing:0}; + + // modifier keys don't work as expected with keypress, + // switch to keydown + if (action == 'keypress' && modifiers.length) { + action = 'keydown'; } - distribution[node.level].amount += 1; - distribution[node.level].nodes[node.id] = node; - } + + return action; } - // determine the largest amount of nodes of all levels - var maxCount = 0; - for (level in distribution) { - if (distribution.hasOwnProperty(level)) { - if (maxCount < distribution[level].amount) { - maxCount = distribution[level].amount; + /** + * binds a key sequence to an event + * + * @param {string} combo - combo specified in bind call + * @param {Array} keys + * @param {Function} callback + * @param {string=} action + * @returns void + */ + function _bindSequence(combo, keys, callback, action) { + + // start off by adding a sequence level record for this combination + // and setting the level to 0 + _sequence_levels[combo] = 0; + + // if there is no action pick the best one for the first key + // in the sequence + if (!action) { + action = _pickBestAction(keys[0], []); } - } - } - // set the initial position and spacing of each nodes accordingly - for (level in distribution) { - if (distribution.hasOwnProperty(level)) { - distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing; - distribution[level].nodeSpacing /= (distribution[level].amount + 1); - distribution[level].minPos = distribution[level].nodeSpacing - (0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing); - } - } + /** + * callback to increase the sequence level for this sequence and reset + * all other sequences that were active + * + * @param {Event} e + * @returns void + */ + var _increaseSequence = function(e) { + _inside_sequence = action; + ++_sequence_levels[combo]; + _resetSequenceTimer(); + }, - return distribution; - }; + /** + * wraps the specified callback inside of another function in order + * to reset all sequence counters as soon as this sequence is done + * + * @param {Event} e + * @returns void + */ + _callbackAndReset = function(e) { + _fireCallback(callback, e); + // we should ignore the next key up if the action is key down + // or keypress. this is so if you finish a sequence and + // release the key the final key will not trigger a keyup + if (action !== 'keyup') { + _ignore_next_keyup = _characterFromEvent(e); + } - /** - * this function allocates nodes in levels based on the recursive branching from the largest hubs. - * - * @param hubsize - * @private - */ - exports._determineLevels = function(hubsize) { - var nodeId, node; + // weird race condition if a sequence ends with the key + // another sequence begins with + setTimeout(_resetSequences, 10); + }, + i; - // determine hubs - for (nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - node = this.nodes[nodeId]; - if (node.edges.length == hubsize) { - node.level = 0; + // loop through keys one at a time and bind the appropriate callback + // function. for any key leading up to the final one it should + // increase the sequence. after the final, it should reset all sequences + for (i = 0; i < keys.length; ++i) { + _bindSingle(keys[i], i < keys.length - 1 ? _increaseSequence : _callbackAndReset, action, combo, i); } - } } - // branch from hubs - for (nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - node = this.nodes[nodeId]; - if (node.level == 0) { - this._setLevel(1,node.edges,node.id); - } - } - } - }; + /** + * binds a single keyboard combination + * + * @param {string} combination + * @param {Function} callback + * @param {string=} action + * @param {string=} sequence_name - name of sequence if part of sequence + * @param {number=} level - what part of the sequence the command is + * @returns void + */ + function _bindSingle(combination, callback, action, sequence_name, level) { + + // make sure multiple spaces in a row become a single space + combination = combination.replace(/\s+/g, ' '); + + var sequence = combination.split(' '), + i, + key, + keys, + modifiers = []; + // if this pattern is a sequence of keys then run through this method + // to reprocess each pattern one key at a time + if (sequence.length > 1) { + return _bindSequence(combination, sequence, callback, action); + } - /** - * Since hierarchical layout does not support: - * - smooth curves (based on the physics), - * - clustering (based on dynamic node counts) - * - * We disable both features so there will be no problems. - * - * @private - */ - exports._changeConstants = function() { - this.constants.clustering.enabled = false; - this.constants.physics.barnesHut.enabled = false; - this.constants.physics.hierarchicalRepulsion.enabled = true; - this._loadSelectedForceSolver(); - this.constants.smoothCurves = false; - this._configureSmoothCurves(); - }; + // take the keys from this pattern and figure out what the actual + // pattern is all about + keys = combination === '+' ? ['+'] : combination.split('+'); + for (i = 0; i < keys.length; ++i) { + key = keys[i]; - /** - * This is a recursively called function to enumerate the branches from the largest hubs and place the nodes - * on a X position that ensures there will be no overlap. - * - * @param edges - * @param parentId - * @param distribution - * @param parentLevel - * @private - */ - exports._placeBranchNodes = function(edges, parentId, distribution, parentLevel) { - for (var i = 0; i < edges.length; i++) { - var childNode = null; - if (edges[i].toId == parentId) { - childNode = edges[i].from; - } - else { - childNode = edges[i].to; - } + // normalize key names + if (_SPECIAL_ALIASES[key]) { + key = _SPECIAL_ALIASES[key]; + } - // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here. - var nodeMoved = false; - if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { - if (childNode.xFixed && childNode.level > parentLevel) { - childNode.xFixed = false; - childNode.x = distribution[childNode.level].minPos; - nodeMoved = true; - } - } - else { - if (childNode.yFixed && childNode.level > parentLevel) { - childNode.yFixed = false; - childNode.y = distribution[childNode.level].minPos; - nodeMoved = true; - } - } + // if this is not a keypress event then we should + // be smart about using shift keys + // this will only work for US keyboards however + if (action && action != 'keypress' && _SHIFT_MAP[key]) { + key = _SHIFT_MAP[key]; + modifiers.push('shift'); + } - if (nodeMoved == true) { - distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing; - if (childNode.edges.length > 1) { - this._placeBranchNodes(childNode.edges,childNode.id,distribution,childNode.level); + // if this key is a modifier then add it to the list of modifiers + if (_isModifier(key)) { + modifiers.push(key); + } } - } - } - }; + // depending on what the key combination is + // we will try to pick the best event for it + action = _pickBestAction(key, modifiers, action); - /** - * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level. - * - * @param level - * @param edges - * @param parentId - * @private - */ - exports._setLevel = function(level, edges, parentId) { - for (var i = 0; i < edges.length; i++) { - var childNode = null; - if (edges[i].toId == parentId) { - childNode = edges[i].from; - } - else { - childNode = edges[i].to; - } - if (childNode.level == -1 || childNode.level > level) { - childNode.level = level; - if (edges.length > 1) { - this._setLevel(level+1, childNode.edges, childNode.id); + // make sure to initialize array if this is the first time + // a callback is added for this key + if (!_callbacks[key]) { + _callbacks[key] = []; } - } - } - }; + // remove an existing match if there is one + _getMatches(key, modifiers, action, !sequence_name, combination); - /** - * Unfix nodes - * - * @private - */ - exports._restoreNodes = function() { - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - this.nodes[nodeId].xFixed = false; - this.nodes[nodeId].yFixed = false; - } + // add this call back to the array + // if it is a sequence put it at the beginning + // if not put it at the end + // + // this is important because the way these are processed expects + // the sequence ones to come first + _callbacks[key][sequence_name ? 'unshift' : 'push']({ + callback: callback, + modifiers: modifiers, + action: action, + seq: sequence_name, + level: level, + combo: combination + }); } - }; - -/***/ }, -/* 49 */ -/***/ function(module, exports, __webpack_require__) { - - /*! Hammer.JS - v1.0.5 - 2013-04-07 - * http://eightmedia.github.com/hammer.js - * - * Copyright (c) 2013 Jorik Tangelder ; - * Licensed under the MIT license */ - - (function(window, undefined) { - 'use strict'; - - /** - * Hammer - * use this to create instances - * @param {HTMLElement} element - * @param {Object} options - * @returns {Hammer.Instance} - * @constructor - */ - var Hammer = function(element, options) { - return new Hammer.Instance(element, options || {}); - }; + /** + * binds multiple combinations to the same callback + * + * @param {Array} combinations + * @param {Function} callback + * @param {string|undefined} action + * @returns void + */ + function _bindMultiple(combinations, callback, action) { + for (var i = 0; i < combinations.length; ++i) { + _bindSingle(combinations[i], callback, action); + } + } - // default settings - Hammer.defaults = { - // add styles and attributes to the element to prevent the browser from doing - // its native behavior. this doesnt prevent the scrolling, but cancels - // the contextmenu, tap highlighting etc - // set to false to disable this - stop_browser_behavior: { - // this also triggers onselectstart=false for IE - userSelect: 'none', - // this makes the element blocking in IE10 >, you could experiment with the value - // see for more options this issue; https://github.com/EightMedia/hammer.js/issues/241 - touchAction: 'none', - touchCallout: 'none', - contentZooming: 'none', - userDrag: 'none', - tapHighlightColor: 'rgba(0,0,0,0)' - } + // start! + _addEvent(document, 'keypress', _handleKey); + _addEvent(document, 'keydown', _handleKey); + _addEvent(document, 'keyup', _handleKey); - // more settings are defined per gesture at gestures.js - }; + var mousetrap = { - // detect touchevents - Hammer.HAS_POINTEREVENTS = navigator.pointerEnabled || navigator.msPointerEnabled; - Hammer.HAS_TOUCHEVENTS = ('ontouchstart' in window); + /** + * binds an event to mousetrap + * + * can be a single key, a combination of keys separated with +, + * a comma separated list of keys, an array of keys, or + * a sequence of keys separated by spaces + * + * be sure to list the modifier keys first to make sure that the + * correct key ends up getting bound (the last key in the pattern) + * + * @param {string|Array} keys + * @param {Function} callback + * @param {string=} action - 'keypress', 'keydown', or 'keyup' + * @returns void + */ + bind: function(keys, callback, action) { + _bindMultiple(keys instanceof Array ? keys : [keys], callback, action); + _direct_map[keys + ':' + action] = callback; + return this; + }, - // dont use mouseevents on mobile devices - Hammer.MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i; - Hammer.NO_MOUSEEVENTS = Hammer.HAS_TOUCHEVENTS && navigator.userAgent.match(Hammer.MOBILE_REGEX); + /** + * unbinds an event to mousetrap + * + * the unbinding sets the callback function of the specified key combo + * to an empty function and deletes the corresponding key in the + * _direct_map dict. + * + * the keycombo+action has to be exactly the same as + * it was defined in the bind method + * + * TODO: actually remove this from the _callbacks dictionary instead + * of binding an empty function + * + * @param {string|Array} keys + * @param {string} action + * @returns void + */ + unbind: function(keys, action) { + if (_direct_map[keys + ':' + action]) { + delete _direct_map[keys + ':' + action]; + this.bind(keys, function() {}, action); + } + return this; + }, - // eventtypes per touchevent (start, move, end) - // are filled by Hammer.event.determineEventTypes on setup - Hammer.EVENT_TYPES = {}; + /** + * triggers an event that has already been bound + * + * @param {string} keys + * @param {string=} action + * @returns void + */ + trigger: function(keys, action) { + _direct_map[keys + ':' + action](); + return this; + }, - // direction defines - Hammer.DIRECTION_DOWN = 'down'; - Hammer.DIRECTION_LEFT = 'left'; - Hammer.DIRECTION_UP = 'up'; - Hammer.DIRECTION_RIGHT = 'right'; + /** + * resets the library back to its initial state. this is useful + * if you want to clear out the current keyboard shortcuts and bind + * new ones - for example if you switch to another page + * + * @returns void + */ + reset: function() { + _callbacks = {}; + _direct_map = {}; + return this; + } + }; - // pointer type - Hammer.POINTER_MOUSE = 'mouse'; - Hammer.POINTER_TOUCH = 'touch'; - Hammer.POINTER_PEN = 'pen'; + module.exports = mousetrap; - // touch event defines - Hammer.EVENT_START = 'start'; - Hammer.EVENT_MOVE = 'move'; - Hammer.EVENT_END = 'end'; - // hammer document where the base events are added at - Hammer.DOCUMENT = document; - // plugins namespace - Hammer.plugins = {}; +/***/ }, +/* 49 */ +/***/ function(module, exports, __webpack_require__) { - // if the window events are set... - Hammer.READY = false; + var util = __webpack_require__(1); + var RepulsionMixin = __webpack_require__(52); + var HierarchialRepulsionMixin = __webpack_require__(53); + var BarnesHutMixin = __webpack_require__(54); /** - * setup events to detect gestures on the document + * Toggling barnes Hut calculation on and off. + * + * @private */ - function setup() { - if(Hammer.READY) { - return; - } - - // find what eventtypes we add listeners to - Hammer.event.determineEventTypes(); - - // Register all gestures inside Hammer.gestures - for(var name in Hammer.gestures) { - if(Hammer.gestures.hasOwnProperty(name)) { - Hammer.detection.register(Hammer.gestures[name]); - } - } - - // Add touch events on the document - Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_MOVE, Hammer.detection.detect); - Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_END, Hammer.detection.detect); + exports._toggleBarnesHut = function () { + this.constants.physics.barnesHut.enabled = !this.constants.physics.barnesHut.enabled; + this._loadSelectedForceSolver(); + this.moving = true; + this.start(); + }; - // Hammer is ready...! - Hammer.READY = true; - } /** - * create new hammer instance - * all methods should return the instance itself, so it is chainable. - * @param {HTMLElement} element - * @param {Object} [options={}] - * @returns {Hammer.Instance} - * @constructor + * This loads the node force solver based on the barnes hut or repulsion algorithm + * + * @private */ - Hammer.Instance = function(element, options) { - var self = this; - - // setup HammerJS window events and register all gestures - // this also sets up the default options - setup(); + exports._loadSelectedForceSolver = function () { + // this overloads the this._calculateNodeForces + if (this.constants.physics.barnesHut.enabled == true) { + this._clearMixin(RepulsionMixin); + this._clearMixin(HierarchialRepulsionMixin); - this.element = element; + this.constants.physics.centralGravity = this.constants.physics.barnesHut.centralGravity; + this.constants.physics.springLength = this.constants.physics.barnesHut.springLength; + this.constants.physics.springConstant = this.constants.physics.barnesHut.springConstant; + this.constants.physics.damping = this.constants.physics.barnesHut.damping; - // start/stop detection option - this.enabled = true; + this._loadMixin(BarnesHutMixin); + } + else if (this.constants.physics.hierarchicalRepulsion.enabled == true) { + this._clearMixin(BarnesHutMixin); + this._clearMixin(RepulsionMixin); - // merge options - this.options = Hammer.utils.extend( - Hammer.utils.extend({}, Hammer.defaults), - options || {}); + this.constants.physics.centralGravity = this.constants.physics.hierarchicalRepulsion.centralGravity; + this.constants.physics.springLength = this.constants.physics.hierarchicalRepulsion.springLength; + this.constants.physics.springConstant = this.constants.physics.hierarchicalRepulsion.springConstant; + this.constants.physics.damping = this.constants.physics.hierarchicalRepulsion.damping; - // add some css to the element to prevent the browser from doing its native behavoir - if(this.options.stop_browser_behavior) { - Hammer.utils.stopDefaultBrowserBehavior(this.element, this.options.stop_browser_behavior); - } + this._loadMixin(HierarchialRepulsionMixin); + } + else { + this._clearMixin(BarnesHutMixin); + this._clearMixin(HierarchialRepulsionMixin); + this.barnesHutTree = undefined; - // start detection on touchstart - Hammer.event.onTouch(element, Hammer.EVENT_START, function(ev) { - if(self.enabled) { - Hammer.detection.startDetect(self, ev); - } - }); + this.constants.physics.centralGravity = this.constants.physics.repulsion.centralGravity; + this.constants.physics.springLength = this.constants.physics.repulsion.springLength; + this.constants.physics.springConstant = this.constants.physics.repulsion.springConstant; + this.constants.physics.damping = this.constants.physics.repulsion.damping; - // return instance - return this; + this._loadMixin(RepulsionMixin); + } }; + /** + * Before calculating the forces, we check if we need to cluster to keep up performance and we check + * if there is more than one node. If it is just one node, we dont calculate anything. + * + * @private + */ + exports._initializeForceCalculation = function () { + // stop calculation if there is only one node + if (this.nodeIndices.length == 1) { + this.nodes[this.nodeIndices[0]]._setForce(0, 0); + } + else { + // if there are too many nodes on screen, we cluster without repositioning + if (this.nodeIndices.length > this.constants.clustering.clusterThreshold && this.constants.clustering.enabled == true) { + this.clusterToFit(this.constants.clustering.reduceToNodes, false); + } - Hammer.Instance.prototype = { - /** - * bind events to the instance - * @param {String} gesture - * @param {Function} handler - * @returns {Hammer.Instance} - */ - on: function onEvent(gesture, handler){ - var gestures = gesture.split(' '); - for(var t=0; t 0) { + if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { + this._calculateSpringForcesWithSupport(); + } + else { + if (this.constants.physics.hierarchicalRepulsion.enabled == true) { + this._calculateHierarchicalSpringForces(); + } + else { + this._calculateSpringForces(); + } } + } }; + /** - * this holds the last move event, - * used to fix empty touchend issue - * see the onTouch event for an explanation - * @type {Object} + * Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also + * handled in the calculateForces function. We then use a quadratic curve with the center node as control. + * This function joins the datanodes and invisible (called support) nodes into one object. + * We do this so we do not contaminate this.nodes with the support nodes. + * + * @private */ - var last_move_event = null; + exports._updateCalculationNodes = function () { + if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { + this.calculationNodes = {}; + this.calculationNodeIndices = []; + for (var nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + this.calculationNodes[nodeId] = this.nodes[nodeId]; + } + } + var supportNodes = this.sectors['support']['nodes']; + for (var supportNodeId in supportNodes) { + if (supportNodes.hasOwnProperty(supportNodeId)) { + if (this.edges.hasOwnProperty(supportNodes[supportNodeId].parentEdgeId)) { + this.calculationNodes[supportNodeId] = supportNodes[supportNodeId]; + } + else { + supportNodes[supportNodeId]._setForce(0, 0); + } + } + } - /** - * when the mouse is hold down, this is true - * @type {Boolean} - */ - var enable_detect = false; + for (var idx in this.calculationNodes) { + if (this.calculationNodes.hasOwnProperty(idx)) { + this.calculationNodeIndices.push(idx); + } + } + } + else { + this.calculationNodes = this.nodes; + this.calculationNodeIndices = this.nodeIndices; + } + }; /** - * when touch events have been fired, this is true - * @type {Boolean} + * this function applies the central gravity effect to keep groups from floating off + * + * @private */ - var touch_triggered = false; - - - Hammer.event = { - /** - * simple addEventListener - * @param {HTMLElement} element - * @param {String} type - * @param {Function} handler - */ - bindDom: function(element, type, handler) { - var types = type.split(' '); - for(var t=0; t 0 && eventType == Hammer.EVENT_END) { - eventType = Hammer.EVENT_MOVE; - } - // no touches, force the end event - else if(!count_touches) { - eventType = Hammer.EVENT_END; - } + if (distance == 0) { + distance = 0.01; + } - // because touchend has no touches, and we often want to use these in our gestures, - // we send the last move event as our eventData in touchend - if(!count_touches && last_move_event !== null) { - ev = last_move_event; - } - // store the last move event - else { - last_move_event = ev; - } + // the 1/distance is so the fx and fy can be calculated without sine or cosine. + springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance; - // trigger the handler - handler.call(Hammer.detection, self.collectEventData(element, eventType, ev)); + fx = dx * springForce; + fy = dy * springForce; - // remove pointerevent from list - if(Hammer.HAS_POINTEREVENTS && eventType == Hammer.EVENT_END) { - count_touches = Hammer.PointerEvent.updatePointer(eventType, ev); - } - } + edge.from.fx += fx; + edge.from.fy += fy; + edge.to.fx -= fx; + edge.to.fy -= fy; + } + } + } + } + }; - //debug(sourceEventType +" "+ eventType); - // on the end we reset everything - if(!count_touches) { - last_move_event = null; - enable_detect = false; - touch_triggered = false; - Hammer.PointerEvent.reset(); - } - }); - }, - /** - * we have different events for each device/browser - * determine what we need and set them in the Hammer.EVENT_TYPES constant - */ - determineEventTypes: function determineEventTypes() { - // determine the eventtype we want to set - var types; + /** + * This function calculates the springforces on the nodes, accounting for the support nodes. + * + * @private + */ + exports._calculateSpringForcesWithSupport = function () { + var edgeLength, edge, edgeId, combinedClusterSize; + var edges = this.edges; - // pointerEvents magic - if(Hammer.HAS_POINTEREVENTS) { - types = Hammer.PointerEvent.getEvents(); - } - // on Android, iOS, blackberry, windows mobile we dont want any mouseevents - else if(Hammer.NO_MOUSEEVENTS) { - types = [ - 'touchstart', - 'touchmove', - 'touchend touchcancel']; - } - // for non pointer events browsers and mixed browsers, - // like chrome on windows8 touch laptop - else { - types = [ - 'touchstart mousedown', - 'touchmove mousemove', - 'touchend touchcancel mouseup']; - } + // forces caused by the edges, modelled as springs + for (edgeId in edges) { + if (edges.hasOwnProperty(edgeId)) { + edge = edges[edgeId]; + if (edge.connected) { + // only calculate forces if nodes are in the same sector + if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) { + if (edge.via != null) { + var node1 = edge.to; + var node2 = edge.via; + var node3 = edge.from; - Hammer.EVENT_TYPES[Hammer.EVENT_START] = types[0]; - Hammer.EVENT_TYPES[Hammer.EVENT_MOVE] = types[1]; - Hammer.EVENT_TYPES[Hammer.EVENT_END] = types[2]; - }, + edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength; + combinedClusterSize = node1.clusterSize + node3.clusterSize - 2; - /** - * create touchlist depending on the event - * @param {Object} ev - * @param {String} eventType used by the fakemultitouch plugin - */ - getTouchList: function getTouchList(ev/*, eventType*/) { - // get the fake pointerEvent touchlist - if(Hammer.HAS_POINTEREVENTS) { - return Hammer.PointerEvent.getTouchList(); - } - // get the touchlist - else if(ev.touches) { - return ev.touches; - } - // make fake touchlist from mouse position - else { - return [{ - identifier: 1, - pageX: ev.pageX, - pageY: ev.pageY, - target: ev.target - }]; + // this implies that the edges between big clusters are longer + edgeLength += combinedClusterSize * this.constants.clustering.edgeGrowth; + this._calculateSpringForce(node1, node2, 0.5 * edgeLength); + this._calculateSpringForce(node2, node3, 0.5 * edgeLength); + } } - }, + } + } + } + }; - /** - * collect event data for Hammer js - * @param {HTMLElement} element - * @param {String} eventType like Hammer.EVENT_MOVE - * @param {Object} eventData - */ - collectEventData: function collectEventData(element, eventType, ev) { - var touches = this.getTouchList(ev, eventType); + /** + * This is the code actually performing the calculation for the function above. It is split out to avoid repetition. + * + * @param node1 + * @param node2 + * @param edgeLength + * @private + */ + exports._calculateSpringForce = function (node1, node2, edgeLength) { + var dx, dy, fx, fy, springForce, distance; - // find out pointerType - var pointerType = Hammer.POINTER_TOUCH; - if(ev.type.match(/mouse/) || Hammer.PointerEvent.matchType(Hammer.POINTER_MOUSE, ev)) { - pointerType = Hammer.POINTER_MOUSE; - } + dx = (node1.x - node2.x); + dy = (node1.y - node2.y); + distance = Math.sqrt(dx * dx + dy * dy); - return { - center : Hammer.utils.getCenter(touches), - timeStamp : new Date().getTime(), - target : ev.target, - touches : touches, - eventType : eventType, - pointerType : pointerType, - srcEvent : ev, + if (distance == 0) { + distance = 0.01; + } - /** - * prevent the browser default actions - * mostly used to disable scrolling of the browser - */ - preventDefault: function() { - if(this.srcEvent.preventManipulation) { - this.srcEvent.preventManipulation(); - } + // the 1/distance is so the fx and fy can be calculated without sine or cosine. + springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance; - if(this.srcEvent.preventDefault) { - this.srcEvent.preventDefault(); - } - }, + fx = dx * springForce; + fy = dy * springForce; - /** - * stop bubbling the event up to its parents - */ - stopPropagation: function() { - this.srcEvent.stopPropagation(); - }, + node1.fx += fx; + node1.fy += fy; + node2.fx -= fx; + node2.fy -= fy; + }; - /** - * immediately stop gesture detection - * might be useful after a swipe was detected - * @return {*} - */ - stopDetect: function() { - return Hammer.detection.stopDetect(); - } - }; + + /** + * Load the HTML for the physics config and bind it + * @private + */ + exports._loadPhysicsConfiguration = function () { + if (this.physicsConfiguration === undefined) { + this.backupConstants = {}; + util.deepExtend(this.backupConstants,this.constants); + + var hierarchicalLayoutDirections = ["LR", "RL", "UD", "DU"]; + this.physicsConfiguration = document.createElement('div'); + this.physicsConfiguration.className = "PhysicsConfiguration"; + this.physicsConfiguration.innerHTML = '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
Simulation Mode:
Barnes HutRepulsionHierarchical
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
Options:
' + this.containerElement.parentElement.insertBefore(this.physicsConfiguration, this.containerElement); + this.optionsDiv = document.createElement("div"); + this.optionsDiv.style.fontSize = "14px"; + this.optionsDiv.style.fontFamily = "verdana"; + this.containerElement.parentElement.insertBefore(this.optionsDiv, this.containerElement); + + var rangeElement; + rangeElement = document.getElementById('graph_BH_gc'); + rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_gc', -1, "physics_barnesHut_gravitationalConstant"); + rangeElement = document.getElementById('graph_BH_cg'); + rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_cg', 1, "physics_centralGravity"); + rangeElement = document.getElementById('graph_BH_sc'); + rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sc', 1, "physics_springConstant"); + rangeElement = document.getElementById('graph_BH_sl'); + rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sl', 1, "physics_springLength"); + rangeElement = document.getElementById('graph_BH_damp'); + rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_damp', 1, "physics_damping"); + + rangeElement = document.getElementById('graph_R_nd'); + rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_nd', 1, "physics_repulsion_nodeDistance"); + rangeElement = document.getElementById('graph_R_cg'); + rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_cg', 1, "physics_centralGravity"); + rangeElement = document.getElementById('graph_R_sc'); + rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sc', 1, "physics_springConstant"); + rangeElement = document.getElementById('graph_R_sl'); + rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sl', 1, "physics_springLength"); + rangeElement = document.getElementById('graph_R_damp'); + rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_damp', 1, "physics_damping"); + + rangeElement = document.getElementById('graph_H_nd'); + rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance"); + rangeElement = document.getElementById('graph_H_cg'); + rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_cg', 1, "physics_centralGravity"); + rangeElement = document.getElementById('graph_H_sc'); + rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sc', 1, "physics_springConstant"); + rangeElement = document.getElementById('graph_H_sl'); + rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sl', 1, "physics_springLength"); + rangeElement = document.getElementById('graph_H_damp'); + rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_damp', 1, "physics_damping"); + rangeElement = document.getElementById('graph_H_direction'); + rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_direction', hierarchicalLayoutDirections, "hierarchicalLayout_direction"); + rangeElement = document.getElementById('graph_H_levsep'); + rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_levsep', 1, "hierarchicalLayout_levelSeparation"); + rangeElement = document.getElementById('graph_H_nspac'); + rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nspac', 1, "hierarchicalLayout_nodeSpacing"); + + var radioButton1 = document.getElementById("graph_physicsMethod1"); + var radioButton2 = document.getElementById("graph_physicsMethod2"); + var radioButton3 = document.getElementById("graph_physicsMethod3"); + radioButton2.checked = true; + if (this.constants.physics.barnesHut.enabled) { + radioButton1.checked = true; + } + if (this.constants.hierarchicalLayout.enabled) { + radioButton3.checked = true; } - }; - Hammer.PointerEvent = { - /** - * holds all pointers - * @type {Object} - */ - pointers: {}, + var graph_toggleSmooth = document.getElementById("graph_toggleSmooth"); + var graph_repositionNodes = document.getElementById("graph_repositionNodes"); + var graph_generateOptions = document.getElementById("graph_generateOptions"); - /** - * get a list of pointers - * @returns {Array} touchlist - */ - getTouchList: function() { - var self = this; - var touchlist = []; + graph_toggleSmooth.onclick = graphToggleSmoothCurves.bind(this); + graph_repositionNodes.onclick = graphRepositionNodes.bind(this); + graph_generateOptions.onclick = graphGenerateOptions.bind(this); + if (this.constants.smoothCurves == true && this.constants.dynamicSmoothCurves == false) { + graph_toggleSmooth.style.background = "#A4FF56"; + } + else { + graph_toggleSmooth.style.background = "#FF8532"; + } - // we can use forEach since pointerEvents only is in IE10 - Object.keys(self.pointers).sort().forEach(function(id) { - touchlist.push(self.pointers[id]); - }); - return touchlist; - }, - /** - * update the position of a pointer - * @param {String} type Hammer.EVENT_END - * @param {Object} pointerEvent - */ - updatePointer: function(type, pointerEvent) { - if(type == Hammer.EVENT_END) { - this.pointers = {}; - } - else { - pointerEvent.identifier = pointerEvent.pointerId; - this.pointers[pointerEvent.pointerId] = pointerEvent; - } + switchConfigurations.apply(this); - return Object.keys(this.pointers).length; - }, + radioButton1.onchange = switchConfigurations.bind(this); + radioButton2.onchange = switchConfigurations.bind(this); + radioButton3.onchange = switchConfigurations.bind(this); + } + }; - /** - * check if ev matches pointertype - * @param {String} pointerType Hammer.POINTER_MOUSE - * @param {PointerEvent} ev - */ - matchType: function(pointerType, ev) { - if(!ev.pointerType) { - return false; - } + /** + * This overwrites the this.constants. + * + * @param constantsVariableName + * @param value + * @private + */ + exports._overWriteGraphConstants = function (constantsVariableName, value) { + var nameArray = constantsVariableName.split("_"); + if (nameArray.length == 1) { + this.constants[nameArray[0]] = value; + } + else if (nameArray.length == 2) { + this.constants[nameArray[0]][nameArray[1]] = value; + } + else if (nameArray.length == 3) { + this.constants[nameArray[0]][nameArray[1]][nameArray[2]] = value; + } + }; - var types = {}; - types[Hammer.POINTER_MOUSE] = (ev.pointerType == ev.MSPOINTER_TYPE_MOUSE || ev.pointerType == Hammer.POINTER_MOUSE); - types[Hammer.POINTER_TOUCH] = (ev.pointerType == ev.MSPOINTER_TYPE_TOUCH || ev.pointerType == Hammer.POINTER_TOUCH); - types[Hammer.POINTER_PEN] = (ev.pointerType == ev.MSPOINTER_TYPE_PEN || ev.pointerType == Hammer.POINTER_PEN); - return types[pointerType]; - }, + /** + * this function is bound to the toggle smooth curves button. That is also why it is not in the prototype. + */ + function graphToggleSmoothCurves () { + this.constants.smoothCurves.enabled = !this.constants.smoothCurves.enabled; + var graph_toggleSmooth = document.getElementById("graph_toggleSmooth"); + if (this.constants.smoothCurves.enabled == true) {graph_toggleSmooth.style.background = "#A4FF56";} + else {graph_toggleSmooth.style.background = "#FF8532";} - /** - * get events - */ - getEvents: function() { - return [ - 'pointerdown MSPointerDown', - 'pointermove MSPointerMove', - 'pointerup pointercancel MSPointerUp MSPointerCancel' - ]; - }, + this._configureSmoothCurves(false); + } - /** - * reset the list - */ - reset: function() { - this.pointers = {}; + /** + * this function is used to scramble the nodes + * + */ + function graphRepositionNodes () { + for (var nodeId in this.calculationNodes) { + if (this.calculationNodes.hasOwnProperty(nodeId)) { + this.calculationNodes[nodeId].vx = 0; this.calculationNodes[nodeId].vy = 0; + this.calculationNodes[nodeId].fx = 0; this.calculationNodes[nodeId].fy = 0; } - }; - + } + if (this.constants.hierarchicalLayout.enabled == true) { + this._setupHierarchicalLayout(); + } + else { + this.repositionNodes(); + } + this.moving = true; + this.start(); + } - Hammer.utils = { - /** - * extend method, - * also used for cloning when dest is an empty object - * @param {Object} dest - * @param {Object} src - * @parm {Boolean} merge do a merge - * @returns {Object} dest - */ - extend: function extend(dest, src, merge) { - for (var key in src) { - if(dest[key] !== undefined && merge) { - continue; - } - dest[key] = src[key]; + /** + * this is used to generate an options file from the playing with physics system. + */ + function graphGenerateOptions () { + var options = "No options are required, default values used."; + var optionsSpecific = []; + var radioButton1 = document.getElementById("graph_physicsMethod1"); + var radioButton2 = document.getElementById("graph_physicsMethod2"); + if (radioButton1.checked == true) { + if (this.constants.physics.barnesHut.gravitationalConstant != this.backupConstants.physics.barnesHut.gravitationalConstant) {optionsSpecific.push("gravitationalConstant: " + this.constants.physics.barnesHut.gravitationalConstant);} + if (this.constants.physics.centralGravity != this.backupConstants.physics.barnesHut.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);} + if (this.constants.physics.springLength != this.backupConstants.physics.barnesHut.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);} + if (this.constants.physics.springConstant != this.backupConstants.physics.barnesHut.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);} + if (this.constants.physics.damping != this.backupConstants.physics.barnesHut.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);} + if (optionsSpecific.length != 0) { + options = "var options = {"; + options += "physics: {barnesHut: {"; + for (var i = 0; i < optionsSpecific.length; i++) { + options += optionsSpecific[i]; + if (i < optionsSpecific.length - 1) { + options += ", " } - return dest; - }, - - - /** - * find if a node is in the given parent - * used for event delegation tricks - * @param {HTMLElement} node - * @param {HTMLElement} parent - * @returns {boolean} has_parent - */ - hasParent: function(node, parent) { - while(node){ - if(node == parent) { - return true; - } - node = node.parentNode; + } + options += '}}' + } + if (this.constants.smoothCurves.enabled != this.backupConstants.smoothCurves.enabled) { + if (optionsSpecific.length == 0) {options = "var options = {";} + else {options += ", "} + options += "smoothCurves: " + this.constants.smoothCurves.enabled; + } + if (options != "No options are required, default values used.") { + options += '};' + } + } + else if (radioButton2.checked == true) { + options = "var options = {"; + options += "physics: {barnesHut: {enabled: false}"; + if (this.constants.physics.repulsion.nodeDistance != this.backupConstants.physics.repulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.repulsion.nodeDistance);} + if (this.constants.physics.centralGravity != this.backupConstants.physics.repulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);} + if (this.constants.physics.springLength != this.backupConstants.physics.repulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);} + if (this.constants.physics.springConstant != this.backupConstants.physics.repulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);} + if (this.constants.physics.damping != this.backupConstants.physics.repulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);} + if (optionsSpecific.length != 0) { + options += ", repulsion: {"; + for (var i = 0; i < optionsSpecific.length; i++) { + options += optionsSpecific[i]; + if (i < optionsSpecific.length - 1) { + options += ", " } - return false; - }, - + } + options += '}}' + } + if (optionsSpecific.length == 0) {options += "}"} + if (this.constants.smoothCurves != this.backupConstants.smoothCurves) { + options += ", smoothCurves: " + this.constants.smoothCurves; + } + options += '};' + } + else { + options = "var options = {"; + if (this.constants.physics.hierarchicalRepulsion.nodeDistance != this.backupConstants.physics.hierarchicalRepulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.hierarchicalRepulsion.nodeDistance);} + if (this.constants.physics.centralGravity != this.backupConstants.physics.hierarchicalRepulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);} + if (this.constants.physics.springLength != this.backupConstants.physics.hierarchicalRepulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);} + if (this.constants.physics.springConstant != this.backupConstants.physics.hierarchicalRepulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);} + if (this.constants.physics.damping != this.backupConstants.physics.hierarchicalRepulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);} + if (optionsSpecific.length != 0) { + options += "physics: {hierarchicalRepulsion: {"; + for (var i = 0; i < optionsSpecific.length; i++) { + options += optionsSpecific[i]; + if (i < optionsSpecific.length - 1) { + options += ", "; + } + } + options += '}},'; + } + options += 'hierarchicalLayout: {'; + optionsSpecific = []; + if (this.constants.hierarchicalLayout.direction != this.backupConstants.hierarchicalLayout.direction) {optionsSpecific.push("direction: " + this.constants.hierarchicalLayout.direction);} + if (Math.abs(this.constants.hierarchicalLayout.levelSeparation) != this.backupConstants.hierarchicalLayout.levelSeparation) {optionsSpecific.push("levelSeparation: " + this.constants.hierarchicalLayout.levelSeparation);} + if (this.constants.hierarchicalLayout.nodeSpacing != this.backupConstants.hierarchicalLayout.nodeSpacing) {optionsSpecific.push("nodeSpacing: " + this.constants.hierarchicalLayout.nodeSpacing);} + if (optionsSpecific.length != 0) { + for (var i = 0; i < optionsSpecific.length; i++) { + options += optionsSpecific[i]; + if (i < optionsSpecific.length - 1) { + options += ", " + } + } + options += '}' + } + else { + options += "enabled:true}"; + } + options += '};' + } - /** - * get the center of all the touches - * @param {Array} touches - * @returns {Object} center - */ - getCenter: function getCenter(touches) { - var valuesX = [], valuesY = []; - for(var t= 0,len=touches.length; t= y) { - return touch1.pageX - touch2.pageX > 0 ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT; - } - else { - return touch1.pageY - touch2.pageY > 0 ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN; - } - }, + /*! Hammer.JS - v1.0.5 - 2013-04-07 + * http://eightmedia.github.com/hammer.js + * + * Copyright (c) 2013 Jorik Tangelder ; + * Licensed under the MIT license */ + (function(window, undefined) { + 'use strict'; - /** - * calculate the distance between two touches - * @param {Touch} touch1 - * @param {Touch} touch2 - * @returns {Number} distance - */ - getDistance: function getDistance(touch1, touch2) { - var x = touch2.pageX - touch1.pageX, - y = touch2.pageY - touch1.pageY; - return Math.sqrt((x*x) + (y*y)); - }, + /** + * Hammer + * use this to create instances + * @param {HTMLElement} element + * @param {Object} options + * @returns {Hammer.Instance} + * @constructor + */ + var Hammer = function(element, options) { + return new Hammer.Instance(element, options || {}); + }; + // default settings + Hammer.defaults = { + // add styles and attributes to the element to prevent the browser from doing + // its native behavior. this doesnt prevent the scrolling, but cancels + // the contextmenu, tap highlighting etc + // set to false to disable this + stop_browser_behavior: { + // this also triggers onselectstart=false for IE + userSelect: 'none', + // this makes the element blocking in IE10 >, you could experiment with the value + // see for more options this issue; https://github.com/EightMedia/hammer.js/issues/241 + touchAction: 'none', + touchCallout: 'none', + contentZooming: 'none', + userDrag: 'none', + tapHighlightColor: 'rgba(0,0,0,0)' + } - /** - * calculate the scale factor between two touchLists (fingers) - * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out - * @param {Array} start - * @param {Array} end - * @returns {Number} scale - */ - getScale: function getScale(start, end) { - // need two fingers... - if(start.length >= 2 && end.length >= 2) { - return this.getDistance(end[0], end[1]) / - this.getDistance(start[0], start[1]); - } - return 1; - }, + // more settings are defined per gesture at gestures.js + }; + // detect touchevents + Hammer.HAS_POINTEREVENTS = navigator.pointerEnabled || navigator.msPointerEnabled; + Hammer.HAS_TOUCHEVENTS = ('ontouchstart' in window); - /** - * calculate the rotation degrees between two touchLists (fingers) - * @param {Array} start - * @param {Array} end - * @returns {Number} rotation - */ - getRotation: function getRotation(start, end) { - // need two fingers - if(start.length >= 2 && end.length >= 2) { - return this.getAngle(end[1], end[0]) - - this.getAngle(start[1], start[0]); - } - return 0; - }, + // dont use mouseevents on mobile devices + Hammer.MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i; + Hammer.NO_MOUSEEVENTS = Hammer.HAS_TOUCHEVENTS && navigator.userAgent.match(Hammer.MOBILE_REGEX); + // eventtypes per touchevent (start, move, end) + // are filled by Hammer.event.determineEventTypes on setup + Hammer.EVENT_TYPES = {}; - /** - * boolean if the direction is vertical - * @param {String} direction - * @returns {Boolean} is_vertical - */ - isVertical: function isVertical(direction) { - return (direction == Hammer.DIRECTION_UP || direction == Hammer.DIRECTION_DOWN); - }, + // direction defines + Hammer.DIRECTION_DOWN = 'down'; + Hammer.DIRECTION_LEFT = 'left'; + Hammer.DIRECTION_UP = 'up'; + Hammer.DIRECTION_RIGHT = 'right'; + // pointer type + Hammer.POINTER_MOUSE = 'mouse'; + Hammer.POINTER_TOUCH = 'touch'; + Hammer.POINTER_PEN = 'pen'; - /** - * stop browser default behavior with css props - * @param {HtmlElement} element - * @param {Object} css_props - */ - stopDefaultBrowserBehavior: function stopDefaultBrowserBehavior(element, css_props) { - var prop, - vendors = ['webkit','khtml','moz','ms','o','']; + // touch event defines + Hammer.EVENT_START = 'start'; + Hammer.EVENT_MOVE = 'move'; + Hammer.EVENT_END = 'end'; - if(!css_props || !element.style) { - return; - } + // hammer document where the base events are added at + Hammer.DOCUMENT = document; - // with css properties for modern browsers - for(var i = 0; i < vendors.length; i++) { - for(var p in css_props) { - if(css_props.hasOwnProperty(p)) { - prop = p; + // plugins namespace + Hammer.plugins = {}; - // vender prefix at the property - if(vendors[i]) { - prop = vendors[i] + prop.substring(0, 1).toUpperCase() + prop.substring(1); - } + // if the window events are set... + Hammer.READY = false; - // set the style - element.style[prop] = css_props[p]; - } - } - } + /** + * setup events to detect gestures on the document + */ + function setup() { + if(Hammer.READY) { + return; + } - // also the disable onselectstart - if(css_props.userSelect == 'none') { - element.onselectstart = function() { - return false; - }; + // find what eventtypes we add listeners to + Hammer.event.determineEventTypes(); + + // Register all gestures inside Hammer.gestures + for(var name in Hammer.gestures) { + if(Hammer.gestures.hasOwnProperty(name)) { + Hammer.detection.register(Hammer.gestures[name]); } } - }; - Hammer.detection = { - // contains all registred Hammer.gestures in the correct order - gestures: [], + // Add touch events on the document + Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_MOVE, Hammer.detection.detect); + Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_END, Hammer.detection.detect); - // data of the current Hammer.gesture detection session - current: null, + // Hammer is ready...! + Hammer.READY = true; + } - // the previous Hammer.gesture session data - // is a full clone of the previous gesture.current object - previous: null, + /** + * create new hammer instance + * all methods should return the instance itself, so it is chainable. + * @param {HTMLElement} element + * @param {Object} [options={}] + * @returns {Hammer.Instance} + * @constructor + */ + Hammer.Instance = function(element, options) { + var self = this; - // when this becomes true, no gestures are fired - stopped: false, + // setup HammerJS window events and register all gestures + // this also sets up the default options + setup(); + this.element = element; - /** - * start Hammer.gesture detection - * @param {Hammer.Instance} inst - * @param {Object} eventData - */ - startDetect: function startDetect(inst, eventData) { - // already busy with a Hammer.gesture detection on an element - if(this.current) { - return; - } + // start/stop detection option + this.enabled = true; - this.stopped = false; + // merge options + this.options = Hammer.utils.extend( + Hammer.utils.extend({}, Hammer.defaults), + options || {}); - this.current = { - inst : inst, // reference to HammerInstance we're working for - startEvent : Hammer.utils.extend({}, eventData), // start eventData for distances, timing etc - lastEvent : false, // last eventData - name : '' // current gesture we're in/detected, can be 'tap', 'hold' etc - }; + // add some css to the element to prevent the browser from doing its native behavoir + if(this.options.stop_browser_behavior) { + Hammer.utils.stopDefaultBrowserBehavior(this.element, this.options.stop_browser_behavior); + } - this.detect(eventData); - }, + // start detection on touchstart + Hammer.event.onTouch(element, Hammer.EVENT_START, function(ev) { + if(self.enabled) { + Hammer.detection.startDetect(self, ev); + } + }); + + // return instance + return this; + }; + Hammer.Instance.prototype = { /** - * Hammer.gesture detection - * @param {Object} eventData - * @param {Object} eventData + * bind events to the instance + * @param {String} gesture + * @param {Function} handler + * @returns {Hammer.Instance} */ - detect: function detect(eventData) { - if(!this.current || this.stopped) { - return; + on: function onEvent(gesture, handler){ + var gestures = gesture.split(' '); + for(var t=0; t 0 && eventType == Hammer.EVENT_END) { + eventType = Hammer.EVENT_MOVE; + } + // no touches, force the end event + else if(!count_touches) { + eventType = Hammer.EVENT_END; + } - // set its index - gesture.index = gesture.index || 1000; + // because touchend has no touches, and we often want to use these in our gestures, + // we send the last move event as our eventData in touchend + if(!count_touches && last_move_event !== null) { + ev = last_move_event; + } + // store the last move event + else { + last_move_event = ev; + } - // add Hammer.gesture to the list - this.gestures.push(gesture); + // trigger the handler + handler.call(Hammer.detection, self.collectEventData(element, eventType, ev)); - // sort the list by index - this.gestures.sort(function(a, b) { - if (a.index < b.index) { - return -1; + // remove pointerevent from list + if(Hammer.HAS_POINTEREVENTS && eventType == Hammer.EVENT_END) { + count_touches = Hammer.PointerEvent.updatePointer(eventType, ev); + } } - if (a.index > b.index) { - return 1; + + //debug(sourceEventType +" "+ eventType); + + // on the end we reset everything + if(!count_touches) { + last_move_event = null; + enable_detect = false; + touch_triggered = false; + Hammer.PointerEvent.reset(); } - return 0; }); + }, - return this.gestures; - } - }; + /** + * we have different events for each device/browser + * determine what we need and set them in the Hammer.EVENT_TYPES constant + */ + determineEventTypes: function determineEventTypes() { + // determine the eventtype we want to set + var types; - Hammer.gestures = Hammer.gestures || {}; + // pointerEvents magic + if(Hammer.HAS_POINTEREVENTS) { + types = Hammer.PointerEvent.getEvents(); + } + // on Android, iOS, blackberry, windows mobile we dont want any mouseevents + else if(Hammer.NO_MOUSEEVENTS) { + types = [ + 'touchstart', + 'touchmove', + 'touchend touchcancel']; + } + // for non pointer events browsers and mixed browsers, + // like chrome on windows8 touch laptop + else { + types = [ + 'touchstart mousedown', + 'touchmove mousemove', + 'touchend touchcancel mouseup']; + } - /** - * Custom gestures - * ============================== - * - * Gesture object - * -------------------- - * The object structure of a gesture: - * - * { name: 'mygesture', - * index: 1337, - * defaults: { - * mygesture_option: true - * } - * handler: function(type, ev, inst) { - * // trigger gesture event - * inst.trigger(this.name, ev); - * } - * } + Hammer.EVENT_TYPES[Hammer.EVENT_START] = types[0]; + Hammer.EVENT_TYPES[Hammer.EVENT_MOVE] = types[1]; + Hammer.EVENT_TYPES[Hammer.EVENT_END] = types[2]; + }, - * @param {String} name - * this should be the name of the gesture, lowercase - * it is also being used to disable/enable the gesture per instance config. - * - * @param {Number} [index=1000] - * the index of the gesture, where it is going to be in the stack of gestures detection - * like when you build an gesture that depends on the drag gesture, it is a good - * idea to place it after the index of the drag gesture. - * - * @param {Object} [defaults={}] - * the default settings of the gesture. these are added to the instance settings, - * and can be overruled per instance. you can also add the name of the gesture, - * but this is also added by default (and set to true). - * - * @param {Function} handler - * this handles the gesture detection of your custom gesture and receives the - * following arguments: - * - * @param {Object} eventData - * event data containing the following properties: - * timeStamp {Number} time the event occurred - * target {HTMLElement} target element - * touches {Array} touches (fingers, pointers, mouse) on the screen - * pointerType {String} kind of pointer that was used. matches Hammer.POINTER_MOUSE|TOUCH - * center {Object} center position of the touches. contains pageX and pageY - * deltaTime {Number} the total time of the touches in the screen - * deltaX {Number} the delta on x axis we haved moved - * deltaY {Number} the delta on y axis we haved moved - * velocityX {Number} the velocity on the x - * velocityY {Number} the velocity on y - * angle {Number} the angle we are moving - * direction {String} the direction we are moving. matches Hammer.DIRECTION_UP|DOWN|LEFT|RIGHT - * distance {Number} the distance we haved moved - * scale {Number} scaling of the touches, needs 2 touches - * rotation {Number} rotation of the touches, needs 2 touches * - * eventType {String} matches Hammer.EVENT_START|MOVE|END - * srcEvent {Object} the source event, like TouchStart or MouseDown * - * startEvent {Object} contains the same properties as above, - * but from the first touch. this is used to calculate - * distances, deltaTime, scaling etc - * - * @param {Hammer.Instance} inst - * the instance we are doing the detection for. you can get the options from - * the inst.options object and trigger the gesture event by calling inst.trigger - * - * - * Handle gestures - * -------------------- - * inside the handler you can get/set Hammer.detection.current. This is the current - * detection session. It has the following properties - * @param {String} name - * contains the name of the gesture we have detected. it has not a real function, - * only to check in other gestures if something is detected. - * like in the drag gesture we set it to 'drag' and in the swipe gesture we can - * check if the current gesture is 'drag' by accessing Hammer.detection.current.name - * - * @readonly - * @param {Hammer.Instance} inst - * the instance we do the detection for - * - * @readonly - * @param {Object} startEvent - * contains the properties of the first gesture detection in this session. - * Used for calculations about timing, distance, etc. - * - * @readonly - * @param {Object} lastEvent - * contains all the properties of the last gesture detect in this session. - * - * after the gesture detection session has been completed (user has released the screen) - * the Hammer.detection.current object is copied into Hammer.detection.previous, - * this is usefull for gestures like doubletap, where you need to know if the - * previous gesture was a tap - * - * options that have been set by the instance can be received by calling inst.options - * - * You can trigger a gesture event by calling inst.trigger("mygesture", event). - * The first param is the name of your gesture, the second the event argument - * - * - * Register gestures - * -------------------- - * When an gesture is added to the Hammer.gestures object, it is auto registered - * at the setup of the first Hammer instance. You can also call Hammer.detection.register - * manually and pass your gesture object as a param - * - */ - /** - * Hold - * Touch stays at the same place for x time - * @events hold - */ - Hammer.gestures.Hold = { - name: 'hold', - index: 10, - defaults: { - hold_timeout : 500, - hold_threshold : 1 + /** + * create touchlist depending on the event + * @param {Object} ev + * @param {String} eventType used by the fakemultitouch plugin + */ + getTouchList: function getTouchList(ev/*, eventType*/) { + // get the fake pointerEvent touchlist + if(Hammer.HAS_POINTEREVENTS) { + return Hammer.PointerEvent.getTouchList(); + } + // get the touchlist + else if(ev.touches) { + return ev.touches; + } + // make fake touchlist from mouse position + else { + return [{ + identifier: 1, + pageX: ev.pageX, + pageY: ev.pageY, + target: ev.target + }]; + } }, - timer: null, - handler: function holdGesture(ev, inst) { - switch(ev.eventType) { - case Hammer.EVENT_START: - // clear any running timers - clearTimeout(this.timer); - // set the gesture so we can check in the timeout if it still is - Hammer.detection.current.name = this.name; - // set timer and if after the timeout it still is hold, - // we trigger the hold event - this.timer = setTimeout(function() { - if(Hammer.detection.current.name == 'hold') { - inst.trigger('hold', ev); - } - }, inst.options.hold_timeout); - break; + /** + * collect event data for Hammer js + * @param {HTMLElement} element + * @param {String} eventType like Hammer.EVENT_MOVE + * @param {Object} eventData + */ + collectEventData: function collectEventData(element, eventType, ev) { + var touches = this.getTouchList(ev, eventType); - // when you move or end we clear the timer - case Hammer.EVENT_MOVE: - if(ev.distance > inst.options.hold_threshold) { - clearTimeout(this.timer); + // find out pointerType + var pointerType = Hammer.POINTER_TOUCH; + if(ev.type.match(/mouse/) || Hammer.PointerEvent.matchType(Hammer.POINTER_MOUSE, ev)) { + pointerType = Hammer.POINTER_MOUSE; + } + + return { + center : Hammer.utils.getCenter(touches), + timeStamp : new Date().getTime(), + target : ev.target, + touches : touches, + eventType : eventType, + pointerType : pointerType, + srcEvent : ev, + + /** + * prevent the browser default actions + * mostly used to disable scrolling of the browser + */ + preventDefault: function() { + if(this.srcEvent.preventManipulation) { + this.srcEvent.preventManipulation(); } - break; - case Hammer.EVENT_END: - clearTimeout(this.timer); - break; - } + if(this.srcEvent.preventDefault) { + this.srcEvent.preventDefault(); + } + }, + + /** + * stop bubbling the event up to its parents + */ + stopPropagation: function() { + this.srcEvent.stopPropagation(); + }, + + /** + * immediately stop gesture detection + * might be useful after a swipe was detected + * @return {*} + */ + stopDetect: function() { + return Hammer.detection.stopDetect(); + } + }; } }; + Hammer.PointerEvent = { + /** + * holds all pointers + * @type {Object} + */ + pointers: {}, - /** - * Tap/DoubleTap - * Quick touch at a place or double at the same place - * @events tap, doubletap - */ - Hammer.gestures.Tap = { - name: 'tap', - index: 100, - defaults: { - tap_max_touchtime : 250, - tap_max_distance : 10, - tap_always : true, - doubletap_distance : 20, - doubletap_interval : 300 + /** + * get a list of pointers + * @returns {Array} touchlist + */ + getTouchList: function() { + var self = this; + var touchlist = []; + + // we can use forEach since pointerEvents only is in IE10 + Object.keys(self.pointers).sort().forEach(function(id) { + touchlist.push(self.pointers[id]); + }); + return touchlist; }, - handler: function tapGesture(ev, inst) { - if(ev.eventType == Hammer.EVENT_END) { - // previous gesture, for the double tap since these are two different gesture detections - var prev = Hammer.detection.previous, - did_doubletap = false; - // when the touchtime is higher then the max touch time - // or when the moving distance is too much - if(ev.deltaTime > inst.options.tap_max_touchtime || - ev.distance > inst.options.tap_max_distance) { - return; - } + /** + * update the position of a pointer + * @param {String} type Hammer.EVENT_END + * @param {Object} pointerEvent + */ + updatePointer: function(type, pointerEvent) { + if(type == Hammer.EVENT_END) { + this.pointers = {}; + } + else { + pointerEvent.identifier = pointerEvent.pointerId; + this.pointers[pointerEvent.pointerId] = pointerEvent; + } - // check if double tap - if(prev && prev.name == 'tap' && - (ev.timeStamp - prev.lastEvent.timeStamp) < inst.options.doubletap_interval && - ev.distance < inst.options.doubletap_distance) { - inst.trigger('doubletap', ev); - did_doubletap = true; - } + return Object.keys(this.pointers).length; + }, - // do a single tap - if(!did_doubletap || inst.options.tap_always) { - Hammer.detection.current.name = 'tap'; - inst.trigger(Hammer.detection.current.name, ev); - } + /** + * check if ev matches pointertype + * @param {String} pointerType Hammer.POINTER_MOUSE + * @param {PointerEvent} ev + */ + matchType: function(pointerType, ev) { + if(!ev.pointerType) { + return false; } + + var types = {}; + types[Hammer.POINTER_MOUSE] = (ev.pointerType == ev.MSPOINTER_TYPE_MOUSE || ev.pointerType == Hammer.POINTER_MOUSE); + types[Hammer.POINTER_TOUCH] = (ev.pointerType == ev.MSPOINTER_TYPE_TOUCH || ev.pointerType == Hammer.POINTER_TOUCH); + types[Hammer.POINTER_PEN] = (ev.pointerType == ev.MSPOINTER_TYPE_PEN || ev.pointerType == Hammer.POINTER_PEN); + return types[pointerType]; + }, + + + /** + * get events + */ + getEvents: function() { + return [ + 'pointerdown MSPointerDown', + 'pointermove MSPointerMove', + 'pointerup pointercancel MSPointerUp MSPointerCancel' + ]; + }, + + /** + * reset the list + */ + reset: function() { + this.pointers = {}; } }; - /** - * Swipe - * triggers swipe events when the end velocity is above the threshold - * @events swipe, swipeleft, swiperight, swipeup, swipedown - */ - Hammer.gestures.Swipe = { - name: 'swipe', - index: 40, - defaults: { - // set 0 for unlimited, but this can conflict with transform - swipe_max_touches : 1, - swipe_velocity : 0.7 + Hammer.utils = { + /** + * extend method, + * also used for cloning when dest is an empty object + * @param {Object} dest + * @param {Object} src + * @parm {Boolean} merge do a merge + * @returns {Object} dest + */ + extend: function extend(dest, src, merge) { + for (var key in src) { + if(dest[key] !== undefined && merge) { + continue; + } + dest[key] = src[key]; + } + return dest; }, - handler: function swipeGesture(ev, inst) { - if(ev.eventType == Hammer.EVENT_END) { - // max touches - if(inst.options.swipe_max_touches > 0 && - ev.touches.length > inst.options.swipe_max_touches) { - return; - } - // when the distance we moved is too small we skip this gesture - // or we can be already in dragging - if(ev.velocityX > inst.options.swipe_velocity || - ev.velocityY > inst.options.swipe_velocity) { - // trigger swipe events - inst.trigger(this.name, ev); - inst.trigger(this.name + ev.direction, ev); + + /** + * find if a node is in the given parent + * used for event delegation tricks + * @param {HTMLElement} node + * @param {HTMLElement} parent + * @returns {boolean} has_parent + */ + hasParent: function(node, parent) { + while(node){ + if(node == parent) { + return true; } + node = node.parentNode; } - } - }; + return false; + }, - /** - * Drag - * Move with x fingers (default 1) around on the page. Blocking the scrolling when - * moving left and right is a good practice. When all the drag events are blocking - * you disable scrolling on that area. - * @events drag, drapleft, dragright, dragup, dragdown - */ - Hammer.gestures.Drag = { - name: 'drag', - index: 50, - defaults: { - drag_min_distance : 10, - // set 0 for unlimited, but this can conflict with transform - drag_max_touches : 1, - // prevent default browser behavior when dragging occurs - // be careful with it, it makes the element a blocking element - // when you are using the drag gesture, it is a good practice to set this true - drag_block_horizontal : false, - drag_block_vertical : false, - // drag_lock_to_axis keeps the drag gesture on the axis that it started on, - // It disallows vertical directions if the initial direction was horizontal, and vice versa. - drag_lock_to_axis : false, - // drag lock only kicks in when distance > drag_lock_min_distance - // This way, locking occurs only when the distance has become large enough to reliably determine the direction - drag_lock_min_distance : 25 - }, - triggered: false, - handler: function dragGesture(ev, inst) { - // current gesture isnt drag, but dragged is true - // this means an other gesture is busy. now call dragend - if(Hammer.detection.current.name != this.name && this.triggered) { - inst.trigger(this.name +'end', ev); - this.triggered = false; - return; - } + /** + * get the center of all the touches + * @param {Array} touches + * @returns {Object} center + */ + getCenter: function getCenter(touches) { + var valuesX = [], valuesY = []; - // max touches - if(inst.options.drag_max_touches > 0 && - ev.touches.length > inst.options.drag_max_touches) { - return; + for(var t= 0,len=touches.length; t= y) { + return touch1.pageX - touch2.pageX > 0 ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT; + } + else { + return touch1.pageY - touch2.pageY > 0 ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN; + } + }, - case Hammer.EVENT_END: - // trigger dragend - if(this.triggered) { - inst.trigger(this.name +'end', ev); - } - this.triggered = false; - break; + /** + * calculate the distance between two touches + * @param {Touch} touch1 + * @param {Touch} touch2 + * @returns {Number} distance + */ + getDistance: function getDistance(touch1, touch2) { + var x = touch2.pageX - touch1.pageX, + y = touch2.pageY - touch1.pageY; + return Math.sqrt((x*x) + (y*y)); + }, + + + /** + * calculate the scale factor between two touchLists (fingers) + * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out + * @param {Array} start + * @param {Array} end + * @returns {Number} scale + */ + getScale: function getScale(start, end) { + // need two fingers... + if(start.length >= 2 && end.length >= 2) { + return this.getDistance(end[0], end[1]) / + this.getDistance(start[0], start[1]); } - } - }; + return 1; + }, - /** - * Transform - * User want to scale or rotate with 2 fingers - * @events transform, pinch, pinchin, pinchout, rotate - */ - Hammer.gestures.Transform = { - name: 'transform', - index: 45, - defaults: { - // factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1 - transform_min_scale : 0.01, - // rotation in degrees - transform_min_rotation : 1, - // prevent default browser behavior when two touches are on the screen - // but it makes the element a blocking element - // when you are using the transform gesture, it is a good practice to set this true - transform_always_block : false + /** + * calculate the rotation degrees between two touchLists (fingers) + * @param {Array} start + * @param {Array} end + * @returns {Number} rotation + */ + getRotation: function getRotation(start, end) { + // need two fingers + if(start.length >= 2 && end.length >= 2) { + return this.getAngle(end[1], end[0]) - + this.getAngle(start[1], start[0]); + } + return 0; }, - triggered: false, - handler: function transformGesture(ev, inst) { - // current gesture isnt drag, but dragged is true - // this means an other gesture is busy. now call dragend - if(Hammer.detection.current.name != this.name && this.triggered) { - inst.trigger(this.name +'end', ev); - this.triggered = false; + + + /** + * boolean if the direction is vertical + * @param {String} direction + * @returns {Boolean} is_vertical + */ + isVertical: function isVertical(direction) { + return (direction == Hammer.DIRECTION_UP || direction == Hammer.DIRECTION_DOWN); + }, + + + /** + * stop browser default behavior with css props + * @param {HtmlElement} element + * @param {Object} css_props + */ + stopDefaultBrowserBehavior: function stopDefaultBrowserBehavior(element, css_props) { + var prop, + vendors = ['webkit','khtml','moz','ms','o','']; + + if(!css_props || !element.style) { return; } - // atleast multitouch - if(ev.touches.length < 2) { - return; + // with css properties for modern browsers + for(var i = 0; i < vendors.length; i++) { + for(var p in css_props) { + if(css_props.hasOwnProperty(p)) { + prop = p; + + // vender prefix at the property + if(vendors[i]) { + prop = vendors[i] + prop.substring(0, 1).toUpperCase() + prop.substring(1); + } + + // set the style + element.style[prop] = css_props[p]; + } + } } - // prevent default when two fingers are on the screen - if(inst.options.transform_always_block) { - ev.preventDefault(); + // also the disable onselectstart + if(css_props.userSelect == 'none') { + element.onselectstart = function() { + return false; + }; } + } + }; - switch(ev.eventType) { - case Hammer.EVENT_START: - this.triggered = false; - break; + Hammer.detection = { + // contains all registred Hammer.gestures in the correct order + gestures: [], - case Hammer.EVENT_MOVE: - var scale_threshold = Math.abs(1-ev.scale); - var rotation_threshold = Math.abs(ev.rotation); + // data of the current Hammer.gesture detection session + current: null, - // when the distance we moved is too small we skip this gesture - // or we can be already in dragging - if(scale_threshold < inst.options.transform_min_scale && - rotation_threshold < inst.options.transform_min_rotation) { - return; - } + // the previous Hammer.gesture session data + // is a full clone of the previous gesture.current object + previous: null, - // we are transforming! - Hammer.detection.current.name = this.name; + // when this becomes true, no gestures are fired + stopped: false, - // first time, trigger dragstart event - if(!this.triggered) { - inst.trigger(this.name +'start', ev); - this.triggered = true; - } - inst.trigger(this.name, ev); // basic transform event + /** + * start Hammer.gesture detection + * @param {Hammer.Instance} inst + * @param {Object} eventData + */ + startDetect: function startDetect(inst, eventData) { + // already busy with a Hammer.gesture detection on an element + if(this.current) { + return; + } - // trigger rotate event - if(rotation_threshold > inst.options.transform_min_rotation) { - inst.trigger('rotate', ev); - } + this.stopped = false; - // trigger pinch event - if(scale_threshold > inst.options.transform_min_scale) { - inst.trigger('pinch', ev); - inst.trigger('pinch'+ ((ev.scale < 1) ? 'in' : 'out'), ev); - } - break; + this.current = { + inst : inst, // reference to HammerInstance we're working for + startEvent : Hammer.utils.extend({}, eventData), // start eventData for distances, timing etc + lastEvent : false, // last eventData + name : '' // current gesture we're in/detected, can be 'tap', 'hold' etc + }; - case Hammer.EVENT_END: - // trigger dragend - if(this.triggered) { - inst.trigger(this.name +'end', ev); - } + this.detect(eventData); + }, - this.triggered = false; - break; + + /** + * Hammer.gesture detection + * @param {Object} eventData + * @param {Object} eventData + */ + detect: function detect(eventData) { + if(!this.current || this.stopped) { + return; } - } - }; + // extend event data with calculations about scale, distance etc + eventData = this.extendEventData(eventData); - /** - * Touch - * Called as first, tells the user has touched the screen - * @events touch - */ - Hammer.gestures.Touch = { - name: 'touch', - index: -Infinity, - defaults: { - // call preventDefault at touchstart, and makes the element blocking by - // disabling the scrolling of the page, but it improves gestures like - // transforming and dragging. - // be careful with using this, it can be very annoying for users to be stuck - // on the page - prevent_default: false, + // instance options + var inst_options = this.current.inst.options; - // disable mouse events, so only touch (or pen!) input triggers events - prevent_mouseevents: false - }, - handler: function touchGesture(ev, inst) { - if(inst.options.prevent_mouseevents && ev.pointerType == Hammer.POINTER_MOUSE) { - ev.stopDetect(); - return; + // call Hammer.gesture handlers + for(var g=0,len=this.gestures.length; g this.constants.clustering.clusterThreshold && this.constants.clustering.enabled == true) { - this.clusterToFit(this.constants.clustering.reduceToNodes, false); - } - // we now start the force calculation - this._calculateForces(); - } - }; + /** + * register new gesture + * @param {Object} gesture object, see gestures.js for documentation + * @returns {Array} gestures + */ + register: function register(gesture) { + // add an enable gesture options if there is no given + var options = gesture.defaults || {}; + if(options[gesture.name] === undefined) { + options[gesture.name] = true; + } + // extend Hammer default options with the Hammer.gesture options + Hammer.utils.extend(Hammer.defaults, options, true); - /** - * Calculate the external forces acting on the nodes - * Forces are caused by: edges, repulsing forces between nodes, gravity - * @private - */ - exports._calculateForces = function () { - // Gravity is required to keep separated groups from floating off - // the forces are reset to zero in this loop by using _setForce instead - // of _addForce + // set its index + gesture.index = gesture.index || 1000; - this._calculateGravitationalForces(); - this._calculateNodeForces(); + // add Hammer.gesture to the list + this.gestures.push(gesture); - if (this.constants.smoothCurves == true) { - this._calculateSpringForcesWithSupport(); - } - else { - if (this.constants.physics.hierarchicalRepulsion.enabled == true) { - this._calculateHierarchicalSpringForces(); - } - else { - this._calculateSpringForces(); + // sort the list by index + this.gestures.sort(function(a, b) { + if (a.index < b.index) { + return -1; + } + if (a.index > b.index) { + return 1; + } + return 0; + }); + + return this.gestures; } - } }; + Hammer.gestures = Hammer.gestures || {}; + /** - * Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also - * handled in the calculateForces function. We then use a quadratic curve with the center node as control. - * This function joins the datanodes and invisible (called support) nodes into one object. - * We do this so we do not contaminate this.nodes with the support nodes. + * Custom gestures + * ============================== + * + * Gesture object + * -------------------- + * The object structure of a gesture: + * + * { name: 'mygesture', + * index: 1337, + * defaults: { + * mygesture_option: true + * } + * handler: function(type, ev, inst) { + * // trigger gesture event + * inst.trigger(this.name, ev); + * } + * } + + * @param {String} name + * this should be the name of the gesture, lowercase + * it is also being used to disable/enable the gesture per instance config. + * + * @param {Number} [index=1000] + * the index of the gesture, where it is going to be in the stack of gestures detection + * like when you build an gesture that depends on the drag gesture, it is a good + * idea to place it after the index of the drag gesture. + * + * @param {Object} [defaults={}] + * the default settings of the gesture. these are added to the instance settings, + * and can be overruled per instance. you can also add the name of the gesture, + * but this is also added by default (and set to true). + * + * @param {Function} handler + * this handles the gesture detection of your custom gesture and receives the + * following arguments: + * + * @param {Object} eventData + * event data containing the following properties: + * timeStamp {Number} time the event occurred + * target {HTMLElement} target element + * touches {Array} touches (fingers, pointers, mouse) on the screen + * pointerType {String} kind of pointer that was used. matches Hammer.POINTER_MOUSE|TOUCH + * center {Object} center position of the touches. contains pageX and pageY + * deltaTime {Number} the total time of the touches in the screen + * deltaX {Number} the delta on x axis we haved moved + * deltaY {Number} the delta on y axis we haved moved + * velocityX {Number} the velocity on the x + * velocityY {Number} the velocity on y + * angle {Number} the angle we are moving + * direction {String} the direction we are moving. matches Hammer.DIRECTION_UP|DOWN|LEFT|RIGHT + * distance {Number} the distance we haved moved + * scale {Number} scaling of the touches, needs 2 touches + * rotation {Number} rotation of the touches, needs 2 touches * + * eventType {String} matches Hammer.EVENT_START|MOVE|END + * srcEvent {Object} the source event, like TouchStart or MouseDown * + * startEvent {Object} contains the same properties as above, + * but from the first touch. this is used to calculate + * distances, deltaTime, scaling etc + * + * @param {Hammer.Instance} inst + * the instance we are doing the detection for. you can get the options from + * the inst.options object and trigger the gesture event by calling inst.trigger + * + * + * Handle gestures + * -------------------- + * inside the handler you can get/set Hammer.detection.current. This is the current + * detection session. It has the following properties + * @param {String} name + * contains the name of the gesture we have detected. it has not a real function, + * only to check in other gestures if something is detected. + * like in the drag gesture we set it to 'drag' and in the swipe gesture we can + * check if the current gesture is 'drag' by accessing Hammer.detection.current.name + * + * @readonly + * @param {Hammer.Instance} inst + * the instance we do the detection for + * + * @readonly + * @param {Object} startEvent + * contains the properties of the first gesture detection in this session. + * Used for calculations about timing, distance, etc. + * + * @readonly + * @param {Object} lastEvent + * contains all the properties of the last gesture detect in this session. + * + * after the gesture detection session has been completed (user has released the screen) + * the Hammer.detection.current object is copied into Hammer.detection.previous, + * this is usefull for gestures like doubletap, where you need to know if the + * previous gesture was a tap + * + * options that have been set by the instance can be received by calling inst.options + * + * You can trigger a gesture event by calling inst.trigger("mygesture", event). + * The first param is the name of your gesture, the second the event argument + * + * + * Register gestures + * -------------------- + * When an gesture is added to the Hammer.gestures object, it is auto registered + * at the setup of the first Hammer instance. You can also call Hammer.detection.register + * manually and pass your gesture object as a param * - * @private */ - exports._updateCalculationNodes = function () { - if (this.constants.smoothCurves == true) { - this.calculationNodes = {}; - this.calculationNodeIndices = []; - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - this.calculationNodes[nodeId] = this.nodes[nodeId]; - } - } - var supportNodes = this.sectors['support']['nodes']; - for (var supportNodeId in supportNodes) { - if (supportNodes.hasOwnProperty(supportNodeId)) { - if (this.edges.hasOwnProperty(supportNodes[supportNodeId].parentEdgeId)) { - this.calculationNodes[supportNodeId] = supportNodes[supportNodeId]; - } - else { - supportNodes[supportNodeId]._setForce(0, 0); - } - } - } + /** + * Hold + * Touch stays at the same place for x time + * @events hold + */ + Hammer.gestures.Hold = { + name: 'hold', + index: 10, + defaults: { + hold_timeout : 500, + hold_threshold : 1 + }, + timer: null, + handler: function holdGesture(ev, inst) { + switch(ev.eventType) { + case Hammer.EVENT_START: + // clear any running timers + clearTimeout(this.timer); - for (var idx in this.calculationNodes) { - if (this.calculationNodes.hasOwnProperty(idx)) { - this.calculationNodeIndices.push(idx); - } + // set the gesture so we can check in the timeout if it still is + Hammer.detection.current.name = this.name; + + // set timer and if after the timeout it still is hold, + // we trigger the hold event + this.timer = setTimeout(function() { + if(Hammer.detection.current.name == 'hold') { + inst.trigger('hold', ev); + } + }, inst.options.hold_timeout); + break; + + // when you move or end we clear the timer + case Hammer.EVENT_MOVE: + if(ev.distance > inst.options.hold_threshold) { + clearTimeout(this.timer); + } + break; + + case Hammer.EVENT_END: + clearTimeout(this.timer); + break; + } } - } - else { - this.calculationNodes = this.nodes; - this.calculationNodeIndices = this.nodeIndices; - } }; /** - * this function applies the central gravity effect to keep groups from floating off - * - * @private + * Tap/DoubleTap + * Quick touch at a place or double at the same place + * @events tap, doubletap */ - exports._calculateGravitationalForces = function () { - var dx, dy, distance, node, i; - var nodes = this.calculationNodes; - var gravity = this.constants.physics.centralGravity; - var gravityForce = 0; + Hammer.gestures.Tap = { + name: 'tap', + index: 100, + defaults: { + tap_max_touchtime : 250, + tap_max_distance : 10, + tap_always : true, + doubletap_distance : 20, + doubletap_interval : 300 + }, + handler: function tapGesture(ev, inst) { + if(ev.eventType == Hammer.EVENT_END) { + // previous gesture, for the double tap since these are two different gesture detections + var prev = Hammer.detection.previous, + did_doubletap = false; - for (i = 0; i < this.calculationNodeIndices.length; i++) { - node = nodes[this.calculationNodeIndices[i]]; - node.damping = this.constants.physics.damping; // possibly add function to alter damping properties of clusters. - // gravity does not apply when we are in a pocket sector - if (this._sector() == "default" && gravity != 0) { - dx = -node.x; - dy = -node.y; - distance = Math.sqrt(dx * dx + dy * dy); + // when the touchtime is higher then the max touch time + // or when the moving distance is too much + if(ev.deltaTime > inst.options.tap_max_touchtime || + ev.distance > inst.options.tap_max_distance) { + return; + } - gravityForce = (distance == 0) ? 0 : (gravity / distance); - node.fx = dx * gravityForce; - node.fy = dy * gravityForce; - } - else { - node.fx = 0; - node.fy = 0; + // check if double tap + if(prev && prev.name == 'tap' && + (ev.timeStamp - prev.lastEvent.timeStamp) < inst.options.doubletap_interval && + ev.distance < inst.options.doubletap_distance) { + inst.trigger('doubletap', ev); + did_doubletap = true; + } + + // do a single tap + if(!did_doubletap || inst.options.tap_always) { + Hammer.detection.current.name = 'tap'; + inst.trigger(Hammer.detection.current.name, ev); + } + } } - } }; - - /** - * this function calculates the effects of the springs in the case of unsmooth curves. - * - * @private + * Swipe + * triggers swipe events when the end velocity is above the threshold + * @events swipe, swipeleft, swiperight, swipeup, swipedown */ - exports._calculateSpringForces = function () { - var edgeLength, edge, edgeId; - var dx, dy, fx, fy, springForce, distance; - var edges = this.edges; - - // forces caused by the edges, modelled as springs - for (edgeId in edges) { - if (edges.hasOwnProperty(edgeId)) { - edge = edges[edgeId]; - if (edge.connected) { - // only calculate forces if nodes are in the same sector - if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) { - edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength; - // this implies that the edges between big clusters are longer - edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth; - - dx = (edge.from.x - edge.to.x); - dy = (edge.from.y - edge.to.y); - distance = Math.sqrt(dx * dx + dy * dy); - - if (distance == 0) { - distance = 0.01; - } - - // the 1/distance is so the fx and fy can be calculated without sine or cosine. - springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance; - - fx = dx * springForce; - fy = dy * springForce; + Hammer.gestures.Swipe = { + name: 'swipe', + index: 40, + defaults: { + // set 0 for unlimited, but this can conflict with transform + swipe_max_touches : 1, + swipe_velocity : 0.7 + }, + handler: function swipeGesture(ev, inst) { + if(ev.eventType == Hammer.EVENT_END) { + // max touches + if(inst.options.swipe_max_touches > 0 && + ev.touches.length > inst.options.swipe_max_touches) { + return; + } - edge.from.fx += fx; - edge.from.fy += fy; - edge.to.fx -= fx; - edge.to.fy -= fy; + // when the distance we moved is too small we skip this gesture + // or we can be already in dragging + if(ev.velocityX > inst.options.swipe_velocity || + ev.velocityY > inst.options.swipe_velocity) { + // trigger swipe events + inst.trigger(this.name, ev); + inst.trigger(this.name + ev.direction, ev); + } } - } } - } }; - - /** - * This function calculates the springforces on the nodes, accounting for the support nodes. - * - * @private + * Drag + * Move with x fingers (default 1) around on the page. Blocking the scrolling when + * moving left and right is a good practice. When all the drag events are blocking + * you disable scrolling on that area. + * @events drag, drapleft, dragright, dragup, dragdown */ - exports._calculateSpringForcesWithSupport = function () { - var edgeLength, edge, edgeId, combinedClusterSize; - var edges = this.edges; + Hammer.gestures.Drag = { + name: 'drag', + index: 50, + defaults: { + drag_min_distance : 10, + // set 0 for unlimited, but this can conflict with transform + drag_max_touches : 1, + // prevent default browser behavior when dragging occurs + // be careful with it, it makes the element a blocking element + // when you are using the drag gesture, it is a good practice to set this true + drag_block_horizontal : false, + drag_block_vertical : false, + // drag_lock_to_axis keeps the drag gesture on the axis that it started on, + // It disallows vertical directions if the initial direction was horizontal, and vice versa. + drag_lock_to_axis : false, + // drag lock only kicks in when distance > drag_lock_min_distance + // This way, locking occurs only when the distance has become large enough to reliably determine the direction + drag_lock_min_distance : 25 + }, + triggered: false, + handler: function dragGesture(ev, inst) { + // current gesture isnt drag, but dragged is true + // this means an other gesture is busy. now call dragend + if(Hammer.detection.current.name != this.name && this.triggered) { + inst.trigger(this.name +'end', ev); + this.triggered = false; + return; + } - // forces caused by the edges, modelled as springs - for (edgeId in edges) { - if (edges.hasOwnProperty(edgeId)) { - edge = edges[edgeId]; - if (edge.connected) { - // only calculate forces if nodes are in the same sector - if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) { - if (edge.via != null) { - var node1 = edge.to; - var node2 = edge.via; - var node3 = edge.from; + // max touches + if(inst.options.drag_max_touches > 0 && + ev.touches.length > inst.options.drag_max_touches) { + return; + } - edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength; + switch(ev.eventType) { + case Hammer.EVENT_START: + this.triggered = false; + break; - combinedClusterSize = node1.clusterSize + node3.clusterSize - 2; + case Hammer.EVENT_MOVE: + // when the distance we moved is too small we skip this gesture + // or we can be already in dragging + if(ev.distance < inst.options.drag_min_distance && + Hammer.detection.current.name != this.name) { + return; + } - // this implies that the edges between big clusters are longer - edgeLength += combinedClusterSize * this.constants.clustering.edgeGrowth; - this._calculateSpringForce(node1, node2, 0.5 * edgeLength); - this._calculateSpringForce(node2, node3, 0.5 * edgeLength); - } - } - } - } - } - }; + // we are dragging! + Hammer.detection.current.name = this.name; + // lock drag to axis? + if(Hammer.detection.current.lastEvent.drag_locked_to_axis || (inst.options.drag_lock_to_axis && inst.options.drag_lock_min_distance<=ev.distance)) { + ev.drag_locked_to_axis = true; + } + var last_direction = Hammer.detection.current.lastEvent.direction; + if(ev.drag_locked_to_axis && last_direction !== ev.direction) { + // keep direction on the axis that the drag gesture started on + if(Hammer.utils.isVertical(last_direction)) { + ev.direction = (ev.deltaY < 0) ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN; + } + else { + ev.direction = (ev.deltaX < 0) ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT; + } + } - /** - * This is the code actually performing the calculation for the function above. It is split out to avoid repetition. - * - * @param node1 - * @param node2 - * @param edgeLength - * @private - */ - exports._calculateSpringForce = function (node1, node2, edgeLength) { - var dx, dy, fx, fy, springForce, distance; + // first time, trigger dragstart event + if(!this.triggered) { + inst.trigger(this.name +'start', ev); + this.triggered = true; + } - dx = (node1.x - node2.x); - dy = (node1.y - node2.y); - distance = Math.sqrt(dx * dx + dy * dy); + // trigger normal event + inst.trigger(this.name, ev); - if (distance == 0) { - distance = 0.01; - } + // direction event, like dragdown + inst.trigger(this.name + ev.direction, ev); - // the 1/distance is so the fx and fy can be calculated without sine or cosine. - springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance; + // block the browser events + if( (inst.options.drag_block_vertical && Hammer.utils.isVertical(ev.direction)) || + (inst.options.drag_block_horizontal && !Hammer.utils.isVertical(ev.direction))) { + ev.preventDefault(); + } + break; - fx = dx * springForce; - fy = dy * springForce; + case Hammer.EVENT_END: + // trigger dragend + if(this.triggered) { + inst.trigger(this.name +'end', ev); + } - node1.fx += fx; - node1.fy += fy; - node2.fx -= fx; - node2.fy -= fy; + this.triggered = false; + break; + } + } }; /** - * Load the HTML for the physics config and bind it - * @private + * Transform + * User want to scale or rotate with 2 fingers + * @events transform, pinch, pinchin, pinchout, rotate */ - exports._loadPhysicsConfiguration = function () { - if (this.physicsConfiguration === undefined) { - this.backupConstants = {}; - util.deepExtend(this.backupConstants,this.constants); - - var hierarchicalLayoutDirections = ["LR", "RL", "UD", "DU"]; - this.physicsConfiguration = document.createElement('div'); - this.physicsConfiguration.className = "PhysicsConfiguration"; - this.physicsConfiguration.innerHTML = '' + - '' + - '' + - '' + - '' + - '' + - '' + - '
Simulation Mode:
Barnes HutRepulsionHierarchical
' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '
Options:
' - this.containerElement.parentElement.insertBefore(this.physicsConfiguration, this.containerElement); - this.optionsDiv = document.createElement("div"); - this.optionsDiv.style.fontSize = "14px"; - this.optionsDiv.style.fontFamily = "verdana"; - this.containerElement.parentElement.insertBefore(this.optionsDiv, this.containerElement); - - var rangeElement; - rangeElement = document.getElementById('graph_BH_gc'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_gc', -1, "physics_barnesHut_gravitationalConstant"); - rangeElement = document.getElementById('graph_BH_cg'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_cg', 1, "physics_centralGravity"); - rangeElement = document.getElementById('graph_BH_sc'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sc', 1, "physics_springConstant"); - rangeElement = document.getElementById('graph_BH_sl'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sl', 1, "physics_springLength"); - rangeElement = document.getElementById('graph_BH_damp'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_damp', 1, "physics_damping"); - - rangeElement = document.getElementById('graph_R_nd'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_nd', 1, "physics_repulsion_nodeDistance"); - rangeElement = document.getElementById('graph_R_cg'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_cg', 1, "physics_centralGravity"); - rangeElement = document.getElementById('graph_R_sc'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sc', 1, "physics_springConstant"); - rangeElement = document.getElementById('graph_R_sl'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sl', 1, "physics_springLength"); - rangeElement = document.getElementById('graph_R_damp'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_damp', 1, "physics_damping"); + Hammer.gestures.Transform = { + name: 'transform', + index: 45, + defaults: { + // factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1 + transform_min_scale : 0.01, + // rotation in degrees + transform_min_rotation : 1, + // prevent default browser behavior when two touches are on the screen + // but it makes the element a blocking element + // when you are using the transform gesture, it is a good practice to set this true + transform_always_block : false + }, + triggered: false, + handler: function transformGesture(ev, inst) { + // current gesture isnt drag, but dragged is true + // this means an other gesture is busy. now call dragend + if(Hammer.detection.current.name != this.name && this.triggered) { + inst.trigger(this.name +'end', ev); + this.triggered = false; + return; + } - rangeElement = document.getElementById('graph_H_nd'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance"); - rangeElement = document.getElementById('graph_H_cg'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_cg', 1, "physics_centralGravity"); - rangeElement = document.getElementById('graph_H_sc'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sc', 1, "physics_springConstant"); - rangeElement = document.getElementById('graph_H_sl'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sl', 1, "physics_springLength"); - rangeElement = document.getElementById('graph_H_damp'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_damp', 1, "physics_damping"); - rangeElement = document.getElementById('graph_H_direction'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_direction', hierarchicalLayoutDirections, "hierarchicalLayout_direction"); - rangeElement = document.getElementById('graph_H_levsep'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_levsep', 1, "hierarchicalLayout_levelSeparation"); - rangeElement = document.getElementById('graph_H_nspac'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nspac', 1, "hierarchicalLayout_nodeSpacing"); + // atleast multitouch + if(ev.touches.length < 2) { + return; + } - var radioButton1 = document.getElementById("graph_physicsMethod1"); - var radioButton2 = document.getElementById("graph_physicsMethod2"); - var radioButton3 = document.getElementById("graph_physicsMethod3"); - radioButton2.checked = true; - if (this.constants.physics.barnesHut.enabled) { - radioButton1.checked = true; - } - if (this.constants.hierarchicalLayout.enabled) { - radioButton3.checked = true; - } + // prevent default when two fingers are on the screen + if(inst.options.transform_always_block) { + ev.preventDefault(); + } - var graph_toggleSmooth = document.getElementById("graph_toggleSmooth"); - var graph_repositionNodes = document.getElementById("graph_repositionNodes"); - var graph_generateOptions = document.getElementById("graph_generateOptions"); + switch(ev.eventType) { + case Hammer.EVENT_START: + this.triggered = false; + break; - graph_toggleSmooth.onclick = graphToggleSmoothCurves.bind(this); - graph_repositionNodes.onclick = graphRepositionNodes.bind(this); - graph_generateOptions.onclick = graphGenerateOptions.bind(this); - if (this.constants.smoothCurves == true) { - graph_toggleSmooth.style.background = "#A4FF56"; - } - else { - graph_toggleSmooth.style.background = "#FF8532"; - } + case Hammer.EVENT_MOVE: + var scale_threshold = Math.abs(1-ev.scale); + var rotation_threshold = Math.abs(ev.rotation); + // when the distance we moved is too small we skip this gesture + // or we can be already in dragging + if(scale_threshold < inst.options.transform_min_scale && + rotation_threshold < inst.options.transform_min_rotation) { + return; + } - switchConfigurations.apply(this); + // we are transforming! + Hammer.detection.current.name = this.name; - radioButton1.onchange = switchConfigurations.bind(this); - radioButton2.onchange = switchConfigurations.bind(this); - radioButton3.onchange = switchConfigurations.bind(this); - } - }; + // first time, trigger dragstart event + if(!this.triggered) { + inst.trigger(this.name +'start', ev); + this.triggered = true; + } - /** - * This overwrites the this.constants. - * - * @param constantsVariableName - * @param value - * @private - */ - exports._overWriteGraphConstants = function (constantsVariableName, value) { - var nameArray = constantsVariableName.split("_"); - if (nameArray.length == 1) { - this.constants[nameArray[0]] = value; - } - else if (nameArray.length == 2) { - this.constants[nameArray[0]][nameArray[1]] = value; - } - else if (nameArray.length == 3) { - this.constants[nameArray[0]][nameArray[1]][nameArray[2]] = value; - } - }; + inst.trigger(this.name, ev); // basic transform event + // trigger rotate event + if(rotation_threshold > inst.options.transform_min_rotation) { + inst.trigger('rotate', ev); + } - /** - * this function is bound to the toggle smooth curves button. That is also why it is not in the prototype. - */ - function graphToggleSmoothCurves () { - this.constants.smoothCurves = !this.constants.smoothCurves; - var graph_toggleSmooth = document.getElementById("graph_toggleSmooth"); - if (this.constants.smoothCurves == true) {graph_toggleSmooth.style.background = "#A4FF56";} - else {graph_toggleSmooth.style.background = "#FF8532";} + // trigger pinch event + if(scale_threshold > inst.options.transform_min_scale) { + inst.trigger('pinch', ev); + inst.trigger('pinch'+ ((ev.scale < 1) ? 'in' : 'out'), ev); + } + break; - this._configureSmoothCurves(false); - } + case Hammer.EVENT_END: + // trigger dragend + if(this.triggered) { + inst.trigger(this.name +'end', ev); + } - /** - * this function is used to scramble the nodes - * - */ - function graphRepositionNodes () { - for (var nodeId in this.calculationNodes) { - if (this.calculationNodes.hasOwnProperty(nodeId)) { - this.calculationNodes[nodeId].vx = 0; this.calculationNodes[nodeId].vy = 0; - this.calculationNodes[nodeId].fx = 0; this.calculationNodes[nodeId].fy = 0; + this.triggered = false; + break; + } } - } - if (this.constants.hierarchicalLayout.enabled == true) { - this._setupHierarchicalLayout(); - } - else { - this.repositionNodes(); - } - this.moving = true; - this.start(); - } + }; + /** - * this is used to generate an options file from the playing with physics system. + * Touch + * Called as first, tells the user has touched the screen + * @events touch */ - function graphGenerateOptions () { - var options = "No options are required, default values used."; - var optionsSpecific = []; - var radioButton1 = document.getElementById("graph_physicsMethod1"); - var radioButton2 = document.getElementById("graph_physicsMethod2"); - if (radioButton1.checked == true) { - if (this.constants.physics.barnesHut.gravitationalConstant != this.backupConstants.physics.barnesHut.gravitationalConstant) {optionsSpecific.push("gravitationalConstant: " + this.constants.physics.barnesHut.gravitationalConstant);} - if (this.constants.physics.centralGravity != this.backupConstants.physics.barnesHut.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);} - if (this.constants.physics.springLength != this.backupConstants.physics.barnesHut.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);} - if (this.constants.physics.springConstant != this.backupConstants.physics.barnesHut.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);} - if (this.constants.physics.damping != this.backupConstants.physics.barnesHut.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);} - if (optionsSpecific.length != 0) { - options = "var options = {"; - options += "physics: {barnesHut: {"; - for (var i = 0; i < optionsSpecific.length; i++) { - options += optionsSpecific[i]; - if (i < optionsSpecific.length - 1) { - options += ", " - } - } - options += '}}' - } - if (this.constants.smoothCurves != this.backupConstants.smoothCurves) { - if (optionsSpecific.length == 0) {options = "var options = {";} - else {options += ", "} - options += "smoothCurves: " + this.constants.smoothCurves; - } - if (options != "No options are required, default values used.") { - options += '};' - } - } - else if (radioButton2.checked == true) { - options = "var options = {"; - options += "physics: {barnesHut: {enabled: false}"; - if (this.constants.physics.repulsion.nodeDistance != this.backupConstants.physics.repulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.repulsion.nodeDistance);} - if (this.constants.physics.centralGravity != this.backupConstants.physics.repulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);} - if (this.constants.physics.springLength != this.backupConstants.physics.repulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);} - if (this.constants.physics.springConstant != this.backupConstants.physics.repulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);} - if (this.constants.physics.damping != this.backupConstants.physics.repulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);} - if (optionsSpecific.length != 0) { - options += ", repulsion: {"; - for (var i = 0; i < optionsSpecific.length; i++) { - options += optionsSpecific[i]; - if (i < optionsSpecific.length - 1) { - options += ", " + Hammer.gestures.Touch = { + name: 'touch', + index: -Infinity, + defaults: { + // call preventDefault at touchstart, and makes the element blocking by + // disabling the scrolling of the page, but it improves gestures like + // transforming and dragging. + // be careful with using this, it can be very annoying for users to be stuck + // on the page + prevent_default: false, + + // disable mouse events, so only touch (or pen!) input triggers events + prevent_mouseevents: false + }, + handler: function touchGesture(ev, inst) { + if(inst.options.prevent_mouseevents && ev.pointerType == Hammer.POINTER_MOUSE) { + ev.stopDetect(); + return; } - } - options += '}}' - } - if (optionsSpecific.length == 0) {options += "}"} - if (this.constants.smoothCurves != this.backupConstants.smoothCurves) { - options += ", smoothCurves: " + this.constants.smoothCurves; - } - options += '};' - } - else { - options = "var options = {"; - if (this.constants.physics.hierarchicalRepulsion.nodeDistance != this.backupConstants.physics.hierarchicalRepulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.hierarchicalRepulsion.nodeDistance);} - if (this.constants.physics.centralGravity != this.backupConstants.physics.hierarchicalRepulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);} - if (this.constants.physics.springLength != this.backupConstants.physics.hierarchicalRepulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);} - if (this.constants.physics.springConstant != this.backupConstants.physics.hierarchicalRepulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);} - if (this.constants.physics.damping != this.backupConstants.physics.hierarchicalRepulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);} - if (optionsSpecific.length != 0) { - options += "physics: {hierarchicalRepulsion: {"; - for (var i = 0; i < optionsSpecific.length; i++) { - options += optionsSpecific[i]; - if (i < optionsSpecific.length - 1) { - options += ", "; + + if(inst.options.prevent_default) { + ev.preventDefault(); } - } - options += '}},'; - } - options += 'hierarchicalLayout: {'; - optionsSpecific = []; - if (this.constants.hierarchicalLayout.direction != this.backupConstants.hierarchicalLayout.direction) {optionsSpecific.push("direction: " + this.constants.hierarchicalLayout.direction);} - if (Math.abs(this.constants.hierarchicalLayout.levelSeparation) != this.backupConstants.hierarchicalLayout.levelSeparation) {optionsSpecific.push("levelSeparation: " + this.constants.hierarchicalLayout.levelSeparation);} - if (this.constants.hierarchicalLayout.nodeSpacing != this.backupConstants.hierarchicalLayout.nodeSpacing) {optionsSpecific.push("nodeSpacing: " + this.constants.hierarchicalLayout.nodeSpacing);} - if (optionsSpecific.length != 0) { - for (var i = 0; i < optionsSpecific.length; i++) { - options += optionsSpecific[i]; - if (i < optionsSpecific.length - 1) { - options += ", " + + if(ev.eventType == Hammer.EVENT_START) { + inst.trigger(this.name, ev); } - } - options += '}' } - else { - options += "enabled:true}"; - } - options += '};' - } - + }; - this.optionsDiv.innerHTML = options; - } /** - * this is used to switch between barnesHut, repulsion and hierarchical. - * + * Release + * Called as last, tells the user has released the screen + * @events release */ - function switchConfigurations () { - var ids = ["graph_BH_table", "graph_R_table", "graph_H_table"]; - var radioButton = document.querySelector('input[name="graph_physicsMethod"]:checked').value; - var tableId = "graph_" + radioButton + "_table"; - var table = document.getElementById(tableId); - table.style.display = "block"; - for (var i = 0; i < ids.length; i++) { - if (ids[i] != tableId) { - table = document.getElementById(ids[i]); - table.style.display = "none"; - } - } - this._restoreNodes(); - if (radioButton == "R") { - this.constants.hierarchicalLayout.enabled = false; - this.constants.physics.hierarchicalRepulsion.enabled = false; - this.constants.physics.barnesHut.enabled = false; - } - else if (radioButton == "H") { - if (this.constants.hierarchicalLayout.enabled == false) { - this.constants.hierarchicalLayout.enabled = true; - this.constants.physics.hierarchicalRepulsion.enabled = true; - this.constants.physics.barnesHut.enabled = false; - this._setupHierarchicalLayout(); + Hammer.gestures.Release = { + name: 'release', + index: Infinity, + handler: function releaseGesture(ev, inst) { + if(ev.eventType == Hammer.EVENT_END) { + inst.trigger(this.name, ev); + } } - } - else { - this.constants.hierarchicalLayout.enabled = false; - this.constants.physics.hierarchicalRepulsion.enabled = false; - this.constants.physics.barnesHut.enabled = true; - } - this._loadSelectedForceSolver(); - var graph_toggleSmooth = document.getElementById("graph_toggleSmooth"); - if (this.constants.smoothCurves == true) {graph_toggleSmooth.style.background = "#A4FF56";} - else {graph_toggleSmooth.style.background = "#FF8532";} - this.moving = true; - this.start(); - } - - - /** - * this generates the ranges depending on the iniital values. - * - * @param id - * @param map - * @param constantsVariableName - */ - function showValueOfRange (id,map,constantsVariableName) { - var valueId = id + "_value"; - var rangeValue = document.getElementById(id).value; - - if (map instanceof Array) { - document.getElementById(valueId).value = map[parseInt(rangeValue)]; - this._overWriteGraphConstants(constantsVariableName,map[parseInt(rangeValue)]); - } - else { - document.getElementById(valueId).value = parseInt(map) * parseFloat(rangeValue); - this._overWriteGraphConstants(constantsVariableName, parseInt(map) * parseFloat(rangeValue)); - } + }; - if (constantsVariableName == "hierarchicalLayout_direction" || - constantsVariableName == "hierarchicalLayout_levelSeparation" || - constantsVariableName == "hierarchicalLayout_nodeSpacing") { - this._setupHierarchicalLayout(); - } - this.moving = true; - this.start(); + // node export + if(typeof module === 'object' && typeof module.exports === 'object'){ + module.exports = Hammer; } + // just window export + else { + window.Hammer = Hammer; + // requireJS module definition + if(typeof window.define === 'function' && window.define.amd) { + window.define('hammer', [], function() { + return Hammer; + }); + } + } + })(this); /***/ }, /* 51 */ diff --git a/examples/network/index.html b/examples/network/index.html index 3a555140..c360c653 100644 --- a/examples/network/index.html +++ b/examples/network/index.html @@ -37,6 +37,8 @@

23_hierarchical_layout.html

24_hierarchical_layout_userdefined.html

25_physics_configuration.html

+

26_staticSmoothCurves.html

+

27_world_cup_network.html

graphviz_gallery.html

From 01856c744ee6223d14f7cc15b73a26eb3cd3eb65 Mon Sep 17 00:00:00 2001 From: Alex de Mulder Date: Fri, 11 Jul 2014 15:34:29 +0200 Subject: [PATCH 3/3] updated docs and example --- docs/network.html | 10 +- examples/network/27_world_cup_network.html | 20027 ++++++++++--------- 2 files changed, 10029 insertions(+), 10008 deletions(-) diff --git a/docs/network.html b/docs/network.html index eec39fef..ef3866ff 100644 --- a/docs/network.html +++ b/docs/network.html @@ -931,11 +931,11 @@ var options = { relative positions of the to and from nodes. This is computationally cheaper but there is no self organisation. - smoothCurves.adaptive - Boolean - true - This option only affects NONdynamic smooth curves. Adaptivity allows the smoothness to transition nicely if there is node movement. The downside is that the maximum roundness is less if adaptivity is enabled. Adaptivity can be disabled for static network. - Sudden switching between orientations can happen on an animated network. + smoothCurves.type + String + "continuous" + This option only affects NONdynamic smooth curves. The supported types are: continuous, discrete, diagonalCross, straightCross, horizontal, vertical. The effects of these types + are shown in examples 26 and 27 smoothCurves.roundness diff --git a/examples/network/27_world_cup_network.html b/examples/network/27_world_cup_network.html index 3b305871..6f4d7844 100644 --- a/examples/network/27_world_cup_network.html +++ b/examples/network/27_world_cup_network.html @@ -41,9 +41,15 @@ Smooth curve type:
+inheritColor option: +
Roundness (0..1):
-Hide edges on drag: +Hide edges on drag:
Hide nodes on drag:
@@ -60,10013 +66,10027 @@ Hide nodes on drag: hideEdgesOnDrag.onchange = update; var hideNodesOnDrag = document.getElementById("hideNodesOnDrag"); hideNodesOnDrag.onchange = update; + var inheritColor = document.getElementById("inheritColor"); + inheritColor.onchange = redrawAll; + + var network; - // create an array with nodes - var nodes = [ -{id: 1, label: 'Abdelmoumene Djabou', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Club Africain', value: 22, group: 24, x: -1392.5499, y: 1124.1614}, -{id: 2, label: 'Abel Aguilar', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'Toulouse', value: 24, group: 11, x: -660.82574, y: 1009.18976}, -{id: 3, label: 'Abel Hernández', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Palermo', value: 22, group: 6, x: -85.6025, y: -6.6782646}, -{id: 4, label: 'Adam Kwarasey', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Strømsgodset', value: 22, group: 5, x: 427.39853, y: 1398.1719}, -{id: 5, label: 'Adam Lallana', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Southampton', value: 26, group: 28, x: -133.68427, y: -732.50476}, -{id: 6, label: 'Adam Taggart', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Newcastle Jets', value: 22, group: 12, x: 2042.4272, y: -579.6042}, -{id: 7, label: 'Admir Mehmedi', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'SC Freiburg', value: 24, group: 0, x: 126.91814, y: 115.84123}, -{id: 8, label: 'Adnan Januzaj', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Manchester United', value: 34, group: 28, x: -638.503, y: -663.07904}, -{id: 9, label: 'Adrián Bone', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'El Nacional', value: 22, group: 4, x: -1657.1593, y: -645.2429}, -{id: 10, label: 'Adrián Ramos', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'Hertha BSC', value: 23, group: 11, x: -712.13385, y: 1053.3159}, -{id: 11, label: 'Afriyie Acquah', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Parma', value: 26, group: 5, x: 358.25735, y: 1238.4801}, -{id: 12, label: 'Agustín Orión', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Boca Juniors', value: 22, group: 19, x: -1115.8746, y: 250.34308}, -{id: 13, label: 'Ahmad Alenemeh', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Naft Tehran', value: 22, group: 1, x: 2028.4565, y: 1067.9126}, -{id: 14, label: 'Ahmed Musa', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'CSKA Moscow', value: 27, group: 14, x: -341.64163, y: -1640.5049}, -{id: 15, label: 'Aïssa Mandi', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Reims', value: 22, group: 24, x: -1380.8287, y: 1169.2931}, -{id: 16, label: 'Alan Dzagoev', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'CSKA Moscow', value: 23, group: 2, x: -1268.165, y: -1469.7052}, -{id: 17, label: 'Alan Pulido', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'UANL', value: 22, group: 21, x: -2016.3092, y: 442.13663}, -{id: 18, label: 'Albert Adomah', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Middlesbrough', value: 23, group: 5, x: 449.02316, y: 1183.7205}, -{id: 19, label: 'Alberto Aquilani', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Fiorentina', value: 24, group: 3, x: 51.16946, y: 883.6703}, -{id: 20, label: 'Alejandro Bedoya', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'Nantes', value: 22, group: 26, x: 784.4289, y: -1547.6515}, -{id: 21, label: 'Aleksandr Kerzhakov', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'Zenit Saint Petersburg', value: 26, group: 2, x: -1228.8892, y: -1267.067}, -{id: 22, label: 'Aleksandr Kokorin', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'Dynamo Moscow', value: 23, group: 2, x: -1414.3739, y: -1377.2596}, -{id: 23, label: 'Aleksandr Samedov', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'Lokomotiv Moscow', value: 23, group: 2, x: -1362.3624, y: -1347.75}, -{id: 24, label: 'Aleksei Ionov', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'Dynamo Moscow', value: 23, group: 2, x: -1428.0071, y: -1427.2177}, -{id: 25, label: 'Aleksei Kozlov', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'Dynamo Moscow', value: 23, group: 2, x: -1463.2527, y: -1376.6138}, -{id: 26, label: 'Alessio Cerci', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Torino', value: 23, group: 3, x: 276.62708, y: 826.51605}, -{id: 27, label: 'Alex Oxlade-Chamberlain', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Arsenal', value: 30, group: 28, x: -56.50232, y: -825.3445}, -{id: 28, label: 'Alex Song', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Barcelona', value: 37, group: 17, x: -256.07828, y: 56.990772}, -{id: 29, label: 'Alex Wilkinson', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Jeonbuk Hyundai Motors', value: 22, group: 12, x: 2120.3818, y: -724.748}, -{id: 30, label: 'Alexander Domínguez', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'LDU Quito', value: 22, group: 4, x: -1643.0283, y: -689.7502}, -{id: 31, label: 'Alexander Mejía', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'Atlético Nacional', value: 22, group: 11, x: -761.32623, y: 1152.3298}, -{id: 32, label: 'Alexandros Tziolis', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'Kayserispor', value: 22, group: 15, x: 1617.3293, y: 542.81915}, -{id: 33, label: 'Alexis Sánchez', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Barcelona', value: 37, group: 18, x: -613.0529, y: 828.08685}, -{id: 34, label: 'Alfredo Talavera', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'Toluca', value: 22, group: 21, x: -1995.7101, y: 401.94843}, -{id: 35, label: 'Alireza Haghighi', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Sporting Covilhã', value: 22, group: 1, x: 1910.1731, y: 1066.8309}, -{id: 36, label: 'Alireza Jahanbakhsh', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'NEC', value: 22, group: 1, x: 1942.0732, y: 1034.9001}, -{id: 37, label: 'Allan Nyom', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Granada', value: 24, group: 17, x: 381.53027, y: 285.77576}, -{id: 38, label: 'Álvaro González', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Lazio', value: 28, group: 6, x: 13.4137335, y: -43.777435}, -{id: 39, label: 'Álvaro Pereira', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'São Paulo', value: 22, group: 6, x: -93.8017, y: 34.243332}, -{id: 40, label: 'Amir Hossein Sadeghi', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Esteghlal', value: 22, group: 1, x: 1990.1855, y: 1052.6255}, -{id: 41, label: 'Andranik Teymourian', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Esteghlal', value: 22, group: 1, x: 1940.6577, y: 1114.8914}, -{id: 42, label: 'André Almeida', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Benfica', value: 25, group: 8, x: -733.05725, y: 266.987}, -{id: 43, label: 'André Ayew', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Marseille', value: 24, group: 5, x: 486.66187, y: 1226.3735}, -{id: 44, label: 'André Schürrle', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Chelsea', value: 33, group: 13, x: 130.8471, y: -528.93024}, -{id: 45, label: 'Andrea Barzagli', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Juventus', value: 28, group: 3, x: 109.97049, y: 937.16266}, -{id: 46, label: 'Andrea Pirlo', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Juventus', value: 28, group: 3, x: 108.0534, y: 870.1171}, -{id: 47, label: 'Andreas Samaris', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'Olympiacos', value: 23, group: 15, x: 1692.9755, y: 475.92816}, -{id: 48, label: 'Andrei Semyonov', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'Terek Grozny', value: 22, group: 2, x: -1427.7258, y: -1522.6016}, -{id: 49, label: 'Andrés Guardado', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'Bayer Leverkusen', value: 24, group: 21, x: -1822.0682, y: 449.03262}, -{id: 50, label: 'Andrés Iniesta', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Barcelona', value: 31, group: 23, x: -1067.9244, y: -187.44284}, -{id: 51, label: 'Andrey Yeshchenko', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'Anzhi Makhachkala', value: 22, group: 2, x: -1412.1168, y: -1477.2361}, -{id: 52, label: 'Andy Najar', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Anderlecht', value: 23, group: 7, x: 1494.2014, y: -1172.4867}, -{id: 53, label: 'Anel Hadžic', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'Sturm Graz', value: 22, group: 20, x: 1149.5178, y: -490.41513}, -{id: 54, label: 'Ángel di María', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Real Madrid', value: 33, group: 19, x: -968.5764, y: 161.48494}, -{id: 55, label: 'Ante Rebic', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Fiorentina', value: 24, group: 25, x: -308.12177, y: 744.399}, -{id: 56, label: 'Anthony Vanden Borre', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Anderlecht', value: 23, group: 28, x: -577.6633, y: -888.84265}, -{id: 57, label: 'Antoine Griezmann', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Real Sociedad', value: 25, group: 16, x: 63.922184, y: -173.65816}, -{id: 58, label: 'Antonio Candreva', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Lazio', value: 28, group: 3, x: 180.96414, y: 574.7693}, -{id: 59, label: 'Antonio Cassano', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Parma', value: 24, group: 3, x: 193.04764, y: 758.9299}, -{id: 60, label: 'Antonio Valencia (c)', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Manchester United', value: 35, group: 4, x: -1293.8275, y: -612.48834}, -{id: 61, label: 'Arjen Robben', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Bayern Munich', value: 35, group: 22, x: 630.80566, y: -143.44237}, -{id: 62, label: 'Aron Jóhannsson', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'AZ', value: 22, group: 26, x: 819.32007, y: -1520.0212}, -{id: 63, label: 'Arthur Boka', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'VfB Stuttgart', value: 25, group: 9, x: 447.86835, y: -798.1806}, -{id: 64, label: 'Arturo Vidal', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Juventus', value: 32, group: 18, x: -116.507996, y: 1233.55}, -{id: 65, label: 'Asamoah Gyan (c)', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Al-Ain', value: 22, group: 5, x: 384.49658, y: 1385.8724}, -{id: 66, label: 'Ashkan Dejagah', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Fulham', value: 24, group: 1, x: 1842.1604, y: 978.62915}, -{id: 67, label: 'Asmir Avdukic', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'Borac Banja Luka', value: 22, group: 20, x: 1126.5564, y: -529.6863}, -{id: 68, label: 'Asmir Begovic', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'Stoke City', value: 25, group: 20, x: 1126.9225, y: -656.7364}, -{id: 69, label: 'Atsuto Uchida', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'Schalke 04', value: 28, group: 27, x: 789.175, y: 479.11423}, -{id: 70, label: 'Augusto Fernández', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Celta Vigo', value: 23, group: 19, x: -1096.7728, y: 332.52386}, -{id: 71, label: 'Aurélien Chedjou', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Galatasaray', value: 26, group: 17, x: 479.9816, y: 42.06589}, -{id: 72, label: 'Austin Ejide', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Hapoel Beer Sheva', value: 22, group: 14, x: -127.8801, y: -1587.7189}, -{id: 73, label: 'Avdija Vršajevic', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'Hajduk Split', value: 22, group: 20, x: 1155.9982, y: -446.01266}, -{id: 74, label: 'Axel Witsel', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Zenit Saint Petersburg', value: 28, group: 28, x: -844.52124, y: -894.0247}, -{id: 75, label: 'Azubuike Egwuekwe', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Warri Wolves', value: 22, group: 14, x: -40.194813, y: -1612.7229}, -{id: 76, label: 'Bacary Sagna', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Arsenal', value: 29, group: 16, x: -102.573074, y: -365.21664}, -{id: 77, label: 'Bailey Wright', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Preston North End', value: 22, group: 12, x: 2074.923, y: -613.972}, -{id: 78, label: 'Bakhtiar Rahmani', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Foolad', value: 22, group: 1, x: 2063.0938, y: 1033.574}, -{id: 79, label: 'Bastian Schweinsteiger', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Bayern Munich', value: 29, group: 13, x: 244.85414, y: -373.98276}, -{id: 80, label: 'Ben Foster', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'West Bromwich Albion', value: 23, group: 28, x: -170.48405, y: -869.56903}, -{id: 81, label: 'Ben Halloran', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Fortuna Düsseldorf', value: 23, group: 12, x: 1954.242, y: -623.5981}, -{id: 82, label: 'Benedikt Höwedes', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Schalke 04', value: 27, group: 13, x: 472.64325, y: -229.06421}, -{id: 83, label: 'Benjamin Moukandjo', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Nancy', value: 22, group: 17, x: 415.3849, y: 99.65612}, -{id: 84, label: 'Benoît Assou-Ekotto', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Queens Park Rangers', value: 23, group: 17, x: 484.1712, y: 273.5127}, -{id: 85, label: 'Bernard', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Shakhtar Donetsk', value: 24, group: 23, x: -458.8, y: -206.65053}, -{id: 86, label: 'Beto', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Sevilla', value: 25, group: 8, x: -614.7038, y: 392.89618}, -{id: 87, label: 'Blaise Matuidi', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Paris Saint-Germain', value: 29, group: 16, x: -108.933846, y: -90.56801}, -{id: 88, label: 'Blerim Džemaili', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'Napoli', value: 31, group: 0, x: -243.03868, y: 290.13797}, -{id: 89, label: 'Boubacar Barry', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Lokeren', value: 22, group: 9, x: 488.79492, y: -907.9203}, -{id: 90, label: 'Brad Davis', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'Houston Dynamo', value: 23, group: 26, x: 915.66956, y: -1565.8953}, -{id: 91, label: 'Brad Guzan', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'Aston Villa', value: 23, group: 26, x: 829.8172, y: -1411.8826}, -{id: 92, label: 'Brayan Beckeles', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Olimpia', value: 22, group: 7, x: 1616.757, y: -1172.5592}, -{id: 93, label: 'Bruno Alves', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Fenerbahçe', value: 25, group: 8, x: -538.8344, y: 183.03185}, -{id: 94, label: 'Bruno Martins Indi', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Feyenoord', value: 22, group: 22, x: 870.94403, y: 71.02484}, -{id: 95, label: 'Bryan Ruiz (c)', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'PSV', value: 25, group: 29, x: 2006.2959, y: 332.36353}, -{id: 96, label: 'Camilo Vargas', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'Santa Fe', value: 23, group: 11, x: -870.7738, y: 1102.7423}, -{id: 97, label: 'Carl Medjani', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Valenciennes', value: 23, group: 24, x: -1275.9651, y: 1205.1012}, -{id: 98, label: 'Carlo Costly', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Real España', value: 22, group: 7, x: 1569.5697, y: -1167.269}, -{id: 99, label: 'Carlos Bacca', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'Sevilla', value: 25, group: 11, x: -687.1921, y: 1106.8958}, -{id: 100, label: 'Carlos Carbonero', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'River Plate', value: 22, group: 11, x: -742.21783, y: 1199.1262}, -{id: 101, label: 'Carlos Carmona', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Atalanta', value: 23, group: 18, x: -345.68073, y: 1473.0652}, -{id: 102, label: 'Carlos Gruezo', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'VfB Stuttgart', value: 25, group: 4, x: -1417.159, y: -636.35205}, -{id: 103, label: 'Carlos Peña', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'León', value: 22, group: 21, x: -2037.2489, y: 386.77597}, -{id: 104, label: 'Carlos Salcido', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'UANL', value: 22, group: 21, x: -2011.8602, y: 347.69363}, -{id: 105, label: 'Carlos Sánchez', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'Elche', value: 22, group: 11, x: -775.67804, y: 1232.4089}, -{id: 106, label: 'Carlos Valdés', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'San Lorenzo', value: 22, group: 11, x: -788.68494, y: 1186.096}, -{id: 107, label: 'Cédric Djeugoué', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Coton Sport', value: 22, group: 17, x: 458.03027, y: 113.75822}, -{id: 108, label: 'Cédric Si Mohamed', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'CS Constantine', value: 22, group: 24, x: -1432.4459, y: 1140.2423}, -{id: 109, label: 'Celso Borges', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'AIK', value: 22, group: 29, x: 2214.5396, y: 283.79788}, -{id: 110, label: 'César Azpilicueta', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Chelsea', value: 32, group: 23, x: -780.85876, y: -518.6595}, -{id: 111, label: 'Cesc Fàbregas', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Barcelona', value: 31, group: 23, x: -1070.0735, y: -271.46603}, -{id: 112, label: 'Charles Aránguiz', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Internacional', value: 22, group: 18, x: -251.59665, y: 1476.4546}, -{id: 113, label: 'Charles Itandje', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Konyaspor', value: 23, group: 17, x: 514.87463, y: 203.30963}, -{id: 114, label: 'Cheick Tioté', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Newcastle United', value: 27, group: 9, x: 389.42743, y: -827.5475}, -{id: 115, label: 'Chigozie Agbim', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Gombe United', value: 22, group: 14, x: -67.006065, y: -1575.516}, -{id: 116, label: 'Chris Smalling', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Manchester United', value: 32, group: 28, x: -375.02072, y: -737.6564}, -{id: 117, label: 'Chris Wondolowski', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'San Jose Earthquakes', value: 23, group: 26, x: 915.553, y: -1512.6752}, -{id: 118, label: 'Christian Atsu', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Vitesse', value: 23, group: 5, x: 298.6339, y: 1290.5527}, -{id: 119, label: 'Christian Bolaños', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Copenhagen', value: 22, group: 29, x: 2234.7017, y: 376.9046}, -{id: 120, label: 'Christian Noboa', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Dynamo Moscow', value: 28, group: 4, x: -1672.2358, y: -885.3366}, -{id: 121, label: 'Christian Stuani', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Espanyol', value: 23, group: 6, x: -159.9744, y: 40.993885}, -{id: 122, label: 'Christoph Kramer', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Borussia Mönchengladbach', value: 23, group: 13, x: 422.9451, y: -364.46622}, -{id: 123, label: 'Ciro Immobile', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Torino', value: 23, group: 3, x: 317.4282, y: 794.25037}, -{id: 124, label: 'Claudio Bravo (c)', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Real Sociedad', value: 25, group: 18, x: -193.70801, y: 1267.7544}, -{id: 125, label: 'Claudio Marchisio', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Juventus', value: 28, group: 3, x: 71.69534, y: 813.5998}, -{id: 126, label: 'Clint Dempsey (c)', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'Seattle Sounders FC', value: 22, group: 26, x: 742.0546, y: -1547.4186}, -{id: 127, label: 'Constant Djakpa', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Eintracht Frankfurt', value: 23, group: 9, x: 513.1434, y: -809.9959}, -{id: 128, label: 'Cristian Gamboa', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Rosenborg', value: 23, group: 29, x: 2154.0825, y: 199.01004}, -{id: 129, label: 'Cristian Rodríguez', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Atlético Madrid', value: 28, group: 6, x: -272.89346, y: -76.41096}, -{id: 130, label: 'Cristián Zapata', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'Milan', value: 29, group: 11, x: -503.784, y: 1159.0504}, -{id: 131, label: 'Cristiano Ronaldo (c)', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Real Madrid', value: 31, group: 8, x: -705.8994, y: 163.73811}, -{id: 132, label: 'Cristopher Toselli', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Universidad Católica', value: 22, group: 18, x: -291.25885, y: 1453.383}, -{id: 133, label: 'Daley Blind', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Ajax', value: 22, group: 22, x: 865.13696, y: -4.895512}, -{id: 134, label: 'DaMarcus Beasley', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'Puebla', value: 22, group: 26, x: 860.4318, y: -1509.4606}, -{id: 135, label: 'Dani Alves', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Barcelona', value: 36, group: 23, x: -742.1678, y: -271.698}, -{id: 136, label: 'Daniel Cambronero', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Herediano', value: 22, group: 29, x: 2228.9766, y: 327.5744}, -{id: 137, label: 'Daniel Davari', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Eintracht Braunschweig', value: 23, group: 1, x: 1905.6099, y: 955.88916}, -{id: 138, label: 'Daniel Opare', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Standard Liège', value: 24, group: 5, x: 399.65134, y: 1199.5255}, -{id: 139, label: 'Daniel Sturridge', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Liverpool', value: 27, group: 28, x: -202.59894, y: -933.40094}, -{id: 140, label: 'Daniel Van Buyten', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Bayern Munich', value: 35, group: 28, x: -361.6232, y: -626.74445}, -{id: 141, label: 'Daniele De Rossi', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Roma', value: 26, group: 3, x: 294.1721, y: 656.48535}, -{id: 142, label: 'Danijel Pranjic', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Panathinaikos', value: 23, group: 25, x: -193.00035, y: 612.0998}, -{id: 143, label: 'Danijel Subašic', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'AS Monaco', value: 25, group: 25, x: -426.1968, y: 636.2631}, -{id: 144, label: 'Danny Welbeck', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Manchester United', value: 32, group: 28, x: -294.47705, y: -689.56665}, -{id: 145, label: 'Dante', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Bayern Munich', value: 35, group: 23, x: -212.9895, y: -416.65964}, -{id: 146, label: 'Dany Nounkeu', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Be?ikta?', value: 24, group: 17, x: 382.6164, y: 41.81477}, -{id: 147, label: 'Darijo Srna (c)', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Shakhtar Donetsk', value: 23, group: 25, x: -317.20358, y: 580.4689}, -{id: 148, label: 'Dario Vidošic', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Sion', value: 22, group: 12, x: 2016.2832, y: -666.32526}, -{id: 149, label: 'Daryl Janmaat', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Feyenoord', value: 22, group: 22, x: 832.52924, y: 28.84025}, -{id: 150, label: 'David de Gea', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Manchester United', value: 34, group: 23, x: -916.8024, y: -469.95193}, -{id: 151, label: 'David Luiz', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Chelsea', value: 30, group: 23, x: -401.12976, y: -483.5873}, -{id: 152, label: 'David Myrie', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Herediano', value: 22, group: 29, x: 2254.471, y: 256.6007}, -{id: 153, label: 'David Ospina', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'Nice', value: 22, group: 11, x: -821.8875, y: 1214.6177}, -{id: 154, label: 'David Silva', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Manchester City', value: 31, group: 23, x: -782.84827, y: -359.3023}, -{id: 155, label: 'David Villa', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Atlético Madrid', value: 27, group: 23, x: -854.8254, y: -313.94424}, -{id: 156, label: 'DeAndre Yedlin', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'Seattle Sounders FC', value: 22, group: 26, x: 776.44666, y: -1500.7616}, -{id: 157, label: 'Dejan Lovren', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Southampton', value: 28, group: 25, x: -235.10854, y: 422.88907}, -{id: 158, label: 'Denis Glushakov', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'Spartak Moscow', value: 22, group: 2, x: -1381.3909, y: -1518.6675}, -{id: 159, label: 'Didier Drogba (c)', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Galatasaray', value: 26, group: 9, x: 598.48517, y: -735.1734}, -{id: 160, label: 'Didier Ya Konan', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Hannover 96', value: 24, group: 9, x: 543.872, y: -767.347}, -{id: 161, label: 'Didier Zokora', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Trabzonspor', value: 22, group: 9, x: 526.23566, y: -881.0933}, -{id: 162, label: 'Diego Benaglio', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'VfL Wolfsburg', value: 27, group: 0, x: -65.30554, y: 256.20117}, -{id: 163, label: 'Diego Calvo', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Vålerenga', value: 22, group: 29, x: 2308.558, y: 341.58264}, -{id: 164, label: 'Diego Costa', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Atlético Madrid', value: 27, group: 23, x: -946.3432, y: -379.19135}, -{id: 165, label: 'Diego Forlán', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Cerezo Osaka', value: 24, group: 6, x: 22.544487, y: 32.103252}, -{id: 166, label: 'Diego Godín', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Atlético Madrid', value: 28, group: 6, x: -229.68459, y: -28.488848}, -{id: 167, label: 'Diego Lugano (c)', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'West Bromwich Albion', value: 23, group: 6, x: -32.813736, y: -13.457554}, -{id: 168, label: 'Diego Pérez', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Bologna', value: 24, group: 6, x: 71.02754, y: 37.87593}, -{id: 169, label: 'Diego Reyes', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'Porto', value: 29, group: 21, x: -1751.0813, y: 432.33847}, -{id: 170, label: 'Dimitris Salpingidis', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'PAOK', value: 22, group: 15, x: 1578.1974, y: 570.63684}, -{id: 171, label: 'Dirk Kuyt', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Fenerbahçe', value: 26, group: 22, x: 698.83246, y: -15.171172}, -{id: 172, label: 'Divock Origi', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Lille', value: 25, group: 28, x: -634.9317, y: -895.1274}, -{id: 173, label: 'Djamel Mesbah', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Livorno', value: 22, group: 24, x: -1360.7583, y: 1211.4519}, -{id: 174, label: 'Dmitri Kombarov', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'Spartak Moscow', value: 22, group: 2, x: -1369.3798, y: -1467.8458}, -{id: 175, label: 'Domagoj Vida', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Dynamo Kyiv', value: 24, group: 25, x: -257.23795, y: 568.68097}, -{id: 176, label: 'Donis Escober', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Olimpia', value: 22, group: 7, x: 1653.151, y: -1192.2112}, -{id: 177, label: 'Dries Mertens', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Napoli', value: 33, group: 28, x: -646.4434, y: -473.2636}, -{id: 178, label: 'Edder Delgado', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Real España', value: 22, group: 7, x: 1622.0984, y: -1283.4814}, -{id: 179, label: 'Eden Hazard', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Chelsea', value: 33, group: 28, x: -567.4557, y: -819.40875}, -{id: 180, label: 'Éder', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Braga', value: 22, group: 8, x: -652.50696, y: 328.93912}, -{id: 181, label: 'Éder Álvarez Balanta', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'River Plate', value: 22, group: 11, x: -862.32965, y: 1190.2361}, -{id: 182, label: 'Edgar Salli', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Lens', value: 22, group: 17, x: 416.1859, y: 196.34885}, -{id: 183, label: 'Edin Džeko', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'Manchester City', value: 31, group: 20, x: 747.8557, y: -487.7818}, -{id: 184, label: 'Edin Višca', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + '?stanbul Ba?ak?ehir', value: 22, group: 20, x: 1198.7845, y: -465.6674}, -{id: 185, label: 'Edinson Cavani', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Paris Saint-Germain', value: 31, group: 6, x: -109.8151, y: 97.26505}, -{id: 186, label: 'Édison Méndez', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Santa Fe', value: 23, group: 4, x: -1680.7289, y: -523.78754}, -{id: 187, label: 'Eduardo da Silva', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Shakhtar Donetsk', value: 23, group: 25, x: -364.4046, y: 586.62573}, -{id: 188, label: 'Eduardo dos Reis Carvalho', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Braga', value: 22, group: 8, x: -685.56335, y: 299.7952}, -{id: 189, label: 'Eduardo Vargas', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Valencia', value: 26, group: 18, x: -348.8911, y: 1339.4359}, -{id: 190, label: 'Efe Ambrose', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Celtic', value: 25, group: 14, x: 91.53676, y: -1502.4221}, -{id: 191, label: 'Egidio Arévalo Ríos', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Morelia', value: 23, group: 6, x: -140.449, y: -11.467088}, -{id: 192, label: 'Ehsan Hajsafi', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Sepahan', value: 22, group: 1, x: 1992.8684, y: 1102.4463}, -{id: 193, label: 'Eiji Kawashima', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'Standard Liège', value: 24, group: 27, x: 599.24896, y: 588.35046}, -{id: 194, label: 'Ejike Uzoenyi', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Enugu Rangers', value: 22, group: 14, x: -90.413765, y: -1613.6277}, -{id: 195, label: 'El Arbi Hillel Soudani', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Dinamo Zagreb', value: 23, group: 24, x: -1331.9408, y: 1124.3699}, -{id: 196, label: 'Eliaquim Mangala', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Porto', value: 30, group: 16, x: -347.64447, y: -15.025993}, -{id: 197, label: 'Emilio Izaguirre', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Celtic', value: 25, group: 7, x: 1455.9241, y: -1104.4338}, -{id: 198, label: 'Emir Spahic (c)', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'Bayer Leverkusen', value: 24, group: 20, x: 1039.7502, y: -336.38666}, -{id: 199, label: 'Emmanuel Agyemang-Badu', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Udinese', value: 23, group: 5, x: 311.23798, y: 1367.9753}, -{id: 200, label: 'Emmanuel Emenike', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Fenerbahçe', value: 26, group: 14, x: -64.248405, y: -1362.0144}, -{id: 201, label: 'Enner Valencia', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Pachuca', value: 22, group: 4, x: -1712.6265, y: -633.4451}, -{id: 202, label: 'Enzo Pérez', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Benfica', value: 25, group: 19, x: -1057.396, y: 279.50247}, -{id: 203, label: 'Erik Durm', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Borussia Dortmund', value: 24, group: 13, x: 553.0518, y: -438.38715}, -{id: 204, label: 'Ermin Bicakcic', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'Eintracht Braunschweig', value: 23, group: 20, x: 1292.2596, y: -362.45374}, -{id: 205, label: 'Essaïd Belkalem', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Watford', value: 23, group: 24, x: -1238.1655, y: 1250.7357}, -{id: 206, label: 'Esteban Granados', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Herediano', value: 22, group: 29, x: 2281.05, y: 393.73032}, -{id: 207, label: 'Esteban Paredes', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Colo-Colo', value: 22, group: 18, x: -262.22748, y: 1531.8533}, -{id: 208, label: 'Eugene Galekovic', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Adelaide United', value: 22, group: 12, x: 2152.1602, y: -634.9465}, -{id: 209, label: 'Eugenio Mena', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Santos', value: 22, group: 18, x: -294.9122, y: 1499.1805}, -{id: 210, label: 'Eyong Enoh', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Antalyaspor', value: 22, group: 17, x: 420.98795, y: 149.03363}, -{id: 211, label: 'Ezequiel Garay', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Benfica', value: 25, group: 19, x: -1064.4406, y: 219.37395}, -{id: 212, label: 'Ezequiel Lavezzi', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Paris Saint-Germain', value: 31, group: 19, x: -846.7565, y: 254.65596}, -{id: 213, label: 'Fabian Johnson', title: 'Country: ' + 'United States' + '
' + 'Team: ' + '1899 Hoffenheim', value: 23, group: 26, x: 879.29755, y: -1453.8761}, -{id: 214, label: 'Fabián Orellana', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Celta Vigo', value: 23, group: 18, x: -331.13403, y: 1411.2639}, -{id: 215, label: 'Fabian Schär', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'Basel', value: 25, group: 0, x: 38.159084, y: 161.5354}, -{id: 216, label: 'Fábio Coentrão', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Real Madrid', value: 31, group: 8, x: -620.60266, y: 152.43254}, -{id: 217, label: 'Fabrice Olinga', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Zulte Waregem', value: 23, group: 17, x: 342.78528, y: 88.49571}, -{id: 218, label: 'Faouzi Ghoulam', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Napoli', value: 33, group: 24, x: -1163.7886, y: 887.72974}, -{id: 219, label: 'Faryd Mondragón', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'Deportivo Cali', value: 22, group: 11, x: -825.1312, y: 1158.5756}, -{id: 220, label: 'Fatau Dauda', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Orlando Pirates', value: 22, group: 5, x: 508.3159, y: 1362.8381}, -{id: 221, label: 'Federico Fernández', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Napoli', value: 32, group: 19, x: -945.41595, y: 329.4419}, -{id: 222, label: 'Felipe Caicedo', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Al-Jazira', value: 22, group: 4, x: -1726.1598, y: -587.78546}, -{id: 223, label: 'Felipe Gutiérrez', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Twente', value: 22, group: 18, x: -184.13504, y: 1490.4882}, -{id: 224, label: 'Fernandinho', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Manchester City', value: 31, group: 23, x: -442.97876, y: -336.2658}, -{id: 225, label: 'Fernando Gago', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Boca Juniors', value: 22, group: 19, x: -1147.289, y: 214.82018}, -{id: 226, label: 'Fernando Muslera', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Galatasaray', value: 26, group: 6, x: 73.75355, y: -37.71824}, -{id: 227, label: 'Fernando Torres', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Chelsea', value: 32, group: 23, x: -744.538, y: -446.911}, -{id: 228, label: 'Fidel Martínez', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Tijuana', value: 22, group: 4, x: -1762.2454, y: -617.66486}, -{id: 229, label: 'Francisco Javier Rodríguez', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'América', value: 22, group: 21, x: -2058.6445, y: 342.12747}, -{id: 230, label: 'Francisco Silva', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Osasuna', value: 22, group: 18, x: -207.91714, y: 1451.4407}, -{id: 231, label: 'Frank Lampard', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Chelsea', value: 32, group: 28, x: -247.65233, y: -855.8526}, -{id: 232, label: 'Fraser Forster', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Celtic', value: 25, group: 28, x: 12.960639, y: -928.6838}, -{id: 233, label: 'Fred', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Fluminense', value: 22, group: 23, x: -513.3818, y: -260.2743}, -{id: 234, label: 'Fredy Guarín', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'Internazionale', value: 29, group: 11, x: -787.85443, y: 1018.71765}, -{id: 235, label: 'Frickson Erazo', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Flamengo', value: 22, group: 4, x: -1740.3123, y: -668.11096}, -{id: 236, label: 'Gabriel Achilier', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Emelec', value: 22, group: 4, x: -1682.2622, y: -719.3627}, -{id: 237, label: 'Gabriel Paletta', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Parma', value: 24, group: 3, x: 206.93822, y: 845.00073}, -{id: 238, label: 'Gary Cahill', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Chelsea', value: 32, group: 28, x: -301.79718, y: -918.2849}, -{id: 239, label: 'Gary Medel', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Cardiff City', value: 23, group: 18, x: -135.52126, y: 1534.2073}, -{id: 240, label: 'Gastón Ramírez', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Southampton', value: 28, group: 6, x: -52.539005, y: -56.373035}, -{id: 241, label: 'Gelson Fernandes', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'SC Freiburg', value: 24, group: 0, x: 151.71802, y: 158.9506}, -{id: 242, label: 'Geoff Cameron', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'Stoke City', value: 25, group: 26, x: 820.3439, y: -1464.1147}, -{id: 243, label: 'Georgi Shchennikov', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'CSKA Moscow', value: 23, group: 2, x: -1330.4204, y: -1544.3962}, -{id: 244, label: 'Georginio Wijnaldum', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'PSV', value: 24, group: 22, x: 874.0655, y: 135.79485}, -{id: 245, label: 'Gerard Piqué', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Barcelona', value: 31, group: 23, x: -1126.4338, y: -326.65405}, -{id: 246, label: 'Gervinho', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Roma', value: 26, group: 9, x: 560.3703, y: -680.46234}, -{id: 247, label: 'Ghasem Haddadifar', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Zob Ahan', value: 22, group: 1, x: 1942.6196, y: 1184.3281}, -{id: 248, label: 'Giancarlo González', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Columbus Crew', value: 22, group: 29, x: 2265.3667, y: 299.92572}, -{id: 249, label: 'Gianluigi Buffon (c)', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Juventus', value: 28, group: 3, x: 152.25356, y: 824.18774}, -{id: 250, label: 'Giannis Fetfatzidis', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'Genoa', value: 24, group: 15, x: 1469.2073, y: 587.92706}, -{id: 251, label: 'Giannis Maniatis', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'Olympiacos', value: 23, group: 15, x: 1675.6614, y: 562.7533}, -{id: 252, label: 'Giorgio Chiellini', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Juventus', value: 28, group: 3, x: 168.9661, y: 898.16156}, -{id: 253, label: 'Giorgos Karagounis (c)', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'Fulham', value: 23, group: 15, x: 1659.2035, y: 651.7564}, -{id: 254, label: 'Giorgos Samaras', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'Celtic', value: 25, group: 15, x: 1468.0847, y: 290.17197}, -{id: 255, label: 'Giorgos Tzavellas', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'PAOK', value: 22, group: 15, x: 1582.3857, y: 615.66473}, -{id: 256, label: 'Giovani dos Santos', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'Villarreal', value: 22, group: 21, x: -2058.4065, y: 426.69418}, -{id: 257, label: 'Giovanni Sio', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Basel', value: 26, group: 9, x: 405.23972, y: -662.28076}, -{id: 258, label: 'Glen Johnson', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Liverpool', value: 27, group: 28, x: -77.03864, y: -917.1485}, -{id: 259, label: 'Godfrey Oboabona', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Çaykur Rizespor', value: 23, group: 14, x: 9.590389, y: -1597.5946}, -{id: 260, label: 'Gökhan Inler (c)', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'Napoli', value: 31, group: 0, x: -228.73499, y: 213.29607}, -{id: 261, label: 'Gonzalo Higuaín', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Napoli', value: 32, group: 19, x: -976.805, y: 255.482}, -{id: 262, label: 'Gonzalo Jara', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Nottingham Forest', value: 22, group: 18, x: -235.43576, y: 1571.7034}, -{id: 263, label: 'Gordon Schildenfeld', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Panathinaikos', value: 23, group: 25, x: -217.73817, y: 655.73315}, -{id: 264, label: 'Gotoku Sakai', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'VfB Stuttgart', value: 25, group: 27, x: 626.25525, y: 448.10638}, -{id: 265, label: 'Graham Zusi', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'Sporting Kansas City', value: 22, group: 26, x: 821.1794, y: -1568.8907}, -{id: 266, label: 'Granit Xhaka', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'Borussia Mönchengladbach', value: 23, group: 0, x: 60.45976, y: 205.48042}, -{id: 267, label: 'Guillermo Ochoa', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'Ajaccio', value: 23, group: 21, x: -2012.4979, y: 495.58713}, -{id: 268, label: 'Ha Dae-sung', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Beijing Guoan', value: 22, group: 10, x: 1235.4569, y: 1551.8241}, -{id: 269, label: 'Han Kook-young', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Kashiwa Reysol', value: 22, group: 10, x: 1158.8308, y: 1599.3705}, -{id: 270, label: 'Haris Medunjanin', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'Gaziantepspor', value: 22, group: 20, x: 1200.2539, y: -418.55362}, -{id: 271, label: 'Haris Seferovic', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'Real Sociedad', value: 25, group: 0, x: 141.21535, y: 262.27655}, -{id: 272, label: 'Harrison Afful', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Espérance', value: 22, group: 5, x: 468.08853, y: 1387.6926}, -{id: 273, label: 'Hashem Beikzadeh', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Esteghlal', value: 22, group: 1, x: 1986.3362, y: 1189.6459}, -{id: 274, label: 'Hassan Yebda', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Udinese', value: 23, group: 24, x: -1303.4868, y: 1254.4517}, -{id: 275, label: 'Héctor Herrera', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'Porto', value: 29, group: 21, x: -1799.6183, y: 372.85077}, -{id: 276, label: 'Héctor Moreno', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'Espanyol', value: 23, group: 21, x: -1943.8708, y: 364.62497}, -{id: 277, label: 'Hélder Postiga', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Lazio', value: 28, group: 8, x: -469.8896, y: 192.226}, -{id: 278, label: 'Henri Bedimo', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Lyon', value: 22, group: 17, x: 380.54697, y: 174.65756}, -{id: 279, label: 'Henrique', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Napoli', value: 33, group: 23, x: -572.6227, y: -84.16057}, -{id: 280, label: 'Hernanes', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Internazionale', value: 29, group: 23, x: -528.0018, y: -15.909561}, -{id: 281, label: 'Hiroki Sakai', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'Hannover 96', value: 24, group: 27, x: 714.5649, y: 462.32593}, -{id: 282, label: 'Hiroshi Kiyotake', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + '1. FC Nürnberg', value: 24, group: 27, x: 729.62537, y: 516.7272}, -{id: 283, label: 'Hong Jeong-ho', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'FC Augsburg', value: 23, group: 10, x: 1189.0176, y: 1491.9882}, -{id: 284, label: 'Hossein Mahini', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Persepolis', value: 22, group: 1, x: 1969.5181, y: 1144.5435}, -{id: 285, label: 'Hotaru Yamaguchi', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'Cerezo Osaka', value: 23, group: 27, x: 665.15576, y: 571.1557}, -{id: 286, label: 'Hugo Almeida', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Be?ikta?', value: 24, group: 8, x: -570.7293, y: 230.924}, -{id: 287, label: 'Hugo Campagnaro', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Internazionale', value: 27, group: 19, x: -1030.6344, y: 363.07056}, -{id: 288, label: 'Hugo Lloris (c)', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Tottenham Hotspur', value: 27, group: 16, x: -181.9427, y: -259.68008}, -{id: 289, label: 'Hulk', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Zenit Saint Petersburg', value: 29, group: 23, x: -676.12946, y: -547.05255}, -{id: 290, label: 'Hwang Seok-ho', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Sanfrecce Hiroshima', value: 23, group: 10, x: 1138.2103, y: 1544.5535}, -{id: 291, label: 'Ignazio Abate', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Milan', value: 27, group: 3, x: 229.40173, y: 946.202}, -{id: 292, label: 'Igor Akinfeev', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'CSKA Moscow', value: 23, group: 2, x: -1278.871, y: -1521.6796}, -{id: 293, label: 'Igor Denisov', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'Dynamo Moscow', value: 23, group: 2, x: -1478.4519, y: -1427.1252}, -{id: 294, label: 'Iker Casillas (c)', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Real Madrid', value: 31, group: 23, x: -800.62396, y: -169.28741}, -{id: 295, label: 'Isaác Brizuela', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'Toluca', value: 22, group: 21, x: -2104.4573, y: 342.27985}, -{id: 296, label: 'Islam Slimani', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Sporting CP', value: 25, group: 24, x: -1357.2412, y: 1056.6638}, -{id: 297, label: 'Ismaël Diomandé', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Saint-Étienne', value: 23, group: 9, x: 445.33255, y: -874.95105}, -{id: 298, label: 'Ivan Franjic', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Brisbane Roar', value: 22, group: 12, x: 2090.495, y: -571.4816}, -{id: 299, label: 'Ivan Perišic', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'VfL Wolfsburg', value: 27, group: 25, x: -294.81628, y: 494.7712}, -{id: 300, label: 'Ivan Rakitic', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Sevilla', value: 25, group: 25, x: -359.27826, y: 645.7861}, -{id: 301, label: 'Ivica Olic', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'VfL Wolfsburg', value: 27, group: 25, x: -356.225, y: 503.76892}, -{id: 302, label: 'Izet Hajrovic', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'Galatasaray', value: 26, group: 20, x: 1073.4325, y: -468.65955}, -{id: 303, label: 'Jack Wilshere', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Arsenal', value: 30, group: 28, x: -130.01361, y: -811.2897}, -{id: 304, label: 'Jackson Martínez', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'Porto', value: 29, group: 11, x: -870.14624, y: 947.02435}, -{id: 305, label: 'Jaime Ayoví', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Tijuana', value: 22, group: 4, x: -1695.5747, y: -675.85455}, -{id: 306, label: 'Jalal Hosseini', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Persepolis', value: 22, group: 1, x: 2076.0352, y: 1075.6108}, -{id: 307, label: 'James Holland', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Austria Wien', value: 22, group: 12, x: 2105.7495, y: -645.33295}, -{id: 308, label: 'James Milner', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Manchester City', value: 30, group: 28, x: -219.63795, y: -778.5797}, -{id: 309, label: 'James Rodríguez', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'AS Monaco', value: 25, group: 11, x: -798.6743, y: 1094.4689}, -{id: 310, label: 'James Troisi', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Melbourne Victory', value: 22, group: 12, x: 2041.5525, y: -703.14703}, -{id: 311, label: 'Jan Vertonghen', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Tottenham Hotspur', value: 25, group: 28, x: -726.46454, y: -735.5794}, -{id: 312, label: 'Jasmin Fejzic', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'VfR Aalen', value: 22, group: 20, x: 1170.3435, y: -544.8657}, -{id: 313, label: 'Jason Davidson', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Heracles Almelo', value: 22, group: 12, x: 2027.0093, y: -621.23444}, -{id: 314, label: 'Jasper Cillessen', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Ajax', value: 22, group: 22, x: 884.7674, y: 31.967285}, -{id: 315, label: 'Javad Nekounam (c)', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Al-Kuwait', value: 22, group: 1, x: 1956.9619, y: 1077.9049}, -{id: 316, label: 'Javi Martínez', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Bayern Munich', value: 35, group: 23, x: -549.74335, y: -388.08502}, -{id: 317, label: 'Javier Aquino', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'Villarreal', value: 22, group: 21, x: -2081.5557, y: 384.58026}, -{id: 318, label: 'Javier Hernández', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'Manchester United', value: 35, group: 21, x: -1606.5636, y: 123.67082}, -{id: 319, label: 'Javier Mascherano', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Barcelona', value: 36, group: 19, x: -1221.5325, y: 91.23916}, -{id: 320, label: 'Jean Beausejour', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Wigan Athletic', value: 24, group: 18, x: -67.39274, y: 1286.5491}, -{id: 321, label: 'Jean Makoun', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Rennes', value: 23, group: 17, x: 430.8337, y: 257.74985}, -{id: 322, label: 'Jean-Daniel Akpa-Akpro', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Toulouse', value: 23, group: 9, x: 413.52197, y: -756.9924}, -{id: 323, label: 'Jefferson', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Botafogo', value: 22, group: 23, x: -426.49158, y: -267.58475}, -{id: 324, label: 'Jefferson Montero', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Morelia', value: 23, group: 4, x: -1599.2291, y: -622.97186}, -{id: 325, label: 'Jeremain Lens', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Dynamo Kyiv', value: 25, group: 22, x: 718.188, y: 97.2607}, -{id: 326, label: 'Jermaine Jones', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'Be?ikta?', value: 24, group: 26, x: 734.773, y: -1356.2697}, -{id: 327, label: 'Jérôme Boateng', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Bayern Munich', value: 29, group: 13, x: 313.90338, y: -414.42447}, -{id: 328, label: 'Jerry Bengtson', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'New England Revolution', value: 22, group: 7, x: 1590.5161, y: -1207.1145}, -{id: 329, label: 'Jerry Palacios', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Alajuelense', value: 24, group: 7, x: 1713.397, y: -1049.3608}, -{id: 330, label: 'Ji Dong-won', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'FC Augsburg', value: 23, group: 10, x: 1240.8452, y: 1492.1494}, -{id: 331, label: 'Jô', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Atlético Mineiro', value: 22, group: 23, x: -470.48615, y: -271.38748}, -{id: 332, label: 'João Moutinho', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'AS Monaco', value: 25, group: 8, x: -709.12415, y: 410.8603}, -{id: 333, label: 'João Pereira', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Valencia', value: 25, group: 8, x: -649.96454, y: 448.82736}, -{id: 334, label: 'Joao Rojas', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Cruz Azul', value: 24, group: 4, x: -1776.6962, y: -531.8545}, -{id: 335, label: 'Joe Hart', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Manchester City', value: 30, group: 28, x: -212.69391, y: -704.6478}, -{id: 336, label: 'Joel Campbell', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Olympiacos', value: 26, group: 29, x: 2111.164, y: 365.17755}, -{id: 337, label: 'Joël Matip', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Schalke 04', value: 28, group: 17, x: 540.77966, y: 139.58159}, -{id: 338, label: 'Joël Veltman', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Ajax', value: 22, group: 22, x: 921.6833, y: 59.578938}, -{id: 339, label: 'Johan Djourou', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'Hamburger SV', value: 23, group: 0, x: 57.06974, y: 323.02927}, -{id: 340, label: 'John Boye', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Rennes', value: 23, group: 5, x: 493.59833, y: 1298.41}, -{id: 341, label: 'John Brooks', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'Hertha BSC', value: 23, group: 26, x: 729.48096, y: -1409.5938}, -{id: 342, label: 'John Obi Mikel', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Chelsea', value: 33, group: 14, x: -197.90224, y: -1324.3247}, -{id: 343, label: 'Johnny Acosta', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Alajuelense', value: 23, group: 29, x: 2202.928, y: 222.98761}, -{id: 344, label: 'Johnny Herrera', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Universidad de Chile', value: 22, group: 18, x: -225.40228, y: 1509.603}, -{id: 345, label: 'Jonathan de Guzmán', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Swansea City', value: 23, group: 22, x: 917.89813, y: -45.654217}, -{id: 346, label: 'Jonathan Mensah', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Évian', value: 22, group: 5, x: 461.7189, y: 1342.4531}, -{id: 347, label: 'Jordan Ayew', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Sochaux', value: 22, group: 5, x: 418.20883, y: 1351.9128}, -{id: 348, label: 'Jordan Henderson', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Liverpool', value: 27, group: 28, x: -137.00108, y: -918.78546}, -{id: 349, label: 'Jordi Alba', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Barcelona', value: 31, group: 23, x: -1139.679, y: -237.86505}, -{id: 350, label: 'Jordy Clasie', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Feyenoord', value: 22, group: 22, x: 920.4804, y: 7.368482}, -{id: 351, label: 'Jorge Claros', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Motagua', value: 22, group: 7, x: 1693.2894, y: -1172.8019}, -{id: 352, label: 'Jorge Fucile', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Porto', value: 30, group: 6, x: -342.37836, y: 145.54729}, -{id: 353, label: 'Jorge Guagua', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Emelec', value: 22, group: 4, x: -1678.4408, y: -602.871}, -{id: 354, label: 'Jorge Valdivia', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Palmeiras', value: 22, group: 18, x: -250.0152, y: 1428.506}, -{id: 355, label: 'José de Jesús Corona', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'Cruz Azul', value: 23, group: 21, x: -2099.094, y: 287.12247}, -{id: 356, label: 'José Holebas', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'Olympiacos', value: 23, group: 15, x: 1657.0046, y: 513.2496}, -{id: 357, label: 'José Juan Vázquez', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'León', value: 22, group: 21, x: -2102.5596, y: 434.67215}, -{id: 358, label: 'José María Basanta', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Monterrey', value: 22, group: 19, x: -1144.7311, y: 286.0747}, -{id: 359, label: 'José María Giménez', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Atlético Madrid', value: 28, group: 6, x: -198.00406, y: -82.70489}, -{id: 360, label: 'José Miguel Cubero', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Herediano', value: 22, group: 29, x: 2268.5837, y: 346.56885}, -{id: 361, label: 'José Pedro Fuenzalida', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Colo-Colo', value: 22, group: 18, x: -198.39777, y: 1545.6372}, -{id: 362, label: 'José Rojas', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Universidad de Chile', value: 22, group: 18, x: -307.82147, y: 1544.147}, -{id: 363, label: 'Joseph Yobo (c)', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Norwich City', value: 23, group: 14, x: 3.3988526, y: -1540.3546}, -{id: 364, label: 'Josip Drmic', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + '1. FC Nürnberg', value: 25, group: 0, x: 179.9546, y: 206.55292}, -{id: 365, label: 'Jozy Altidore', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'Sunderland', value: 23, group: 26, x: 866.4315, y: -1353.6399}, -{id: 366, label: 'Juan Camilo Zúñiga', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'Napoli', value: 33, group: 11, x: -759.6773, y: 893.11926}, -{id: 367, label: 'Juan Carlos García', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Wigan Athletic', value: 23, group: 7, x: 1576.5138, y: -1044.397}, -{id: 368, label: 'Juan Carlos Paredes', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Barcelona', value: 35, group: 4, x: -1452.1322, y: -446.39807}, -{id: 369, label: 'Juan Fernando Quintero', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'Porto', value: 29, group: 11, x: -908.9095, y: 1006.1945}, -{id: 370, label: 'Juan Guillermo Cuadrado', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'Fiorentina', value: 24, group: 11, x: -683.1348, y: 1184.008}, -{id: 371, label: 'Juan Mata', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Manchester United', value: 34, group: 23, x: -837.1373, y: -428.5978}, -{id: 372, label: 'Juan Pablo Montes', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Motagua', value: 22, group: 7, x: 1592.682, y: -1250.384}, -{id: 373, label: 'Juanfran', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Atlético Madrid', value: 27, group: 23, x: -888.2895, y: -365.17215}, -{id: 374, label: 'Julian Draxler', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Schalke 04', value: 27, group: 13, x: 528.5164, y: -263.55563}, -{id: 375, label: 'Julian Green', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'Bayern Munich', value: 35, group: 26, x: 627.9602, y: -1176.4528}, -{id: 376, label: 'Júlio César', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Toronto FC', value: 23, group: 23, x: -374.46234, y: -336.27332}, -{id: 377, label: 'Jung Sung-ryong', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Suwon Bluewings', value: 22, group: 10, x: 1253.4236, y: 1593.7097}, -{id: 378, label: 'Júnior Díaz', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Mainz 05', value: 26, group: 29, x: 2052.3333, y: 457.91708}, -{id: 379, label: 'Juwon Oshaniwa', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Ashdod', value: 22, group: 14, x: -3.9951146, y: -1656.1483}, -{id: 380, label: 'Karim Ansarifard', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Tractor Sazi', value: 22, group: 1, x: 2030.3977, y: 1187.764}, -{id: 381, label: 'Karim Benzema', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Real Madrid', value: 32, group: 16, x: -255.21576, y: -165.30316}, -{id: 382, label: 'Keisuke Honda', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'Milan', value: 29, group: 27, x: 610.39655, y: 750.20026}, -{id: 383, label: 'Kenneth Omeruo', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Middlesbrough', value: 23, group: 14, x: -33.32675, y: -1484.3856}, -{id: 384, label: 'Kevin De Bruyne', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'VfL Wolfsburg', value: 28, group: 28, x: -581.4455, y: -583.9621}, -{id: 385, label: 'Kevin Großkreutz', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Borussia Dortmund', value: 24, group: 13, x: 553.73175, y: -380.0992}, -{id: 386, label: 'Kevin Mirallas', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Everton', value: 26, group: 28, x: -563.9285, y: -964.3166}, -{id: 387, label: 'Kevin-Prince Boateng', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Schalke 04', value: 28, group: 5, x: 528.2719, y: 1086.7677}, -{id: 388, label: 'Keylor Navas', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Levante', value: 23, group: 29, x: 2179.6377, y: 330.61267}, -{id: 389, label: 'Khosro Heydari', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Esteghlal', value: 22, group: 1, x: 2085.2766, y: 1118.5546}, -{id: 390, label: 'Ki Sung-yueng', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Sunderland', value: 23, group: 10, x: 1168.5514, y: 1424.8241}, -{id: 391, label: 'Kim Bo-kyung', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Cardiff City', value: 23, group: 10, x: 1094.6575, y: 1613.0087}, -{id: 392, label: 'Kim Chang-soo', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Kashiwa Reysol', value: 22, group: 10, x: 1182.648, y: 1681.8923}, -{id: 393, label: 'Kim Seung-gyu', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Ulsan Hyundai', value: 22, group: 10, x: 1189.8958, y: 1559.8545}, -{id: 394, label: 'Kim Shin-wook', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Ulsan Hyundai', value: 22, group: 10, x: 1231.2048, y: 1679.3086}, -{id: 395, label: 'Kim Young-gwon', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Guangzhou Evergrande', value: 22, group: 10, x: 1284.3221, y: 1556.8948}, -{id: 396, label: 'Klaas-Jan Huntelaar', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Schalke 04', value: 28, group: 22, x: 809.16656, y: 91.84488}, -{id: 397, label: 'Koke', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Atlético Madrid', value: 27, group: 23, x: -921.22095, y: -304.28424}, -{id: 398, label: 'Kolo Touré', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Liverpool', value: 31, group: 9, x: 297.04135, y: -918.4601}, -{id: 399, label: 'Koo Ja-cheol (c)', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Mainz 05', value: 25, group: 10, x: 1210.03, y: 1383.6355}, -{id: 400, label: 'Kostas Katsouranis', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'PAOK', value: 22, group: 15, x: 1625.112, y: 590.2659}, -{id: 401, label: 'Kostas Manolas', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'Olympiacos', value: 23, group: 15, x: 1643.8208, y: 458.0363}, -{id: 402, label: 'Kostas Mitroglou', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'Fulham', value: 23, group: 15, x: 1704.107, y: 623.1121}, -{id: 403, label: 'Kunle Odunlami', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Sunshine Stars', value: 22, group: 14, x: -51.509785, y: -1656.867}, -{id: 404, label: 'Kwadwo Asamoah', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Juventus', value: 33, group: 5, x: 285.16757, y: 1193.1697}, -{id: 405, label: 'Kwak Tae-hwi', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Al-Hilal', value: 22, group: 10, x: 1276.5813, y: 1652.845}, -{id: 406, label: 'Kyle Beckerman', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'Real Salt Lake', value: 22, group: 26, x: 814.4154, y: -1616.4198}, -{id: 407, label: 'Landry N Guémo', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Bordeaux', value: 22, group: 17, x: 380.33423, y: 127.532715}, -{id: 408, label: 'Laurent Ciman', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Standard Liège', value: 24, group: 28, x: -542.0193, y: -660.84076}, -{id: 409, label: 'Laurent Koscielny', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Arsenal', value: 29, group: 16, x: -15.0555935, y: -387.5162}, -{id: 410, label: 'Lazaros Christodoulopoulos', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'Bologna', value: 23, group: 15, x: 1501.5779, y: 504.68384}, -{id: 411, label: 'Lee Bum-young', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Busan IPark', value: 22, group: 10, x: 1190.9927, y: 1637.5756}, -{id: 412, label: 'Lee Chung-yong', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Bolton Wanderers', value: 22, group: 10, x: 1146.0409, y: 1647.9602}, -{id: 413, label: 'Lee Keun-ho', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Sangju Sangmu', value: 22, group: 10, x: 1296.3544, y: 1607.5996}, -{id: 414, label: 'Lee Yong', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Ulsan Hyundai', value: 22, group: 10, x: 1208.6063, y: 1598.109}, -{id: 415, label: 'Leighton Baines', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Everton', value: 25, group: 28, x: -237.56212, y: -998.078}, -{id: 416, label: 'Leonardo Bonucci', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Juventus', value: 28, group: 3, x: 125.05671, y: 766.19403}, -{id: 417, label: 'Leroy Fer', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Norwich City', value: 23, group: 22, x: 837.3325, y: -102.88975}, -{id: 418, label: 'Liassine Cadamuro-Bentaïba', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Mallorca', value: 22, group: 24, x: -1424.9585, y: 1185.58}, -{id: 419, label: 'Lionel Messi (c)', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Barcelona', value: 36, group: 19, x: -1133.2008, y: 55.981808}, -{id: 420, label: 'Loïc Feudjou', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Coton Sport', value: 22, group: 17, x: 464.74194, y: 157.333}, -{id: 421, label: 'Loïc Rémy', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Newcastle United', value: 25, group: 16, x: 73.68377, y: -313.17633}, -{id: 422, label: 'Lorenzo Insigne', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Napoli', value: 33, group: 3, x: -68.64961, y: 680.98474}, -{id: 423, label: 'Loukas Vyntra', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'Levante', value: 23, group: 15, x: 1712.4525, y: 526.83075}, -{id: 424, label: 'Lucas Biglia', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Lazio', value: 28, group: 19, x: -845.6186, y: 161.40001}, -{id: 425, label: 'Lucas Digne', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Paris Saint-Germain', value: 29, group: 16, x: -18.416775, y: -111.03686}, -{id: 426, label: 'Luis Garrido', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Olimpia', value: 22, group: 7, x: 1665.7246, y: -1263.9408}, -{id: 427, label: 'Luis López', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Real España', value: 22, group: 7, x: 1610.1837, y: -1129.5691}, -{id: 428, label: 'Luís Neto', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Zenit Saint Petersburg', value: 29, group: 8, x: -787.05585, y: -14.597502}, -{id: 429, label: 'Luis Saritama', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Barcelona', value: 35, group: 4, x: -1546.8987, y: -441.0774}, -{id: 430, label: 'Luis Suárez', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Liverpool', value: 31, group: 6, x: -100.21393, y: -246.37468}, -{id: 431, label: 'Luiz Gustavo', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'VfL Wolfsburg', value: 28, group: 23, x: -456.7165, y: -142.2136}, -{id: 432, label: 'Luka Modric', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Real Madrid', value: 33, group: 25, x: -410.41797, y: 416.6111}, -{id: 433, label: 'Lukas Podolski', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Arsenal', value: 29, group: 13, x: 202.08969, y: -446.2755}, -{id: 434, label: 'Luke Shaw', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Southampton', value: 26, group: 28, x: -92.229225, y: -688.88574}, -{id: 435, label: 'Madjid Bougherra (c)', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Lekhwiya', value: 22, group: 24, x: -1470.3363, y: 1180.3844}, -{id: 436, label: 'Maicon', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Roma', value: 26, group: 23, x: -278.08972, y: -249.45703}, -{id: 437, label: 'Majeed Waris', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Valenciennes', value: 23, group: 5, x: 324.35605, y: 1417.5355}, -{id: 438, label: 'Makoto Hasebe (c)', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + '1. FC Nürnberg', value: 24, group: 27, x: 672.80505, y: 505.12762}, -{id: 439, label: 'Maksim Kanunnikov', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'Rubin Kazan', value: 23, group: 2, x: -1315.3818, y: -1323.4706}, -{id: 440, label: 'Mamadou Sakho', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Liverpool', value: 31, group: 16, x: -55.2884, y: -503.5874}, -{id: 441, label: 'Manabu Saito', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'Yokohama F. Marinos', value: 22, group: 27, x: 700.693, y: 617.4117}, -{id: 442, label: 'Manuel Neuer', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Bayern Munich', value: 29, group: 13, x: 362.29532, y: -299.95224}, -{id: 443, label: 'Marcelo', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Real Madrid', value: 33, group: 23, x: -546.0523, y: -181.72266}, -{id: 444, label: 'Marcelo Brozovic', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Dinamo Zagreb', value: 23, group: 25, x: -406.19418, y: 695.72943}, -{id: 445, label: 'Marcelo Díaz', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Basel', value: 27, group: 18, x: -193.87224, y: 1188.147}, -{id: 446, label: 'Marco Fabián', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'Cruz Azul', value: 23, group: 21, x: -2042.7997, y: 288.54993}, -{id: 447, label: 'Marco Parolo', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Parma', value: 24, group: 3, x: 223.34402, y: 798.16846}, -{id: 448, label: 'Marco Ureña', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Kuban Krasnodar', value: 23, group: 29, x: 2171.2605, y: 406.7075}, -{id: 449, label: 'Marco Verratti', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Paris Saint-Germain', value: 29, group: 3, x: 74.62252, y: 597.4002}, -{id: 450, label: 'Marcos Rojo', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Sporting CP', value: 25, group: 19, x: -1169.2754, y: 359.3405}, -{id: 451, label: 'Mariano Andújar', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Catania', value: 22, group: 19, x: -1186.1453, y: 246.04404}, -{id: 452, label: 'Mario Balotelli', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Milan', value: 27, group: 3, x: 253.78076, y: 886.26984}, -{id: 453, label: 'Mario Gavranovic', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'Zürich', value: 22, group: 0, x: 52.757668, y: 247.96585}, -{id: 454, label: 'Mario Götze', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Bayern Munich', value: 29, group: 13, x: 301.41776, y: -338.43552}, -{id: 455, label: 'Mario Mandžukic', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Bayern Munich', value: 35, group: 25, x: -149.6339, y: 325.6033}, -{id: 456, label: 'Mario Martínez', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Real España', value: 22, group: 7, x: 1689.1534, y: -1223.153}, -{id: 457, label: 'Mario Yepes (c)', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'Atalanta', value: 23, group: 11, x: -719.321, y: 1256.8893}, -{id: 458, label: 'Mark Bresciano', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Al-Gharafa', value: 22, group: 12, x: 2122.0056, y: -604.5107}, -{id: 459, label: 'Mark Milligan', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Melbourne Victory', value: 22, group: 12, x: 2173.8164, y: -588.3221}, -{id: 460, label: 'Marouane Fellaini', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Manchester United', value: 34, group: 28, x: -655.5912, y: -756.77374}, -{id: 461, label: 'Martín Cáceres', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Juventus', value: 33, group: 6, x: -21.211044, y: 343.79504}, -{id: 462, label: 'Martín Demichelis', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Manchester City', value: 29, group: 19, x: -893.08545, y: 82.947815}, -{id: 463, label: 'Martín Silva', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Vasco da Gama', value: 22, group: 6, x: -0.6348668, y: 1.9825428}, -{id: 464, label: 'Marvin Chávez', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Chivas USA', value: 23, group: 7, x: 1429.7988, y: -1179.9895}, -{id: 465, label: 'Masahiko Inoha', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'Jubilo Iwata', value: 22, group: 27, x: 730.9411, y: 583.1111}, -{id: 466, label: 'Masato Morishige', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'F.C. Tokyo', value: 22, group: 27, x: 677.74445, y: 664.5135}, -{id: 467, label: 'Masoud Shojaei', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Las Palmas', value: 22, group: 1, x: 2059.2344, y: 1154.0554}, -{id: 468, label: 'Massimo Luongo', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Swindon Town', value: 22, group: 12, x: 2135.3752, y: -676.93585}, -{id: 469, label: 'Mateo Kovacic', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Internazionale', value: 29, group: 25, x: -492.02667, y: 654.4242}, -{id: 470, label: 'Mathew Leckie', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'FSV Frankfurt', value: 22, group: 12, x: 2138.549, y: -562.8361}, -{id: 471, label: 'Mathew Ryan', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Club Brugge', value: 23, group: 12, x: 2056.7805, y: -519.5844}, -{id: 472, label: 'Mathieu Debuchy', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Newcastle United', value: 25, group: 16, x: 14.882936, y: -313.20358}, -{id: 473, label: 'Mathieu Valbuena', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Marseille', value: 24, group: 16, x: 44.39426, y: -119.345985}, -{id: 474, label: 'Mathis Bolly', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Fortuna Düsseldorf', value: 23, group: 9, x: 651.62463, y: -893.97076}, -{id: 475, label: 'Mats Hummels', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Borussia Dortmund', value: 24, group: 13, x: 607.8975, y: -421.7086}, -{id: 476, label: 'Matt Besler', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'Sporting Kansas City', value: 22, group: 26, x: 861.9521, y: -1604.1628}, -{id: 477, label: 'Matt McKay', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Brisbane Roar', value: 22, group: 12, x: 2090.5696, y: -687.9733}, -{id: 478, label: 'Matteo Darmian', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Torino', value: 23, group: 3, x: 332.64136, y: 846.05145}, -{id: 479, label: 'Matthew Špiranovic', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Western Sydney Wanderers', value: 22, group: 12, x: 2061.1667, y: -656.2603}, -{id: 480, label: 'Matthias Ginter', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'SC Freiburg', value: 25, group: 13, x: 444.28552, y: -312.17847}, -{id: 481, label: 'Mattia De Sciglio', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Milan', value: 27, group: 3, x: 304.86957, y: 920.4894}, -{id: 482, label: 'Mattia Perin', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Genoa', value: 24, group: 3, x: 272.21268, y: 763.70386}, -{id: 483, label: 'Mauricio Isla', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Juventus', value: 32, group: 18, x: -142.68803, y: 1330.8896}, -{id: 484, label: 'Mauricio Pinilla', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Cagliari', value: 23, group: 18, x: -356.0086, y: 1526.6892}, -{id: 485, label: 'Max Gradel', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Saint-Étienne', value: 23, group: 9, x: 486.36218, y: -849.3238}, -{id: 486, label: 'Maxi Pereira', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Benfica', value: 26, group: 6, x: -192.70482, y: 101.33695}, -{id: 487, label: 'Maxi Rodríguez', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Newells Old Boys', value: 22, group: 19, x: -1193.7656, y: 294.7356}, -{id: 488, label: 'Maxim Choupo-Moting', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Mainz 05', value: 26, group: 17, x: 590.2084, y: 305.84305}, -{id: 489, label: 'Máximo Banguera', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Barcelona', value: 35, group: 4, x: -1488.9634, y: -533.33093}, -{id: 490, label: 'Maxwell', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Paris Saint-Germain', value: 30, group: 23, x: -388.1638, y: -99.59259}, -{id: 491, label: 'Maya Yoshida', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'Southampton', value: 28, group: 27, x: 540.3439, y: 427.26245}, -{id: 492, label: 'Maynor Figueroa', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Hull City', value: 23, group: 7, x: 1528.1024, y: -1100.3427}, -{id: 493, label: 'Medhi Lacen', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Getafe', value: 23, group: 24, x: -1321.0677, y: 1173.3302}, -{id: 494, label: 'Mehdi Mostefa', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Ajaccio', value: 23, group: 24, x: -1480.4698, y: 1115.9075}, -{id: 495, label: 'Mehrdad Pouladi', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Persepolis', value: 22, group: 1, x: 1894.8638, y: 1109.2692}, -{id: 496, label: 'Memphis Depay', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'PSV', value: 24, group: 22, x: 929.35187, y: 119.25908}, -{id: 497, label: 'Mensur Mujdža', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'SC Freiburg', value: 25, group: 20, x: 1039.0459, y: -418.09897}, -{id: 498, label: 'Mesut Özil', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Arsenal', value: 29, group: 13, x: 266.21005, y: -466.70053}, -{id: 499, label: 'Michael Arroyo', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Atlante', value: 22, group: 4, x: -1730.8958, y: -727.36395}, -{id: 500, label: 'Michael Babatunde', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Volyn Lutsk', value: 22, group: 14, x: -143.01881, y: -1634.2734}, -{id: 501, label: 'Michael Barrantes', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Aalesund', value: 22, group: 29, x: 2300.9563, y: 256.13895}, -{id: 502, label: 'Michael Bradley', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'Toronto FC', value: 23, group: 26, x: 721.9479, y: -1477.4308}, -{id: 503, label: 'Michael Essien', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Milan', value: 28, group: 5, x: 376.83282, y: 1298.3724}, -{id: 504, label: 'Michael Lang', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'Grasshopper', value: 22, group: 0, x: 29.479486, y: 282.8444}, -{id: 505, label: 'Michael Uchebo', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Cercle Brugge', value: 22, group: 14, x: -95.68781, y: -1656.3585}, -{id: 506, label: 'Michael Umaña', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Saprissa', value: 22, group: 29, x: 2330.0725, y: 379.5474}, -{id: 507, label: 'Michel Vorm', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Swansea City', value: 23, group: 22, x: 868.7987, y: -56.305706}, -{id: 508, label: 'Mickaël Landreau', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Bastia', value: 22, group: 16, x: -46.63565, y: -207.56238}, -{id: 509, label: 'Miguel Ángel Ponce', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'Toluca', value: 22, group: 21, x: -2068.7258, y: 475.15393}, -{id: 510, label: 'Miguel Layún', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'América', value: 22, group: 21, x: -2150.149, y: 351.6338}, -{id: 511, label: 'Miguel Veloso', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Dynamo Kyiv', value: 25, group: 8, x: -552.1939, y: 364.91592}, -{id: 512, label: 'Miiko Albornoz', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Malmö FF', value: 22, group: 18, x: -282.78622, y: 1583.4946}, -{id: 513, label: 'Mikkel Diskerud', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'Rosenborg', value: 23, group: 26, x: 941.49945, y: -1436.3448}, -{id: 514, label: 'Milan Badelj', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Hamburger SV', value: 23, group: 25, x: -271.98166, y: 685.1374}, -{id: 515, label: 'Mile Jedinak (c)', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Crystal Palace', value: 22, group: 12, x: 2075.4526, y: -732.8337}, -{id: 516, label: 'Miralem Pjanic', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'Roma', value: 26, group: 20, x: 1103.221, y: -385.46555}, -{id: 517, label: 'Miroslav Klose', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Lazio', value: 28, group: 13, x: 293.14236, y: -267.2075}, -{id: 518, label: 'Mitchell Langerak', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Borussia Dortmund', value: 27, group: 12, x: 1759.8835, y: -484.94678}, -{id: 519, label: 'Mohamed Zemmamouche', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'USM Alger', value: 22, group: 24, x: -1405.2527, y: 1223.2103}, -{id: 520, label: 'Mohammed Rabiu', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Kuban Krasnodar', value: 23, group: 5, x: 577.33563, y: 1315.1465}, -{id: 521, label: 'Morgan Schneiderlin', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Southampton', value: 28, group: 16, x: 8.893564, y: -207.08623}, -{id: 522, label: 'Mousa Dembélé', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Tottenham Hotspur', value: 25, group: 28, x: -780.7014, y: -765.0794}, -{id: 523, label: 'Moussa Sissoko', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Newcastle United', value: 25, group: 16, x: 49.931614, y: -364.4847}, -{id: 524, label: 'Muhamed Bešic', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'Ferencváros', value: 22, group: 20, x: 1194.7092, y: -510.00156}, -{id: 525, label: 'Nabil Bentaleb', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Tottenham Hotspur', value: 27, group: 24, x: -1282.9584, y: 861.7018}, -{id: 526, label: 'Nabil Ghilas', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Porto', value: 30, group: 24, x: -1331.139, y: 965.7551}, -{id: 527, label: 'Nacer Chadli', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Tottenham Hotspur', value: 25, group: 28, x: -730.6295, y: -798.0246}, -{id: 528, label: 'Nani', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Manchester United', value: 35, group: 8, x: -646.50024, y: 40.378365}, -{id: 529, label: 'Neymar', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Barcelona', value: 36, group: 23, x: -688.3395, y: -195.97823}, -{id: 530, label: 'Nick Rimando', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'Real Salt Lake', value: 22, group: 26, x: 864.0869, y: -1556.7881}, -{id: 531, label: 'Nicolás Lodeiro', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Corinthians', value: 22, group: 6, x: -54.92223, y: 16.616009}, -{id: 532, label: 'Nicolas Lombaerts', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Zenit Saint Petersburg', value: 28, group: 28, x: -803.9264, y: -951.1398}, -{id: 533, label: 'Nicolas N Koulou', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Marseille', value: 24, group: 17, x: 368.89407, y: 227.7929}, -{id: 534, label: 'Nigel de Jong', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Milan', value: 29, group: 22, x: 764.12317, y: 266.0992}, -{id: 535, label: 'Nikica Jelavic', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Hull City', value: 23, group: 25, x: -197.7674, y: 532.7603}, -{id: 536, label: 'Noel Valladares (c)', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Olimpia', value: 22, group: 7, x: 1633.6897, y: -1230.4397}, -{id: 537, label: 'Ogenyi Onazi', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Lazio', value: 28, group: 14, x: -33.871628, y: -1294.2328}, -{id: 538, label: 'Ognjen Vranješ', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'Elaz??spor', value: 22, group: 20, x: 1242.7872, y: -442.58514}, -{id: 539, label: 'Ognjen Vukojevic', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Dynamo Kyiv', value: 24, group: 25, x: -265.94672, y: 620.2862}, -{id: 540, label: 'Oleg Shatov', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'Zenit Saint Petersburg', value: 26, group: 2, x: -1223.3152, y: -1368.6674}, -{id: 541, label: 'Oliver Bozanic', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Luzern', value: 22, group: 12, x: 2198.3757, y: -627.18024}, -{id: 542, label: 'Oliver Zelenika', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Lokomotiva', value: 22, group: 25, x: -310.13934, y: 653.3941}, -{id: 543, label: 'Olivier Giroud', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Arsenal', value: 29, group: 16, x: -51.68798, y: -320.77396}, -{id: 544, label: 'Omar Gonzalez', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'Los Angeles Galaxy', value: 22, group: 26, x: 770.25964, y: -1596.3325}, -{id: 545, label: 'Orestis Karnezis', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'Granada', value: 24, group: 15, x: 1393.8566, y: 576.5566}, -{id: 546, label: 'Oribe Peralta', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'Santos Laguna', value: 22, group: 21, x: -2123.5435, y: 394.2029}, -{id: 547, label: 'Oscar', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Chelsea', value: 30, group: 23, x: -364.28693, y: -412.46796}, -{id: 548, label: 'Óscar Bagüí', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Emelec', value: 22, group: 4, x: -1773.5126, y: -705.2896}, -{id: 549, label: 'Óscar Boniek García', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Houston Dynamo', value: 23, group: 7, x: 1554.0684, y: -1285.4417}, -{id: 550, label: 'Óscar Duarte', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Club Brugge', value: 23, group: 29, x: 2292.3699, y: 190.47668}, -{id: 551, label: 'Osman Chávez', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Qingdao Jonoon', value: 22, group: 7, x: 1657.8716, y: -1139.4136}, -{id: 552, label: 'Oswaldo Minda', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Chivas USA', value: 23, group: 4, x: -1549.4302, y: -719.534}, -{id: 553, label: 'Ousmane Viera', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Çaykur Rizespor', value: 23, group: 9, x: 474.08282, y: -965.51855}, -{id: 554, label: 'Pablo Armero', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'West Ham United', value: 22, group: 11, x: -854.2187, y: 1249.3016}, -{id: 555, label: 'Pablo Zabaleta', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Manchester City', value: 29, group: 19, x: -933.6388, y: 24.648056}, -{id: 556, label: 'Panagiotis Glykos', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'PAOK', value: 22, group: 15, x: 1575.4261, y: 522.7162}, -{id: 557, label: 'Panagiotis Kone', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'Bologna', value: 23, group: 15, x: 1535.2936, y: 466.857}, -{id: 558, label: 'Panagiotis Tachtsidis', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'Torino', value: 25, group: 15, x: 1428.6139, y: 635.1239}, -{id: 559, label: 'Park Chu-young', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Watford', value: 23, group: 10, x: 1047.7448, y: 1576.756}, -{id: 560, label: 'Park Jong-woo', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Guangzhou R&F', value: 22, group: 10, x: 1236.0852, y: 1634.4038}, -{id: 561, label: 'Park Joo-ho', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Mainz 05', value: 25, group: 10, x: 1252.9922, y: 1424.8129}, -{id: 562, label: 'Patrice Evra', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Manchester United', value: 35, group: 16, x: -226.57672, y: -327.5888}, -{id: 563, label: 'Patrick Pemberton', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Alajuelense', value: 23, group: 29, x: 2230.4392, y: 179.53189}, -{id: 564, label: 'Paul Aguilar', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'América', value: 22, group: 21, x: -2114.9287, y: 482.15585}, -{id: 565, label: 'Paul Pogba', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Juventus', value: 33, group: 16, x: 8.138252, y: 94.4195}, -{id: 566, label: 'Paul Verhaegh', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'FC Augsburg', value: 24, group: 22, x: 949.3831, y: 201.00778}, -{id: 567, label: 'Paulinho', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Tottenham Hotspur', value: 27, group: 23, x: -575.7446, y: -298.09418}, -{id: 568, label: 'Pavel Mogilevets', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'Rubin Kazan', value: 23, group: 2, x: -1357.9305, y: -1289.3833}, -{id: 569, label: 'Pedro', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Barcelona', value: 31, group: 23, x: -1064.4056, y: -381.13626}, -{id: 570, label: 'Pejman Montazeri', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Umm Salal', value: 22, group: 1, x: 2022.9941, y: 1015.42993}, -{id: 571, label: 'Pepe', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Real Madrid', value: 31, group: 8, x: -652.3342, y: 226.08397}, -{id: 572, label: 'Pepe Reina', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Napoli', value: 32, group: 23, x: -850.5622, y: -89.60556}, -{id: 573, label: 'Per Mertesacker', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Arsenal', value: 29, group: 13, x: 261.49197, y: -532.3377}, -{id: 574, label: 'Peter Odemwingie', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Stoke City', value: 25, group: 14, x: 110.87254, y: -1595.627}, -{id: 575, label: 'Phil Jagielka', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Everton', value: 25, group: 28, x: -210.36139, y: -1046.034}, -{id: 576, label: 'Phil Jones', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Manchester United', value: 32, group: 28, x: -300.32303, y: -774.0247}, -{id: 577, label: 'Philipp Lahm (c)', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Bayern Munich', value: 29, group: 13, x: 350.3983, y: -483.03665}, -{id: 578, label: 'Philippe Senderos', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'Valencia', value: 26, group: 0, x: -84.25211, y: 385.70135}, -{id: 579, label: 'Pierre Webó', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Fenerbahçe', value: 26, group: 17, x: 292.58267, y: 67.772385}, -{id: 580, label: 'Rafa Silva', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Braga', value: 22, group: 8, x: -692.3677, y: 355.65155}, -{id: 581, label: 'Rafael Márquez (c)', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'León', value: 22, group: 21, x: -2148.7192, y: 446.013}, -{id: 582, label: 'Rafik Halliche', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Académica', value: 22, group: 24, x: -1426.0991, y: 1266.2908}, -{id: 583, label: 'Raheem Sterling', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Liverpool', value: 27, group: 28, x: -93.51011, y: -985.4643}, -{id: 584, label: 'Rahman Ahmadi', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Sepahan', value: 22, group: 1, x: 2011.6289, y: 1143.9183}, -{id: 585, label: 'Raïs M Bolhi', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'CSKA Sofia', value: 22, group: 24, x: -1459.3608, y: 1229.282}, -{id: 586, label: 'Ramires', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Chelsea', value: 30, group: 23, x: -481.02625, y: -469.71396}, -{id: 587, label: 'Ramon Azeez', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Almería', value: 22, group: 14, x: -83.15391, y: -1703.9006}, -{id: 588, label: 'Randall Brenes', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Cartaginés', value: 22, group: 29, x: 2309.6873, y: 299.45453}, -{id: 589, label: 'Raphaël Varane', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Real Madrid', value: 32, group: 16, x: -176.20541, y: -169.91304}, -{id: 590, label: 'Rashid Sumaila', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Mamelodi Sundowns', value: 22, group: 5, x: 457.3916, y: 1442.739}, -{id: 591, label: 'Raúl Albiol', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Napoli', value: 32, group: 23, x: -934.9327, y: -101.35684}, -{id: 592, label: 'Raúl Jiménez', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'América', value: 22, group: 21, x: -2167.434, y: 400.85532}, -{id: 593, label: 'Raul Meireles', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Fenerbahçe', value: 25, group: 8, x: -515.2749, y: 255.22029}, -{id: 594, label: 'Rémy Cabella', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Montpellier', value: 22, group: 16, x: -28.49823, y: -252.28802}, -{id: 595, label: 'Renato Ibarra', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Vitesse', value: 23, group: 4, x: -1613.8063, y: -545.05145}, -{id: 596, label: 'Reto Ziegler', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'Sassuolo', value: 22, group: 0, x: 3.861307, y: 248.17929}, -{id: 597, label: 'Reuben Gabriel', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Waasland-Beveren', value: 22, group: 14, x: -132.04297, y: -1684.2073}, -{id: 598, label: 'Reza Ghoochannejhad', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Charlton Athletic', value: 22, group: 1, x: 2037.9062, y: 1109.297}, -{id: 599, label: 'Reza Haghighi', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Persepolis', value: 22, group: 1, x: 1912.5083, y: 1151.8527}, -{id: 600, label: 'Ricardo Álvarez', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Internazionale', value: 27, group: 19, x: -991.71326, y: 419.20453}, -{id: 601, label: 'Ricardo Costa', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Valencia', value: 25, group: 8, x: -699.53125, y: 481.92715}, -{id: 602, label: 'Ricardo Rodríguez', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'VfL Wolfsburg', value: 27, group: 0, x: -71.65908, y: 197.11438}, -{id: 603, label: 'Rickie Lambert', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Southampton', value: 26, group: 28, x: -64.72023, y: -747.43665}, -{id: 604, label: 'Rio Mavuba', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Lille', value: 25, group: 16, x: -65.83039, y: -421.9733}, -{id: 605, label: 'Riyad Mahrez', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Leicester City', value: 22, group: 24, x: -1375.4896, y: 1263.6211}, -{id: 606, label: 'Robin van Persie (c)', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Manchester United', value: 35, group: 22, x: 425.40573, y: -117.8186}, -{id: 607, label: 'Rodrigo Muñoz', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Libertad', value: 22, group: 6, x: -20.128693, y: 28.408825}, -{id: 608, label: 'Rodrigo Palacio', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Internazionale', value: 27, group: 19, x: -1056.1539, y: 433.82733}, -{id: 609, label: 'Roger Espinoza', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Wigan Athletic', value: 23, group: 7, x: 1525.8236, y: -1042.1475}, -{id: 610, label: 'Roman Bürki', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'Grasshopper', value: 22, group: 0, x: 84.8047, y: 279.10205}, -{id: 611, label: 'Roman Weidenfeller', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Borussia Dortmund', value: 24, group: 13, x: 605.1841, y: -360.4882}, -{id: 612, label: 'Romelu Lukaku', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Everton', value: 26, group: 28, x: -624.76385, y: -965.3788}, -{id: 613, label: 'Ron Vlaar', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Aston Villa', value: 23, group: 22, x: 922.5167, y: -99.8845}, -{id: 614, label: 'Ron-Robert Zieler', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Hannover 96', value: 24, group: 13, x: 479.21454, y: -376.45038}, -{id: 615, label: 'Rony Martínez', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Real Sociedad', value: 25, group: 7, x: 1436.8522, y: -978.24146}, -{id: 616, label: 'Ross Barkley', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Everton', value: 25, group: 28, x: -149.7628, y: -1043.2092}, -{id: 617, label: 'Roy Miller', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'New York Red Bulls', value: 23, group: 29, x: 2341.1836, y: 210.36285}, -{id: 618, label: 'Rúben Amorim', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Benfica', value: 25, group: 8, x: -743.5818, y: 322.5777}, -{id: 619, label: 'Rui Patrício', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Sporting CP', value: 24, group: 8, x: -770.5219, y: 432.82077}, -{id: 620, label: 'Ryan McGowan', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Shandong Luneng Taishan', value: 22, group: 12, x: 2185.5203, y: -671.7802}, -{id: 621, label: 'Salomon Kalou', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Lille', value: 25, group: 9, x: 392.33093, y: -927.2915}, -{id: 622, label: 'Salvatore Sirigu', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Paris Saint-Germain', value: 29, group: 3, x: 133.34747, y: 646.7461}, -{id: 623, label: 'Sami Khedira', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Real Madrid', value: 33, group: 13, x: 147.37221, y: -251.96838}, -{id: 624, label: 'Sammir', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Getafe', value: 23, group: 25, x: -386.4237, y: 741.6884}, -{id: 625, label: 'Sammy Bossut', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Zulte Waregem', value: 23, group: 28, x: -665.6252, y: -835.4098}, -{id: 626, label: 'Sammy N Djock', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Fethiyespor', value: 22, group: 17, x: 341.5248, y: 155.85918}, -{id: 627, label: 'Samuel Etoo (c)', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Chelsea', value: 33, group: 17, x: 207.89883, y: -77.141884}, -{id: 628, label: 'Samuel Inkoom', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Platanias', value: 22, group: 5, x: 406.61176, y: 1441.4194}, -{id: 629, label: 'Santi Cazorla', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Arsenal', value: 31, group: 23, x: -670.40643, y: -383.8588}, -{id: 630, label: 'Santiago Arias', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'PSV', value: 25, group: 11, x: -524.84265, y: 1069.8534}, -{id: 631, label: 'Saphir Taïder', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Internazionale', value: 29, group: 24, x: -1233.4976, y: 1029.0317}, -{id: 632, label: 'Sayouba Mandé', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Stabæk', value: 22, group: 9, x: 565.81647, y: -858.44836}, -{id: 633, label: 'Sead Kolašinac', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'Schalke 04', value: 28, group: 20, x: 1107.5244, y: -303.29904}, -{id: 634, label: 'Sebastián Coates', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Nacional', value: 22, group: 6, x: -52.670105, y: 55.847183}, -{id: 635, label: 'Sejad Salihovic', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + '1899 Hoffenheim', value: 23, group: 20, x: 1178.5911, y: -598.751}, -{id: 636, label: 'Senad Lulic', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'Lazio', value: 28, group: 20, x: 921.65936, y: -424.2279}, -{id: 637, label: 'Senijad Ibricic', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'Kayseri Erciyesspor', value: 22, group: 20, x: 1235.9749, y: -497.09393}, -{id: 638, label: 'Serey Die', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Basel', value: 26, group: 9, x: 467.3826, y: -653.70386}, -{id: 639, label: 'Serge Aurier', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Toulouse', value: 23, group: 9, x: 471.92194, y: -746.91907}, -{id: 640, label: 'Sergei Ignashevich', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'CSKA Moscow', value: 23, group: 2, x: -1314.4222, y: -1444.7848}, -{id: 641, label: 'Sergey Ryzhikov', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'Rubin Kazan', value: 23, group: 2, x: -1292.9913, y: -1369.3878}, -{id: 642, label: 'Sergio Agüero', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Manchester City', value: 29, group: 19, x: -986.27966, y: 70.57652}, -{id: 643, label: 'Sergio Busquets', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Barcelona', value: 31, group: 23, x: -999.5799, y: -234.1426}, -{id: 644, label: 'Sergio Ramos', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Real Madrid', value: 31, group: 23, x: -838.31433, y: -237.33427}, -{id: 645, label: 'Sergio Romero', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'AS Monaco', value: 25, group: 19, x: -1110.6039, y: 391.88278}, -{id: 646, label: 'Shinji Kagawa', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'Manchester United', value: 35, group: 27, x: 282.65262, y: 314.0348}, -{id: 647, label: 'Shinji Okazaki', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'Mainz 05', value: 26, group: 27, x: 873.3198, y: 703.759}, -{id: 648, label: 'Shkodran Mustafi', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Sampdoria', value: 22, group: 13, x: 459.89215, y: -438.27008}, -{id: 649, label: 'Shola Ameobi', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Newcastle United', value: 27, group: 14, x: 18.686876, y: -1408.742}, -{id: 650, label: 'Shuichi Gonda', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'F.C. Tokyo', value: 22, group: 27, x: 757.8243, y: 624.09985}, -{id: 651, label: 'Shusaku Nishikawa', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'Urawa Red Diamonds', value: 22, group: 27, x: 727.42017, y: 656.2659}, -{id: 652, label: 'Silvestre Varela', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Porto', value: 30, group: 8, x: -839.6357, y: 400.2162}, -{id: 653, label: 'Šime Vrsaljko', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Genoa', value: 24, group: 25, x: -183.16594, y: 697.412}, -{id: 654, label: 'Simon Mignolet', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Liverpool', value: 31, group: 28, x: -491.45493, y: -919.83154}, -{id: 655, label: 'Sofiane Feghouli', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Valencia', value: 26, group: 24, x: -1244.9492, y: 1115.6299}, -{id: 656, label: 'Sokratis Papastathopoulos', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'Borussia Dortmund', value: 27, group: 15, x: 1506.5099, y: 339.67212}, -{id: 657, label: 'Sol Bamba', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Trabzonspor', value: 22, group: 9, x: 570.6759, y: -908.82056}, -{id: 658, label: 'Son Heung-min', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Bayer Leverkusen', value: 24, group: 10, x: 1048.6976, y: 1445.7692}, -{id: 659, label: 'Stefan de Vrij', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Feyenoord', value: 22, group: 22, x: 967.54407, y: 46.134007}, -{id: 660, label: 'Stefanos Kapino', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'Panathinaikos', value: 24, group: 15, x: 1427.7283, y: 531.81995}, -{id: 661, label: 'Stephan Lichtsteiner', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'Juventus', value: 33, group: 0, x: 67.66878, y: 456.67883}, -{id: 662, label: 'Stéphane Mbia', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Sevilla', value: 25, group: 17, x: 284.887, y: 226.59521}, -{id: 663, label: 'Stéphane Ruffier', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Saint-Étienne', value: 24, group: 16, x: 44.785976, y: -265.3774}, -{id: 664, label: 'Stephen Adams', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Aduana Stars', value: 22, group: 5, x: 502.8429, y: 1418.3192}, -{id: 665, label: 'Steve von Bergen', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'Young Boys', value: 22, group: 0, x: 10.2854805, y: 206.53181}, -{id: 666, label: 'Steven Beitashour', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Vancouver Whitecaps FC', value: 22, group: 1, x: 1978.9785, y: 1007.8008}, -{id: 667, label: 'Steven Defour', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Porto', value: 30, group: 28, x: -855.4899, y: -553.74506}, -{id: 668, label: 'Steven Gerrard (c)', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Liverpool', value: 27, group: 28, x: -159.6521, y: -980.6687}, -{id: 669, label: 'Stipe Pletikosa', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Rostov', value: 22, group: 25, x: -333.2818, y: 696.163}, -{id: 670, label: 'Sulley Muntari', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Milan', value: 28, group: 5, x: 435.759, y: 1263.3812}, -{id: 671, label: 'Sylvain Gbohouo', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Séwé Sport', value: 22, group: 9, x: 531.5453, y: -936.86206}, -{id: 672, label: 'Teófilo Gutiérrez', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'River Plate', value: 22, group: 11, x: -811.0555, y: 1271.3983}, -{id: 673, label: 'Terence Kongolo', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Feyenoord', value: 22, group: 22, x: 966.41876, y: -4.162721}, -{id: 674, label: 'Theofanis Gekas', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'Konyaspor', value: 23, group: 15, x: 1527.9011, y: 552.6124}, -{id: 675, label: 'Thiago Motta', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Paris Saint-Germain', value: 29, group: 3, x: 60.09504, y: 671.3873}, -{id: 676, label: 'Thiago Silva (c)', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Paris Saint-Germain', value: 30, group: 23, x: -361.46573, y: -169.68611}, -{id: 677, label: 'Thibaut Courtois', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Atlético Madrid', value: 29, group: 28, x: -784.1882, y: -694.4416}, -{id: 678, label: 'Thomas Müller', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Bayern Munich', value: 29, group: 13, x: 396.2324, y: -434.3364}, -{id: 679, label: 'Thomas Vermaelen', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Arsenal', value: 31, group: 28, x: -482.76413, y: -771.15424}, -{id: 680, label: 'Tim Cahill', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'New York Red Bulls', value: 23, group: 12, x: 2114.505, y: -511.01007}, -{id: 681, label: 'Tim Howard', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'Everton', value: 27, group: 26, x: 597.101, y: -1458.6305}, -{id: 682, label: 'Tim Krul', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Newcastle United', value: 27, group: 22, x: 749.57495, y: -122.823105}, -{id: 683, label: 'Timothy Chandler', title: 'Country: ' + 'United States' + '
' + 'Team: ' + '1. FC Nürnberg', value: 25, group: 26, x: 803.35706, y: -1282.8247}, -{id: 684, label: 'Tino-Sven Sušic', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'Hajduk Split', value: 22, group: 20, x: 1264.119, y: -534.24}, -{id: 685, label: 'Toby Alderweireld', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Atlético Madrid', value: 29, group: 28, x: -719.4183, y: -665.748}, -{id: 686, label: 'Tommy Oar', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Utrecht', value: 22, group: 12, x: 2165.0227, y: -713.54254}, -{id: 687, label: 'Toni Kroos', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Bayern Munich', value: 29, group: 13, x: 364.47653, y: -371.89417}, -{id: 688, label: 'Toni Šunjic', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'Zorya Luhansk', value: 22, group: 20, x: 1221.8553, y: -554.841}, -{id: 689, label: 'Toshihiro Aoyama', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'Sanfrecce Hiroshima', value: 23, group: 27, x: 774.47, y: 733.8078}, -{id: 690, label: 'Tranquillo Barnetta', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'Eintracht Frankfurt', value: 23, group: 0, x: 73.72464, y: 117.78337}, -{id: 691, label: 'Uche Nwofor', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Heerenveen', value: 22, group: 14, x: -33.31396, y: -1701.1675}, -{id: 692, label: 'Valentin Stocker', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'Basel', value: 25, group: 0, x: 93.94299, y: 165.77863}, -{id: 693, label: 'Valon Behrami', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'Napoli', value: 31, group: 0, x: -152.94186, y: 233.43562}, -{id: 694, label: 'Vangelis Moras', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'Verona', value: 22, group: 15, x: 1602.7228, y: 488.25735}, -{id: 695, label: 'Vasili Berezutski (c)', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'CSKA Moscow', value: 23, group: 2, x: -1323.1439, y: -1494.2708}, -{id: 696, label: 'Vasilis Torosidis', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'Roma', value: 26, group: 15, x: 1423.1809, y: 425.1927}, -{id: 697, label: 'Vedad Ibiševic', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'VfB Stuttgart', value: 25, group: 20, x: 1011.34985, y: -507.73672}, -{id: 698, label: 'Vedran Corluka', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Lokomotiv Moscow', value: 23, group: 25, x: -415.4615, y: 539.5565}, -{id: 699, label: 'Victor', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Atlético Mineiro', value: 22, group: 23, x: -504.1157, y: -310.5912}, -{id: 700, label: 'Víctor Bernárdez', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'San Jose Earthquakes', value: 23, group: 7, x: 1542.3271, y: -1230.5049}, -{id: 701, label: 'Víctor Ibarbo', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'Cagliari', value: 23, group: 11, x: -760.3384, y: 1293.0891}, -{id: 702, label: 'Victor Moses', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Liverpool', value: 31, group: 14, x: -114.12856, y: -1433.1643}, -{id: 703, label: 'Vieirinha', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'VfL Wolfsburg', value: 28, group: 8, x: -584.53986, y: 300.7302}, -{id: 704, label: 'Viktor Fayzulin', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'Zenit Saint Petersburg', value: 26, group: 2, x: -1257.4415, y: -1320.7031}, -{id: 705, label: 'Vincent Aboubakar', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Lorient', value: 22, group: 17, x: 458.34485, y: 202.27162}, -{id: 706, label: 'Vincent Enyeama', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Lille', value: 25, group: 14, x: -105.49051, y: -1519.4764}, -{id: 707, label: 'Vincent Kompany (c)', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Manchester City', value: 31, group: 28, x: -575.3739, y: -726.92163}, -{id: 708, label: 'Vladimir Granat', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'Dynamo Moscow', value: 23, group: 2, x: -1378.1497, y: -1417.719}, -{id: 709, label: 'Wakaso Mubarak', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Rubin Kazan', value: 25, group: 5, x: 209.43652, y: 1057.448}, -{id: 710, label: 'Walter Ayoví', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Pachuca', value: 22, group: 4, x: -1792.0483, y: -657.5009}, -{id: 711, label: 'Walter Gargano', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Parma', value: 26, group: 6, x: -40.095936, y: 145.01854}, -{id: 712, label: 'Waylon Francis', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Columbus Crew', value: 22, group: 29, x: 2350.4897, y: 280.31845}, -{id: 713, label: 'Wayne Rooney', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Manchester United', value: 32, group: 28, x: -356.85434, y: -834.0883}, -{id: 714, label: 'Wesley Sneijder', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Galatasaray', value: 26, group: 22, x: 805.6672, y: -40.132378}, -{id: 715, label: 'Wilfried Bony', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Swansea City', value: 24, group: 9, x: 607.71, y: -803.1463}, -{id: 716, label: 'William Carvalho', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Sporting CP', value: 24, group: 8, x: -772.3611, y: 375.09537}, -{id: 717, label: 'Willian', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Chelsea', value: 30, group: 23, x: -440.73843, y: -410.8239}, -{id: 718, label: 'Wilson Palacios', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Stoke City', value: 25, group: 7, x: 1475.9537, y: -1233.8828}, -{id: 719, label: 'Xabi Alonso', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Real Madrid', value: 31, group: 23, x: -899.6201, y: -193.28745}, -{id: 720, label: 'Xavi', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Barcelona', value: 31, group: 23, x: -1013.3928, y: -319.86545}, -{id: 721, label: 'Xherdan Shaqiri', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'Bayern Munich', value: 35, group: 0, x: 141.7251, y: 12.289529}, -{id: 722, label: 'Yacine Brahimi', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Granada', value: 24, group: 24, x: -1176.7251, y: 1144.9346}, -{id: 723, label: 'Yann Sommer', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'Basel', value: 25, group: 0, x: 110.022545, y: 216.66074}, -{id: 724, label: 'Yasuhito Endo', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'Gamba Osaka', value: 22, group: 27, x: 785.91925, y: 586.32904}, -{id: 725, label: 'Yasuyuki Konno', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'Gamba Osaka', value: 22, group: 27, x: 772.3632, y: 672.5744}, -{id: 726, label: 'Yaya Touré', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Manchester City', value: 31, group: 9, x: 251.69077, y: -758.7758}, -{id: 727, label: 'Yeltsin Tejeda', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Saprissa', value: 22, group: 29, x: 2354.9373, y: 330.56363}, -{id: 728, label: 'Yohan Cabaye', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Paris Saint-Germain', value: 29, group: 16, x: -73.94801, y: -145.80449}, -{id: 729, label: 'Yoichiro Kakitani', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'Cerezo Osaka', value: 23, group: 27, x: 646.941, y: 622.23926}, -{id: 730, label: 'Yoshito Okubo', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'Kawasaki Frontale', value: 22, group: 27, x: 717.32806, y: 699.96234}, -{id: 731, label: 'Yun Suk-young', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Queens Park Rangers', value: 23, group: 10, x: 1131.6682, y: 1494.4373}, -{id: 732, label: 'Yuri Lodygin', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'Zenit Saint Petersburg', value: 26, group: 2, x: -1301.0415, y: -1265.7511}, -{id: 733, label: 'Yuri Zhirkov', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'Dynamo Moscow', value: 23, group: 2, x: -1464.4825, y: -1475.7117}, -{id: 734, label: 'Yuto Nagatomo', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'Internazionale', value: 29, group: 27, x: 395.00394, y: 607.5659}, -{id: 735, label: 'Yuya Osako', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + '1860 München', value: 22, group: 27, x: 806.69904, y: 633.54565}, -{id: 736, label: 'Zvjezdan Misimovic', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'Guizhou Renhe', value: 22, group: 20, x: 1277.4697, y: -479.12265} + function redrawAll() { + network = null; + // create an array with nodes + var nodes = [ + {id: 1, label: 'Abdelmoumene Djabou', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Club Africain', value: 22, group: 24, x: -1392.5499, y: 1124.1614}, + {id: 2, label: 'Abel Aguilar', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'Toulouse', value: 24, group: 11, x: -660.82574, y: 1009.18976}, + {id: 3, label: 'Abel Hernández', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Palermo', value: 22, group: 6, x: -85.6025, y: -6.6782646}, + {id: 4, label: 'Adam Kwarasey', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Strømsgodset', value: 22, group: 5, x: 427.39853, y: 1398.1719}, + {id: 5, label: 'Adam Lallana', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Southampton', value: 26, group: 28, x: -133.68427, y: -732.50476}, + {id: 6, label: 'Adam Taggart', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Newcastle Jets', value: 22, group: 12, x: 2042.4272, y: -579.6042}, + {id: 7, label: 'Admir Mehmedi', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'SC Freiburg', value: 24, group: 0, x: 126.91814, y: 115.84123}, + {id: 8, label: 'Adnan Januzaj', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Manchester United', value: 34, group: 28, x: -638.503, y: -663.07904}, + {id: 9, label: 'Adrián Bone', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'El Nacional', value: 22, group: 4, x: -1657.1593, y: -645.2429}, + {id: 10, label: 'Adrián Ramos', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'Hertha BSC', value: 23, group: 11, x: -712.13385, y: 1053.3159}, + {id: 11, label: 'Afriyie Acquah', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Parma', value: 26, group: 5, x: 358.25735, y: 1238.4801}, + {id: 12, label: 'Agustín Orión', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Boca Juniors', value: 22, group: 19, x: -1115.8746, y: 250.34308}, + {id: 13, label: 'Ahmad Alenemeh', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Naft Tehran', value: 22, group: 1, x: 2028.4565, y: 1067.9126}, + {id: 14, label: 'Ahmed Musa', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'CSKA Moscow', value: 27, group: 14, x: -341.64163, y: -1640.5049}, + {id: 15, label: 'Aïssa Mandi', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Reims', value: 22, group: 24, x: -1380.8287, y: 1169.2931}, + {id: 16, label: 'Alan Dzagoev', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'CSKA Moscow', value: 23, group: 2, x: -1268.165, y: -1469.7052}, + {id: 17, label: 'Alan Pulido', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'UANL', value: 22, group: 21, x: -2016.3092, y: 442.13663}, + {id: 18, label: 'Albert Adomah', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Middlesbrough', value: 23, group: 5, x: 449.02316, y: 1183.7205}, + {id: 19, label: 'Alberto Aquilani', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Fiorentina', value: 24, group: 3, x: 51.16946, y: 883.6703}, + {id: 20, label: 'Alejandro Bedoya', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'Nantes', value: 22, group: 26, x: 784.4289, y: -1547.6515}, + {id: 21, label: 'Aleksandr Kerzhakov', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'Zenit Saint Petersburg', value: 26, group: 2, x: -1228.8892, y: -1267.067}, + {id: 22, label: 'Aleksandr Kokorin', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'Dynamo Moscow', value: 23, group: 2, x: -1414.3739, y: -1377.2596}, + {id: 23, label: 'Aleksandr Samedov', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'Lokomotiv Moscow', value: 23, group: 2, x: -1362.3624, y: -1347.75}, + {id: 24, label: 'Aleksei Ionov', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'Dynamo Moscow', value: 23, group: 2, x: -1428.0071, y: -1427.2177}, + {id: 25, label: 'Aleksei Kozlov', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'Dynamo Moscow', value: 23, group: 2, x: -1463.2527, y: -1376.6138}, + {id: 26, label: 'Alessio Cerci', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Torino', value: 23, group: 3, x: 276.62708, y: 826.51605}, + {id: 27, label: 'Alex Oxlade-Chamberlain', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Arsenal', value: 30, group: 28, x: -56.50232, y: -825.3445}, + {id: 28, label: 'Alex Song', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Barcelona', value: 37, group: 17, x: -256.07828, y: 56.990772}, + {id: 29, label: 'Alex Wilkinson', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Jeonbuk Hyundai Motors', value: 22, group: 12, x: 2120.3818, y: -724.748}, + {id: 30, label: 'Alexander Domínguez', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'LDU Quito', value: 22, group: 4, x: -1643.0283, y: -689.7502}, + {id: 31, label: 'Alexander Mejía', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'Atlético Nacional', value: 22, group: 11, x: -761.32623, y: 1152.3298}, + {id: 32, label: 'Alexandros Tziolis', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'Kayserispor', value: 22, group: 15, x: 1617.3293, y: 542.81915}, + {id: 33, label: 'Alexis Sánchez', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Barcelona', value: 37, group: 18, x: -613.0529, y: 828.08685}, + {id: 34, label: 'Alfredo Talavera', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'Toluca', value: 22, group: 21, x: -1995.7101, y: 401.94843}, + {id: 35, label: 'Alireza Haghighi', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Sporting Covilhã', value: 22, group: 1, x: 1910.1731, y: 1066.8309}, + {id: 36, label: 'Alireza Jahanbakhsh', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'NEC', value: 22, group: 1, x: 1942.0732, y: 1034.9001}, + {id: 37, label: 'Allan Nyom', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Granada', value: 24, group: 17, x: 381.53027, y: 285.77576}, + {id: 38, label: 'Álvaro González', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Lazio', value: 28, group: 6, x: 13.4137335, y: -43.777435}, + {id: 39, label: 'Álvaro Pereira', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'São Paulo', value: 22, group: 6, x: -93.8017, y: 34.243332}, + {id: 40, label: 'Amir Hossein Sadeghi', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Esteghlal', value: 22, group: 1, x: 1990.1855, y: 1052.6255}, + {id: 41, label: 'Andranik Teymourian', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Esteghlal', value: 22, group: 1, x: 1940.6577, y: 1114.8914}, + {id: 42, label: 'André Almeida', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Benfica', value: 25, group: 8, x: -733.05725, y: 266.987}, + {id: 43, label: 'André Ayew', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Marseille', value: 24, group: 5, x: 486.66187, y: 1226.3735}, + {id: 44, label: 'André Schürrle', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Chelsea', value: 33, group: 13, x: 130.8471, y: -528.93024}, + {id: 45, label: 'Andrea Barzagli', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Juventus', value: 28, group: 3, x: 109.97049, y: 937.16266}, + {id: 46, label: 'Andrea Pirlo', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Juventus', value: 28, group: 3, x: 108.0534, y: 870.1171}, + {id: 47, label: 'Andreas Samaris', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'Olympiacos', value: 23, group: 15, x: 1692.9755, y: 475.92816}, + {id: 48, label: 'Andrei Semyonov', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'Terek Grozny', value: 22, group: 2, x: -1427.7258, y: -1522.6016}, + {id: 49, label: 'Andrés Guardado', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'Bayer Leverkusen', value: 24, group: 21, x: -1822.0682, y: 449.03262}, + {id: 50, label: 'Andrés Iniesta', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Barcelona', value: 31, group: 23, x: -1067.9244, y: -187.44284}, + {id: 51, label: 'Andrey Yeshchenko', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'Anzhi Makhachkala', value: 22, group: 2, x: -1412.1168, y: -1477.2361}, + {id: 52, label: 'Andy Najar', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Anderlecht', value: 23, group: 7, x: 1494.2014, y: -1172.4867}, + {id: 53, label: 'Anel Hadžic', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'Sturm Graz', value: 22, group: 20, x: 1149.5178, y: -490.41513}, + {id: 54, label: 'Ángel di María', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Real Madrid', value: 33, group: 19, x: -968.5764, y: 161.48494}, + {id: 55, label: 'Ante Rebic', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Fiorentina', value: 24, group: 25, x: -308.12177, y: 744.399}, + {id: 56, label: 'Anthony Vanden Borre', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Anderlecht', value: 23, group: 28, x: -577.6633, y: -888.84265}, + {id: 57, label: 'Antoine Griezmann', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Real Sociedad', value: 25, group: 16, x: 63.922184, y: -173.65816}, + {id: 58, label: 'Antonio Candreva', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Lazio', value: 28, group: 3, x: 180.96414, y: 574.7693}, + {id: 59, label: 'Antonio Cassano', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Parma', value: 24, group: 3, x: 193.04764, y: 758.9299}, + {id: 60, label: 'Antonio Valencia (c)', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Manchester United', value: 35, group: 4, x: -1293.8275, y: -612.48834}, + {id: 61, label: 'Arjen Robben', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Bayern Munich', value: 35, group: 22, x: 630.80566, y: -143.44237}, + {id: 62, label: 'Aron Jóhannsson', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'AZ', value: 22, group: 26, x: 819.32007, y: -1520.0212}, + {id: 63, label: 'Arthur Boka', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'VfB Stuttgart', value: 25, group: 9, x: 447.86835, y: -798.1806}, + {id: 64, label: 'Arturo Vidal', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Juventus', value: 32, group: 18, x: -116.507996, y: 1233.55}, + {id: 65, label: 'Asamoah Gyan (c)', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Al-Ain', value: 22, group: 5, x: 384.49658, y: 1385.8724}, + {id: 66, label: 'Ashkan Dejagah', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Fulham', value: 24, group: 1, x: 1842.1604, y: 978.62915}, + {id: 67, label: 'Asmir Avdukic', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'Borac Banja Luka', value: 22, group: 20, x: 1126.5564, y: -529.6863}, + {id: 68, label: 'Asmir Begovic', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'Stoke City', value: 25, group: 20, x: 1126.9225, y: -656.7364}, + {id: 69, label: 'Atsuto Uchida', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'Schalke 04', value: 28, group: 27, x: 789.175, y: 479.11423}, + {id: 70, label: 'Augusto Fernández', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Celta Vigo', value: 23, group: 19, x: -1096.7728, y: 332.52386}, + {id: 71, label: 'Aurélien Chedjou', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Galatasaray', value: 26, group: 17, x: 479.9816, y: 42.06589}, + {id: 72, label: 'Austin Ejide', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Hapoel Beer Sheva', value: 22, group: 14, x: -127.8801, y: -1587.7189}, + {id: 73, label: 'Avdija Vršajevic', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'Hajduk Split', value: 22, group: 20, x: 1155.9982, y: -446.01266}, + {id: 74, label: 'Axel Witsel', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Zenit Saint Petersburg', value: 28, group: 28, x: -844.52124, y: -894.0247}, + {id: 75, label: 'Azubuike Egwuekwe', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Warri Wolves', value: 22, group: 14, x: -40.194813, y: -1612.7229}, + {id: 76, label: 'Bacary Sagna', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Arsenal', value: 29, group: 16, x: -102.573074, y: -365.21664}, + {id: 77, label: 'Bailey Wright', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Preston North End', value: 22, group: 12, x: 2074.923, y: -613.972}, + {id: 78, label: 'Bakhtiar Rahmani', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Foolad', value: 22, group: 1, x: 2063.0938, y: 1033.574}, + {id: 79, label: 'Bastian Schweinsteiger', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Bayern Munich', value: 29, group: 13, x: 244.85414, y: -373.98276}, + {id: 80, label: 'Ben Foster', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'West Bromwich Albion', value: 23, group: 28, x: -170.48405, y: -869.56903}, + {id: 81, label: 'Ben Halloran', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Fortuna Düsseldorf', value: 23, group: 12, x: 1954.242, y: -623.5981}, + {id: 82, label: 'Benedikt Höwedes', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Schalke 04', value: 27, group: 13, x: 472.64325, y: -229.06421}, + {id: 83, label: 'Benjamin Moukandjo', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Nancy', value: 22, group: 17, x: 415.3849, y: 99.65612}, + {id: 84, label: 'Benoît Assou-Ekotto', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Queens Park Rangers', value: 23, group: 17, x: 484.1712, y: 273.5127}, + {id: 85, label: 'Bernard', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Shakhtar Donetsk', value: 24, group: 23, x: -458.8, y: -206.65053}, + {id: 86, label: 'Beto', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Sevilla', value: 25, group: 8, x: -614.7038, y: 392.89618}, + {id: 87, label: 'Blaise Matuidi', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Paris Saint-Germain', value: 29, group: 16, x: -108.933846, y: -90.56801}, + {id: 88, label: 'Blerim Džemaili', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'Napoli', value: 31, group: 0, x: -243.03868, y: 290.13797}, + {id: 89, label: 'Boubacar Barry', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Lokeren', value: 22, group: 9, x: 488.79492, y: -907.9203}, + {id: 90, label: 'Brad Davis', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'Houston Dynamo', value: 23, group: 26, x: 915.66956, y: -1565.8953}, + {id: 91, label: 'Brad Guzan', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'Aston Villa', value: 23, group: 26, x: 829.8172, y: -1411.8826}, + {id: 92, label: 'Brayan Beckeles', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Olimpia', value: 22, group: 7, x: 1616.757, y: -1172.5592}, + {id: 93, label: 'Bruno Alves', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Fenerbahçe', value: 25, group: 8, x: -538.8344, y: 183.03185}, + {id: 94, label: 'Bruno Martins Indi', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Feyenoord', value: 22, group: 22, x: 870.94403, y: 71.02484}, + {id: 95, label: 'Bryan Ruiz (c)', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'PSV', value: 25, group: 29, x: 2006.2959, y: 332.36353}, + {id: 96, label: 'Camilo Vargas', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'Santa Fe', value: 23, group: 11, x: -870.7738, y: 1102.7423}, + {id: 97, label: 'Carl Medjani', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Valenciennes', value: 23, group: 24, x: -1275.9651, y: 1205.1012}, + {id: 98, label: 'Carlo Costly', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Real España', value: 22, group: 7, x: 1569.5697, y: -1167.269}, + {id: 99, label: 'Carlos Bacca', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'Sevilla', value: 25, group: 11, x: -687.1921, y: 1106.8958}, + {id: 100, label: 'Carlos Carbonero', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'River Plate', value: 22, group: 11, x: -742.21783, y: 1199.1262}, + {id: 101, label: 'Carlos Carmona', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Atalanta', value: 23, group: 18, x: -345.68073, y: 1473.0652}, + {id: 102, label: 'Carlos Gruezo', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'VfB Stuttgart', value: 25, group: 4, x: -1417.159, y: -636.35205}, + {id: 103, label: 'Carlos Peña', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'León', value: 22, group: 21, x: -2037.2489, y: 386.77597}, + {id: 104, label: 'Carlos Salcido', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'UANL', value: 22, group: 21, x: -2011.8602, y: 347.69363}, + {id: 105, label: 'Carlos Sánchez', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'Elche', value: 22, group: 11, x: -775.67804, y: 1232.4089}, + {id: 106, label: 'Carlos Valdés', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'San Lorenzo', value: 22, group: 11, x: -788.68494, y: 1186.096}, + {id: 107, label: 'Cédric Djeugoué', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Coton Sport', value: 22, group: 17, x: 458.03027, y: 113.75822}, + {id: 108, label: 'Cédric Si Mohamed', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'CS Constantine', value: 22, group: 24, x: -1432.4459, y: 1140.2423}, + {id: 109, label: 'Celso Borges', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'AIK', value: 22, group: 29, x: 2214.5396, y: 283.79788}, + {id: 110, label: 'César Azpilicueta', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Chelsea', value: 32, group: 23, x: -780.85876, y: -518.6595}, + {id: 111, label: 'Cesc Fàbregas', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Barcelona', value: 31, group: 23, x: -1070.0735, y: -271.46603}, + {id: 112, label: 'Charles Aránguiz', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Internacional', value: 22, group: 18, x: -251.59665, y: 1476.4546}, + {id: 113, label: 'Charles Itandje', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Konyaspor', value: 23, group: 17, x: 514.87463, y: 203.30963}, + {id: 114, label: 'Cheick Tioté', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Newcastle United', value: 27, group: 9, x: 389.42743, y: -827.5475}, + {id: 115, label: 'Chigozie Agbim', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Gombe United', value: 22, group: 14, x: -67.006065, y: -1575.516}, + {id: 116, label: 'Chris Smalling', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Manchester United', value: 32, group: 28, x: -375.02072, y: -737.6564}, + {id: 117, label: 'Chris Wondolowski', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'San Jose Earthquakes', value: 23, group: 26, x: 915.553, y: -1512.6752}, + {id: 118, label: 'Christian Atsu', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Vitesse', value: 23, group: 5, x: 298.6339, y: 1290.5527}, + {id: 119, label: 'Christian Bolaños', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Copenhagen', value: 22, group: 29, x: 2234.7017, y: 376.9046}, + {id: 120, label: 'Christian Noboa', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Dynamo Moscow', value: 28, group: 4, x: -1672.2358, y: -885.3366}, + {id: 121, label: 'Christian Stuani', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Espanyol', value: 23, group: 6, x: -159.9744, y: 40.993885}, + {id: 122, label: 'Christoph Kramer', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Borussia Mönchengladbach', value: 23, group: 13, x: 422.9451, y: -364.46622}, + {id: 123, label: 'Ciro Immobile', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Torino', value: 23, group: 3, x: 317.4282, y: 794.25037}, + {id: 124, label: 'Claudio Bravo (c)', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Real Sociedad', value: 25, group: 18, x: -193.70801, y: 1267.7544}, + {id: 125, label: 'Claudio Marchisio', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Juventus', value: 28, group: 3, x: 71.69534, y: 813.5998}, + {id: 126, label: 'Clint Dempsey (c)', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'Seattle Sounders FC', value: 22, group: 26, x: 742.0546, y: -1547.4186}, + {id: 127, label: 'Constant Djakpa', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Eintracht Frankfurt', value: 23, group: 9, x: 513.1434, y: -809.9959}, + {id: 128, label: 'Cristian Gamboa', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Rosenborg', value: 23, group: 29, x: 2154.0825, y: 199.01004}, + {id: 129, label: 'Cristian Rodríguez', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Atlético Madrid', value: 28, group: 6, x: -272.89346, y: -76.41096}, + {id: 130, label: 'Cristián Zapata', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'Milan', value: 29, group: 11, x: -503.784, y: 1159.0504}, + {id: 131, label: 'Cristiano Ronaldo (c)', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Real Madrid', value: 31, group: 8, x: -705.8994, y: 163.73811}, + {id: 132, label: 'Cristopher Toselli', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Universidad Católica', value: 22, group: 18, x: -291.25885, y: 1453.383}, + {id: 133, label: 'Daley Blind', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Ajax', value: 22, group: 22, x: 865.13696, y: -4.895512}, + {id: 134, label: 'DaMarcus Beasley', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'Puebla', value: 22, group: 26, x: 860.4318, y: -1509.4606}, + {id: 135, label: 'Dani Alves', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Barcelona', value: 36, group: 23, x: -742.1678, y: -271.698}, + {id: 136, label: 'Daniel Cambronero', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Herediano', value: 22, group: 29, x: 2228.9766, y: 327.5744}, + {id: 137, label: 'Daniel Davari', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Eintracht Braunschweig', value: 23, group: 1, x: 1905.6099, y: 955.88916}, + {id: 138, label: 'Daniel Opare', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Standard Liège', value: 24, group: 5, x: 399.65134, y: 1199.5255}, + {id: 139, label: 'Daniel Sturridge', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Liverpool', value: 27, group: 28, x: -202.59894, y: -933.40094}, + {id: 140, label: 'Daniel Van Buyten', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Bayern Munich', value: 35, group: 28, x: -361.6232, y: -626.74445}, + {id: 141, label: 'Daniele De Rossi', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Roma', value: 26, group: 3, x: 294.1721, y: 656.48535}, + {id: 142, label: 'Danijel Pranjic', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Panathinaikos', value: 23, group: 25, x: -193.00035, y: 612.0998}, + {id: 143, label: 'Danijel Subašic', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'AS Monaco', value: 25, group: 25, x: -426.1968, y: 636.2631}, + {id: 144, label: 'Danny Welbeck', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Manchester United', value: 32, group: 28, x: -294.47705, y: -689.56665}, + {id: 145, label: 'Dante', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Bayern Munich', value: 35, group: 23, x: -212.9895, y: -416.65964}, + {id: 146, label: 'Dany Nounkeu', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Be?ikta?', value: 24, group: 17, x: 382.6164, y: 41.81477}, + {id: 147, label: 'Darijo Srna (c)', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Shakhtar Donetsk', value: 23, group: 25, x: -317.20358, y: 580.4689}, + {id: 148, label: 'Dario Vidošic', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Sion', value: 22, group: 12, x: 2016.2832, y: -666.32526}, + {id: 149, label: 'Daryl Janmaat', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Feyenoord', value: 22, group: 22, x: 832.52924, y: 28.84025}, + {id: 150, label: 'David de Gea', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Manchester United', value: 34, group: 23, x: -916.8024, y: -469.95193}, + {id: 151, label: 'David Luiz', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Chelsea', value: 30, group: 23, x: -401.12976, y: -483.5873}, + {id: 152, label: 'David Myrie', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Herediano', value: 22, group: 29, x: 2254.471, y: 256.6007}, + {id: 153, label: 'David Ospina', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'Nice', value: 22, group: 11, x: -821.8875, y: 1214.6177}, + {id: 154, label: 'David Silva', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Manchester City', value: 31, group: 23, x: -782.84827, y: -359.3023}, + {id: 155, label: 'David Villa', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Atlético Madrid', value: 27, group: 23, x: -854.8254, y: -313.94424}, + {id: 156, label: 'DeAndre Yedlin', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'Seattle Sounders FC', value: 22, group: 26, x: 776.44666, y: -1500.7616}, + {id: 157, label: 'Dejan Lovren', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Southampton', value: 28, group: 25, x: -235.10854, y: 422.88907}, + {id: 158, label: 'Denis Glushakov', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'Spartak Moscow', value: 22, group: 2, x: -1381.3909, y: -1518.6675}, + {id: 159, label: 'Didier Drogba (c)', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Galatasaray', value: 26, group: 9, x: 598.48517, y: -735.1734}, + {id: 160, label: 'Didier Ya Konan', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Hannover 96', value: 24, group: 9, x: 543.872, y: -767.347}, + {id: 161, label: 'Didier Zokora', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Trabzonspor', value: 22, group: 9, x: 526.23566, y: -881.0933}, + {id: 162, label: 'Diego Benaglio', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'VfL Wolfsburg', value: 27, group: 0, x: -65.30554, y: 256.20117}, + {id: 163, label: 'Diego Calvo', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Vålerenga', value: 22, group: 29, x: 2308.558, y: 341.58264}, + {id: 164, label: 'Diego Costa', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Atlético Madrid', value: 27, group: 23, x: -946.3432, y: -379.19135}, + {id: 165, label: 'Diego Forlán', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Cerezo Osaka', value: 24, group: 6, x: 22.544487, y: 32.103252}, + {id: 166, label: 'Diego Godín', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Atlético Madrid', value: 28, group: 6, x: -229.68459, y: -28.488848}, + {id: 167, label: 'Diego Lugano (c)', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'West Bromwich Albion', value: 23, group: 6, x: -32.813736, y: -13.457554}, + {id: 168, label: 'Diego Pérez', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Bologna', value: 24, group: 6, x: 71.02754, y: 37.87593}, + {id: 169, label: 'Diego Reyes', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'Porto', value: 29, group: 21, x: -1751.0813, y: 432.33847}, + {id: 170, label: 'Dimitris Salpingidis', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'PAOK', value: 22, group: 15, x: 1578.1974, y: 570.63684}, + {id: 171, label: 'Dirk Kuyt', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Fenerbahçe', value: 26, group: 22, x: 698.83246, y: -15.171172}, + {id: 172, label: 'Divock Origi', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Lille', value: 25, group: 28, x: -634.9317, y: -895.1274}, + {id: 173, label: 'Djamel Mesbah', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Livorno', value: 22, group: 24, x: -1360.7583, y: 1211.4519}, + {id: 174, label: 'Dmitri Kombarov', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'Spartak Moscow', value: 22, group: 2, x: -1369.3798, y: -1467.8458}, + {id: 175, label: 'Domagoj Vida', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Dynamo Kyiv', value: 24, group: 25, x: -257.23795, y: 568.68097}, + {id: 176, label: 'Donis Escober', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Olimpia', value: 22, group: 7, x: 1653.151, y: -1192.2112}, + {id: 177, label: 'Dries Mertens', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Napoli', value: 33, group: 28, x: -646.4434, y: -473.2636}, + {id: 178, label: 'Edder Delgado', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Real España', value: 22, group: 7, x: 1622.0984, y: -1283.4814}, + {id: 179, label: 'Eden Hazard', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Chelsea', value: 33, group: 28, x: -567.4557, y: -819.40875}, + {id: 180, label: 'Éder', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Braga', value: 22, group: 8, x: -652.50696, y: 328.93912}, + {id: 181, label: 'Éder Álvarez Balanta', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'River Plate', value: 22, group: 11, x: -862.32965, y: 1190.2361}, + {id: 182, label: 'Edgar Salli', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Lens', value: 22, group: 17, x: 416.1859, y: 196.34885}, + {id: 183, label: 'Edin Džeko', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'Manchester City', value: 31, group: 20, x: 747.8557, y: -487.7818}, + {id: 184, label: 'Edin Višca', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + '?stanbul Ba?ak?ehir', value: 22, group: 20, x: 1198.7845, y: -465.6674}, + {id: 185, label: 'Edinson Cavani', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Paris Saint-Germain', value: 31, group: 6, x: -109.8151, y: 97.26505}, + {id: 186, label: 'Édison Méndez', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Santa Fe', value: 23, group: 4, x: -1680.7289, y: -523.78754}, + {id: 187, label: 'Eduardo da Silva', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Shakhtar Donetsk', value: 23, group: 25, x: -364.4046, y: 586.62573}, + {id: 188, label: 'Eduardo dos Reis Carvalho', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Braga', value: 22, group: 8, x: -685.56335, y: 299.7952}, + {id: 189, label: 'Eduardo Vargas', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Valencia', value: 26, group: 18, x: -348.8911, y: 1339.4359}, + {id: 190, label: 'Efe Ambrose', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Celtic', value: 25, group: 14, x: 91.53676, y: -1502.4221}, + {id: 191, label: 'Egidio Arévalo Ríos', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Morelia', value: 23, group: 6, x: -140.449, y: -11.467088}, + {id: 192, label: 'Ehsan Hajsafi', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Sepahan', value: 22, group: 1, x: 1992.8684, y: 1102.4463}, + {id: 193, label: 'Eiji Kawashima', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'Standard Liège', value: 24, group: 27, x: 599.24896, y: 588.35046}, + {id: 194, label: 'Ejike Uzoenyi', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Enugu Rangers', value: 22, group: 14, x: -90.413765, y: -1613.6277}, + {id: 195, label: 'El Arbi Hillel Soudani', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Dinamo Zagreb', value: 23, group: 24, x: -1331.9408, y: 1124.3699}, + {id: 196, label: 'Eliaquim Mangala', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Porto', value: 30, group: 16, x: -347.64447, y: -15.025993}, + {id: 197, label: 'Emilio Izaguirre', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Celtic', value: 25, group: 7, x: 1455.9241, y: -1104.4338}, + {id: 198, label: 'Emir Spahic (c)', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'Bayer Leverkusen', value: 24, group: 20, x: 1039.7502, y: -336.38666}, + {id: 199, label: 'Emmanuel Agyemang-Badu', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Udinese', value: 23, group: 5, x: 311.23798, y: 1367.9753}, + {id: 200, label: 'Emmanuel Emenike', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Fenerbahçe', value: 26, group: 14, x: -64.248405, y: -1362.0144}, + {id: 201, label: 'Enner Valencia', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Pachuca', value: 22, group: 4, x: -1712.6265, y: -633.4451}, + {id: 202, label: 'Enzo Pérez', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Benfica', value: 25, group: 19, x: -1057.396, y: 279.50247}, + {id: 203, label: 'Erik Durm', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Borussia Dortmund', value: 24, group: 13, x: 553.0518, y: -438.38715}, + {id: 204, label: 'Ermin Bicakcic', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'Eintracht Braunschweig', value: 23, group: 20, x: 1292.2596, y: -362.45374}, + {id: 205, label: 'Essaïd Belkalem', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Watford', value: 23, group: 24, x: -1238.1655, y: 1250.7357}, + {id: 206, label: 'Esteban Granados', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Herediano', value: 22, group: 29, x: 2281.05, y: 393.73032}, + {id: 207, label: 'Esteban Paredes', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Colo-Colo', value: 22, group: 18, x: -262.22748, y: 1531.8533}, + {id: 208, label: 'Eugene Galekovic', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Adelaide United', value: 22, group: 12, x: 2152.1602, y: -634.9465}, + {id: 209, label: 'Eugenio Mena', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Santos', value: 22, group: 18, x: -294.9122, y: 1499.1805}, + {id: 210, label: 'Eyong Enoh', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Antalyaspor', value: 22, group: 17, x: 420.98795, y: 149.03363}, + {id: 211, label: 'Ezequiel Garay', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Benfica', value: 25, group: 19, x: -1064.4406, y: 219.37395}, + {id: 212, label: 'Ezequiel Lavezzi', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Paris Saint-Germain', value: 31, group: 19, x: -846.7565, y: 254.65596}, + {id: 213, label: 'Fabian Johnson', title: 'Country: ' + 'United States' + '
' + 'Team: ' + '1899 Hoffenheim', value: 23, group: 26, x: 879.29755, y: -1453.8761}, + {id: 214, label: 'Fabián Orellana', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Celta Vigo', value: 23, group: 18, x: -331.13403, y: 1411.2639}, + {id: 215, label: 'Fabian Schär', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'Basel', value: 25, group: 0, x: 38.159084, y: 161.5354}, + {id: 216, label: 'Fábio Coentrão', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Real Madrid', value: 31, group: 8, x: -620.60266, y: 152.43254}, + {id: 217, label: 'Fabrice Olinga', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Zulte Waregem', value: 23, group: 17, x: 342.78528, y: 88.49571}, + {id: 218, label: 'Faouzi Ghoulam', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Napoli', value: 33, group: 24, x: -1163.7886, y: 887.72974}, + {id: 219, label: 'Faryd Mondragón', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'Deportivo Cali', value: 22, group: 11, x: -825.1312, y: 1158.5756}, + {id: 220, label: 'Fatau Dauda', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Orlando Pirates', value: 22, group: 5, x: 508.3159, y: 1362.8381}, + {id: 221, label: 'Federico Fernández', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Napoli', value: 32, group: 19, x: -945.41595, y: 329.4419}, + {id: 222, label: 'Felipe Caicedo', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Al-Jazira', value: 22, group: 4, x: -1726.1598, y: -587.78546}, + {id: 223, label: 'Felipe Gutiérrez', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Twente', value: 22, group: 18, x: -184.13504, y: 1490.4882}, + {id: 224, label: 'Fernandinho', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Manchester City', value: 31, group: 23, x: -442.97876, y: -336.2658}, + {id: 225, label: 'Fernando Gago', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Boca Juniors', value: 22, group: 19, x: -1147.289, y: 214.82018}, + {id: 226, label: 'Fernando Muslera', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Galatasaray', value: 26, group: 6, x: 73.75355, y: -37.71824}, + {id: 227, label: 'Fernando Torres', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Chelsea', value: 32, group: 23, x: -744.538, y: -446.911}, + {id: 228, label: 'Fidel Martínez', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Tijuana', value: 22, group: 4, x: -1762.2454, y: -617.66486}, + {id: 229, label: 'Francisco Javier Rodríguez', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'América', value: 22, group: 21, x: -2058.6445, y: 342.12747}, + {id: 230, label: 'Francisco Silva', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Osasuna', value: 22, group: 18, x: -207.91714, y: 1451.4407}, + {id: 231, label: 'Frank Lampard', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Chelsea', value: 32, group: 28, x: -247.65233, y: -855.8526}, + {id: 232, label: 'Fraser Forster', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Celtic', value: 25, group: 28, x: 12.960639, y: -928.6838}, + {id: 233, label: 'Fred', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Fluminense', value: 22, group: 23, x: -513.3818, y: -260.2743}, + {id: 234, label: 'Fredy Guarín', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'Internazionale', value: 29, group: 11, x: -787.85443, y: 1018.71765}, + {id: 235, label: 'Frickson Erazo', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Flamengo', value: 22, group: 4, x: -1740.3123, y: -668.11096}, + {id: 236, label: 'Gabriel Achilier', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Emelec', value: 22, group: 4, x: -1682.2622, y: -719.3627}, + {id: 237, label: 'Gabriel Paletta', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Parma', value: 24, group: 3, x: 206.93822, y: 845.00073}, + {id: 238, label: 'Gary Cahill', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Chelsea', value: 32, group: 28, x: -301.79718, y: -918.2849}, + {id: 239, label: 'Gary Medel', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Cardiff City', value: 23, group: 18, x: -135.52126, y: 1534.2073}, + {id: 240, label: 'Gastón Ramírez', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Southampton', value: 28, group: 6, x: -52.539005, y: -56.373035}, + {id: 241, label: 'Gelson Fernandes', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'SC Freiburg', value: 24, group: 0, x: 151.71802, y: 158.9506}, + {id: 242, label: 'Geoff Cameron', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'Stoke City', value: 25, group: 26, x: 820.3439, y: -1464.1147}, + {id: 243, label: 'Georgi Shchennikov', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'CSKA Moscow', value: 23, group: 2, x: -1330.4204, y: -1544.3962}, + {id: 244, label: 'Georginio Wijnaldum', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'PSV', value: 24, group: 22, x: 874.0655, y: 135.79485}, + {id: 245, label: 'Gerard Piqué', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Barcelona', value: 31, group: 23, x: -1126.4338, y: -326.65405}, + {id: 246, label: 'Gervinho', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Roma', value: 26, group: 9, x: 560.3703, y: -680.46234}, + {id: 247, label: 'Ghasem Haddadifar', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Zob Ahan', value: 22, group: 1, x: 1942.6196, y: 1184.3281}, + {id: 248, label: 'Giancarlo González', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Columbus Crew', value: 22, group: 29, x: 2265.3667, y: 299.92572}, + {id: 249, label: 'Gianluigi Buffon (c)', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Juventus', value: 28, group: 3, x: 152.25356, y: 824.18774}, + {id: 250, label: 'Giannis Fetfatzidis', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'Genoa', value: 24, group: 15, x: 1469.2073, y: 587.92706}, + {id: 251, label: 'Giannis Maniatis', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'Olympiacos', value: 23, group: 15, x: 1675.6614, y: 562.7533}, + {id: 252, label: 'Giorgio Chiellini', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Juventus', value: 28, group: 3, x: 168.9661, y: 898.16156}, + {id: 253, label: 'Giorgos Karagounis (c)', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'Fulham', value: 23, group: 15, x: 1659.2035, y: 651.7564}, + {id: 254, label: 'Giorgos Samaras', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'Celtic', value: 25, group: 15, x: 1468.0847, y: 290.17197}, + {id: 255, label: 'Giorgos Tzavellas', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'PAOK', value: 22, group: 15, x: 1582.3857, y: 615.66473}, + {id: 256, label: 'Giovani dos Santos', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'Villarreal', value: 22, group: 21, x: -2058.4065, y: 426.69418}, + {id: 257, label: 'Giovanni Sio', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Basel', value: 26, group: 9, x: 405.23972, y: -662.28076}, + {id: 258, label: 'Glen Johnson', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Liverpool', value: 27, group: 28, x: -77.03864, y: -917.1485}, + {id: 259, label: 'Godfrey Oboabona', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Çaykur Rizespor', value: 23, group: 14, x: 9.590389, y: -1597.5946}, + {id: 260, label: 'Gökhan Inler (c)', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'Napoli', value: 31, group: 0, x: -228.73499, y: 213.29607}, + {id: 261, label: 'Gonzalo Higuaín', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Napoli', value: 32, group: 19, x: -976.805, y: 255.482}, + {id: 262, label: 'Gonzalo Jara', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Nottingham Forest', value: 22, group: 18, x: -235.43576, y: 1571.7034}, + {id: 263, label: 'Gordon Schildenfeld', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Panathinaikos', value: 23, group: 25, x: -217.73817, y: 655.73315}, + {id: 264, label: 'Gotoku Sakai', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'VfB Stuttgart', value: 25, group: 27, x: 626.25525, y: 448.10638}, + {id: 265, label: 'Graham Zusi', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'Sporting Kansas City', value: 22, group: 26, x: 821.1794, y: -1568.8907}, + {id: 266, label: 'Granit Xhaka', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'Borussia Mönchengladbach', value: 23, group: 0, x: 60.45976, y: 205.48042}, + {id: 267, label: 'Guillermo Ochoa', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'Ajaccio', value: 23, group: 21, x: -2012.4979, y: 495.58713}, + {id: 268, label: 'Ha Dae-sung', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Beijing Guoan', value: 22, group: 10, x: 1235.4569, y: 1551.8241}, + {id: 269, label: 'Han Kook-young', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Kashiwa Reysol', value: 22, group: 10, x: 1158.8308, y: 1599.3705}, + {id: 270, label: 'Haris Medunjanin', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'Gaziantepspor', value: 22, group: 20, x: 1200.2539, y: -418.55362}, + {id: 271, label: 'Haris Seferovic', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'Real Sociedad', value: 25, group: 0, x: 141.21535, y: 262.27655}, + {id: 272, label: 'Harrison Afful', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Espérance', value: 22, group: 5, x: 468.08853, y: 1387.6926}, + {id: 273, label: 'Hashem Beikzadeh', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Esteghlal', value: 22, group: 1, x: 1986.3362, y: 1189.6459}, + {id: 274, label: 'Hassan Yebda', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Udinese', value: 23, group: 24, x: -1303.4868, y: 1254.4517}, + {id: 275, label: 'Héctor Herrera', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'Porto', value: 29, group: 21, x: -1799.6183, y: 372.85077}, + {id: 276, label: 'Héctor Moreno', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'Espanyol', value: 23, group: 21, x: -1943.8708, y: 364.62497}, + {id: 277, label: 'Hélder Postiga', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Lazio', value: 28, group: 8, x: -469.8896, y: 192.226}, + {id: 278, label: 'Henri Bedimo', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Lyon', value: 22, group: 17, x: 380.54697, y: 174.65756}, + {id: 279, label: 'Henrique', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Napoli', value: 33, group: 23, x: -572.6227, y: -84.16057}, + {id: 280, label: 'Hernanes', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Internazionale', value: 29, group: 23, x: -528.0018, y: -15.909561}, + {id: 281, label: 'Hiroki Sakai', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'Hannover 96', value: 24, group: 27, x: 714.5649, y: 462.32593}, + {id: 282, label: 'Hiroshi Kiyotake', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + '1. FC Nürnberg', value: 24, group: 27, x: 729.62537, y: 516.7272}, + {id: 283, label: 'Hong Jeong-ho', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'FC Augsburg', value: 23, group: 10, x: 1189.0176, y: 1491.9882}, + {id: 284, label: 'Hossein Mahini', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Persepolis', value: 22, group: 1, x: 1969.5181, y: 1144.5435}, + {id: 285, label: 'Hotaru Yamaguchi', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'Cerezo Osaka', value: 23, group: 27, x: 665.15576, y: 571.1557}, + {id: 286, label: 'Hugo Almeida', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Be?ikta?', value: 24, group: 8, x: -570.7293, y: 230.924}, + {id: 287, label: 'Hugo Campagnaro', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Internazionale', value: 27, group: 19, x: -1030.6344, y: 363.07056}, + {id: 288, label: 'Hugo Lloris (c)', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Tottenham Hotspur', value: 27, group: 16, x: -181.9427, y: -259.68008}, + {id: 289, label: 'Hulk', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Zenit Saint Petersburg', value: 29, group: 23, x: -676.12946, y: -547.05255}, + {id: 290, label: 'Hwang Seok-ho', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Sanfrecce Hiroshima', value: 23, group: 10, x: 1138.2103, y: 1544.5535}, + {id: 291, label: 'Ignazio Abate', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Milan', value: 27, group: 3, x: 229.40173, y: 946.202}, + {id: 292, label: 'Igor Akinfeev', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'CSKA Moscow', value: 23, group: 2, x: -1278.871, y: -1521.6796}, + {id: 293, label: 'Igor Denisov', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'Dynamo Moscow', value: 23, group: 2, x: -1478.4519, y: -1427.1252}, + {id: 294, label: 'Iker Casillas (c)', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Real Madrid', value: 31, group: 23, x: -800.62396, y: -169.28741}, + {id: 295, label: 'Isaác Brizuela', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'Toluca', value: 22, group: 21, x: -2104.4573, y: 342.27985}, + {id: 296, label: 'Islam Slimani', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Sporting CP', value: 25, group: 24, x: -1357.2412, y: 1056.6638}, + {id: 297, label: 'Ismaël Diomandé', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Saint-Étienne', value: 23, group: 9, x: 445.33255, y: -874.95105}, + {id: 298, label: 'Ivan Franjic', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Brisbane Roar', value: 22, group: 12, x: 2090.495, y: -571.4816}, + {id: 299, label: 'Ivan Perišic', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'VfL Wolfsburg', value: 27, group: 25, x: -294.81628, y: 494.7712}, + {id: 300, label: 'Ivan Rakitic', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Sevilla', value: 25, group: 25, x: -359.27826, y: 645.7861}, + {id: 301, label: 'Ivica Olic', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'VfL Wolfsburg', value: 27, group: 25, x: -356.225, y: 503.76892}, + {id: 302, label: 'Izet Hajrovic', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'Galatasaray', value: 26, group: 20, x: 1073.4325, y: -468.65955}, + {id: 303, label: 'Jack Wilshere', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Arsenal', value: 30, group: 28, x: -130.01361, y: -811.2897}, + {id: 304, label: 'Jackson Martínez', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'Porto', value: 29, group: 11, x: -870.14624, y: 947.02435}, + {id: 305, label: 'Jaime Ayoví', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Tijuana', value: 22, group: 4, x: -1695.5747, y: -675.85455}, + {id: 306, label: 'Jalal Hosseini', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Persepolis', value: 22, group: 1, x: 2076.0352, y: 1075.6108}, + {id: 307, label: 'James Holland', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Austria Wien', value: 22, group: 12, x: 2105.7495, y: -645.33295}, + {id: 308, label: 'James Milner', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Manchester City', value: 30, group: 28, x: -219.63795, y: -778.5797}, + {id: 309, label: 'James Rodríguez', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'AS Monaco', value: 25, group: 11, x: -798.6743, y: 1094.4689}, + {id: 310, label: 'James Troisi', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Melbourne Victory', value: 22, group: 12, x: 2041.5525, y: -703.14703}, + {id: 311, label: 'Jan Vertonghen', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Tottenham Hotspur', value: 25, group: 28, x: -726.46454, y: -735.5794}, + {id: 312, label: 'Jasmin Fejzic', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'VfR Aalen', value: 22, group: 20, x: 1170.3435, y: -544.8657}, + {id: 313, label: 'Jason Davidson', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Heracles Almelo', value: 22, group: 12, x: 2027.0093, y: -621.23444}, + {id: 314, label: 'Jasper Cillessen', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Ajax', value: 22, group: 22, x: 884.7674, y: 31.967285}, + {id: 315, label: 'Javad Nekounam (c)', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Al-Kuwait', value: 22, group: 1, x: 1956.9619, y: 1077.9049}, + {id: 316, label: 'Javi Martínez', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Bayern Munich', value: 35, group: 23, x: -549.74335, y: -388.08502}, + {id: 317, label: 'Javier Aquino', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'Villarreal', value: 22, group: 21, x: -2081.5557, y: 384.58026}, + {id: 318, label: 'Javier Hernández', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'Manchester United', value: 35, group: 21, x: -1606.5636, y: 123.67082}, + {id: 319, label: 'Javier Mascherano', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Barcelona', value: 36, group: 19, x: -1221.5325, y: 91.23916}, + {id: 320, label: 'Jean Beausejour', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Wigan Athletic', value: 24, group: 18, x: -67.39274, y: 1286.5491}, + {id: 321, label: 'Jean Makoun', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Rennes', value: 23, group: 17, x: 430.8337, y: 257.74985}, + {id: 322, label: 'Jean-Daniel Akpa-Akpro', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Toulouse', value: 23, group: 9, x: 413.52197, y: -756.9924}, + {id: 323, label: 'Jefferson', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Botafogo', value: 22, group: 23, x: -426.49158, y: -267.58475}, + {id: 324, label: 'Jefferson Montero', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Morelia', value: 23, group: 4, x: -1599.2291, y: -622.97186}, + {id: 325, label: 'Jeremain Lens', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Dynamo Kyiv', value: 25, group: 22, x: 718.188, y: 97.2607}, + {id: 326, label: 'Jermaine Jones', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'Be?ikta?', value: 24, group: 26, x: 734.773, y: -1356.2697}, + {id: 327, label: 'Jérôme Boateng', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Bayern Munich', value: 29, group: 13, x: 313.90338, y: -414.42447}, + {id: 328, label: 'Jerry Bengtson', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'New England Revolution', value: 22, group: 7, x: 1590.5161, y: -1207.1145}, + {id: 329, label: 'Jerry Palacios', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Alajuelense', value: 24, group: 7, x: 1713.397, y: -1049.3608}, + {id: 330, label: 'Ji Dong-won', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'FC Augsburg', value: 23, group: 10, x: 1240.8452, y: 1492.1494}, + {id: 331, label: 'Jô', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Atlético Mineiro', value: 22, group: 23, x: -470.48615, y: -271.38748}, + {id: 332, label: 'João Moutinho', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'AS Monaco', value: 25, group: 8, x: -709.12415, y: 410.8603}, + {id: 333, label: 'João Pereira', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Valencia', value: 25, group: 8, x: -649.96454, y: 448.82736}, + {id: 334, label: 'Joao Rojas', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Cruz Azul', value: 24, group: 4, x: -1776.6962, y: -531.8545}, + {id: 335, label: 'Joe Hart', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Manchester City', value: 30, group: 28, x: -212.69391, y: -704.6478}, + {id: 336, label: 'Joel Campbell', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Olympiacos', value: 26, group: 29, x: 2111.164, y: 365.17755}, + {id: 337, label: 'Joël Matip', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Schalke 04', value: 28, group: 17, x: 540.77966, y: 139.58159}, + {id: 338, label: 'Joël Veltman', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Ajax', value: 22, group: 22, x: 921.6833, y: 59.578938}, + {id: 339, label: 'Johan Djourou', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'Hamburger SV', value: 23, group: 0, x: 57.06974, y: 323.02927}, + {id: 340, label: 'John Boye', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Rennes', value: 23, group: 5, x: 493.59833, y: 1298.41}, + {id: 341, label: 'John Brooks', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'Hertha BSC', value: 23, group: 26, x: 729.48096, y: -1409.5938}, + {id: 342, label: 'John Obi Mikel', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Chelsea', value: 33, group: 14, x: -197.90224, y: -1324.3247}, + {id: 343, label: 'Johnny Acosta', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Alajuelense', value: 23, group: 29, x: 2202.928, y: 222.98761}, + {id: 344, label: 'Johnny Herrera', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Universidad de Chile', value: 22, group: 18, x: -225.40228, y: 1509.603}, + {id: 345, label: 'Jonathan de Guzmán', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Swansea City', value: 23, group: 22, x: 917.89813, y: -45.654217}, + {id: 346, label: 'Jonathan Mensah', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Évian', value: 22, group: 5, x: 461.7189, y: 1342.4531}, + {id: 347, label: 'Jordan Ayew', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Sochaux', value: 22, group: 5, x: 418.20883, y: 1351.9128}, + {id: 348, label: 'Jordan Henderson', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Liverpool', value: 27, group: 28, x: -137.00108, y: -918.78546}, + {id: 349, label: 'Jordi Alba', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Barcelona', value: 31, group: 23, x: -1139.679, y: -237.86505}, + {id: 350, label: 'Jordy Clasie', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Feyenoord', value: 22, group: 22, x: 920.4804, y: 7.368482}, + {id: 351, label: 'Jorge Claros', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Motagua', value: 22, group: 7, x: 1693.2894, y: -1172.8019}, + {id: 352, label: 'Jorge Fucile', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Porto', value: 30, group: 6, x: -342.37836, y: 145.54729}, + {id: 353, label: 'Jorge Guagua', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Emelec', value: 22, group: 4, x: -1678.4408, y: -602.871}, + {id: 354, label: 'Jorge Valdivia', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Palmeiras', value: 22, group: 18, x: -250.0152, y: 1428.506}, + {id: 355, label: 'José de Jesús Corona', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'Cruz Azul', value: 23, group: 21, x: -2099.094, y: 287.12247}, + {id: 356, label: 'José Holebas', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'Olympiacos', value: 23, group: 15, x: 1657.0046, y: 513.2496}, + {id: 357, label: 'José Juan Vázquez', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'León', value: 22, group: 21, x: -2102.5596, y: 434.67215}, + {id: 358, label: 'José María Basanta', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Monterrey', value: 22, group: 19, x: -1144.7311, y: 286.0747}, + {id: 359, label: 'José María Giménez', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Atlético Madrid', value: 28, group: 6, x: -198.00406, y: -82.70489}, + {id: 360, label: 'José Miguel Cubero', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Herediano', value: 22, group: 29, x: 2268.5837, y: 346.56885}, + {id: 361, label: 'José Pedro Fuenzalida', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Colo-Colo', value: 22, group: 18, x: -198.39777, y: 1545.6372}, + {id: 362, label: 'José Rojas', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Universidad de Chile', value: 22, group: 18, x: -307.82147, y: 1544.147}, + {id: 363, label: 'Joseph Yobo (c)', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Norwich City', value: 23, group: 14, x: 3.3988526, y: -1540.3546}, + {id: 364, label: 'Josip Drmic', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + '1. FC Nürnberg', value: 25, group: 0, x: 179.9546, y: 206.55292}, + {id: 365, label: 'Jozy Altidore', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'Sunderland', value: 23, group: 26, x: 866.4315, y: -1353.6399}, + {id: 366, label: 'Juan Camilo Zúñiga', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'Napoli', value: 33, group: 11, x: -759.6773, y: 893.11926}, + {id: 367, label: 'Juan Carlos García', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Wigan Athletic', value: 23, group: 7, x: 1576.5138, y: -1044.397}, + {id: 368, label: 'Juan Carlos Paredes', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Barcelona', value: 35, group: 4, x: -1452.1322, y: -446.39807}, + {id: 369, label: 'Juan Fernando Quintero', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'Porto', value: 29, group: 11, x: -908.9095, y: 1006.1945}, + {id: 370, label: 'Juan Guillermo Cuadrado', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'Fiorentina', value: 24, group: 11, x: -683.1348, y: 1184.008}, + {id: 371, label: 'Juan Mata', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Manchester United', value: 34, group: 23, x: -837.1373, y: -428.5978}, + {id: 372, label: 'Juan Pablo Montes', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Motagua', value: 22, group: 7, x: 1592.682, y: -1250.384}, + {id: 373, label: 'Juanfran', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Atlético Madrid', value: 27, group: 23, x: -888.2895, y: -365.17215}, + {id: 374, label: 'Julian Draxler', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Schalke 04', value: 27, group: 13, x: 528.5164, y: -263.55563}, + {id: 375, label: 'Julian Green', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'Bayern Munich', value: 35, group: 26, x: 627.9602, y: -1176.4528}, + {id: 376, label: 'Júlio César', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Toronto FC', value: 23, group: 23, x: -374.46234, y: -336.27332}, + {id: 377, label: 'Jung Sung-ryong', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Suwon Bluewings', value: 22, group: 10, x: 1253.4236, y: 1593.7097}, + {id: 378, label: 'Júnior Díaz', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Mainz 05', value: 26, group: 29, x: 2052.3333, y: 457.91708}, + {id: 379, label: 'Juwon Oshaniwa', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Ashdod', value: 22, group: 14, x: -3.9951146, y: -1656.1483}, + {id: 380, label: 'Karim Ansarifard', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Tractor Sazi', value: 22, group: 1, x: 2030.3977, y: 1187.764}, + {id: 381, label: 'Karim Benzema', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Real Madrid', value: 32, group: 16, x: -255.21576, y: -165.30316}, + {id: 382, label: 'Keisuke Honda', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'Milan', value: 29, group: 27, x: 610.39655, y: 750.20026}, + {id: 383, label: 'Kenneth Omeruo', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Middlesbrough', value: 23, group: 14, x: -33.32675, y: -1484.3856}, + {id: 384, label: 'Kevin De Bruyne', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'VfL Wolfsburg', value: 28, group: 28, x: -581.4455, y: -583.9621}, + {id: 385, label: 'Kevin Großkreutz', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Borussia Dortmund', value: 24, group: 13, x: 553.73175, y: -380.0992}, + {id: 386, label: 'Kevin Mirallas', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Everton', value: 26, group: 28, x: -563.9285, y: -964.3166}, + {id: 387, label: 'Kevin-Prince Boateng', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Schalke 04', value: 28, group: 5, x: 528.2719, y: 1086.7677}, + {id: 388, label: 'Keylor Navas', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Levante', value: 23, group: 29, x: 2179.6377, y: 330.61267}, + {id: 389, label: 'Khosro Heydari', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Esteghlal', value: 22, group: 1, x: 2085.2766, y: 1118.5546}, + {id: 390, label: 'Ki Sung-yueng', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Sunderland', value: 23, group: 10, x: 1168.5514, y: 1424.8241}, + {id: 391, label: 'Kim Bo-kyung', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Cardiff City', value: 23, group: 10, x: 1094.6575, y: 1613.0087}, + {id: 392, label: 'Kim Chang-soo', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Kashiwa Reysol', value: 22, group: 10, x: 1182.648, y: 1681.8923}, + {id: 393, label: 'Kim Seung-gyu', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Ulsan Hyundai', value: 22, group: 10, x: 1189.8958, y: 1559.8545}, + {id: 394, label: 'Kim Shin-wook', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Ulsan Hyundai', value: 22, group: 10, x: 1231.2048, y: 1679.3086}, + {id: 395, label: 'Kim Young-gwon', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Guangzhou Evergrande', value: 22, group: 10, x: 1284.3221, y: 1556.8948}, + {id: 396, label: 'Klaas-Jan Huntelaar', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Schalke 04', value: 28, group: 22, x: 809.16656, y: 91.84488}, + {id: 397, label: 'Koke', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Atlético Madrid', value: 27, group: 23, x: -921.22095, y: -304.28424}, + {id: 398, label: 'Kolo Touré', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Liverpool', value: 31, group: 9, x: 297.04135, y: -918.4601}, + {id: 399, label: 'Koo Ja-cheol (c)', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Mainz 05', value: 25, group: 10, x: 1210.03, y: 1383.6355}, + {id: 400, label: 'Kostas Katsouranis', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'PAOK', value: 22, group: 15, x: 1625.112, y: 590.2659}, + {id: 401, label: 'Kostas Manolas', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'Olympiacos', value: 23, group: 15, x: 1643.8208, y: 458.0363}, + {id: 402, label: 'Kostas Mitroglou', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'Fulham', value: 23, group: 15, x: 1704.107, y: 623.1121}, + {id: 403, label: 'Kunle Odunlami', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Sunshine Stars', value: 22, group: 14, x: -51.509785, y: -1656.867}, + {id: 404, label: 'Kwadwo Asamoah', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Juventus', value: 33, group: 5, x: 285.16757, y: 1193.1697}, + {id: 405, label: 'Kwak Tae-hwi', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Al-Hilal', value: 22, group: 10, x: 1276.5813, y: 1652.845}, + {id: 406, label: 'Kyle Beckerman', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'Real Salt Lake', value: 22, group: 26, x: 814.4154, y: -1616.4198}, + {id: 407, label: 'Landry N Guémo', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Bordeaux', value: 22, group: 17, x: 380.33423, y: 127.532715}, + {id: 408, label: 'Laurent Ciman', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Standard Liège', value: 24, group: 28, x: -542.0193, y: -660.84076}, + {id: 409, label: 'Laurent Koscielny', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Arsenal', value: 29, group: 16, x: -15.0555935, y: -387.5162}, + {id: 410, label: 'Lazaros Christodoulopoulos', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'Bologna', value: 23, group: 15, x: 1501.5779, y: 504.68384}, + {id: 411, label: 'Lee Bum-young', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Busan IPark', value: 22, group: 10, x: 1190.9927, y: 1637.5756}, + {id: 412, label: 'Lee Chung-yong', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Bolton Wanderers', value: 22, group: 10, x: 1146.0409, y: 1647.9602}, + {id: 413, label: 'Lee Keun-ho', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Sangju Sangmu', value: 22, group: 10, x: 1296.3544, y: 1607.5996}, + {id: 414, label: 'Lee Yong', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Ulsan Hyundai', value: 22, group: 10, x: 1208.6063, y: 1598.109}, + {id: 415, label: 'Leighton Baines', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Everton', value: 25, group: 28, x: -237.56212, y: -998.078}, + {id: 416, label: 'Leonardo Bonucci', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Juventus', value: 28, group: 3, x: 125.05671, y: 766.19403}, + {id: 417, label: 'Leroy Fer', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Norwich City', value: 23, group: 22, x: 837.3325, y: -102.88975}, + {id: 418, label: 'Liassine Cadamuro-Bentaïba', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Mallorca', value: 22, group: 24, x: -1424.9585, y: 1185.58}, + {id: 419, label: 'Lionel Messi (c)', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Barcelona', value: 36, group: 19, x: -1133.2008, y: 55.981808}, + {id: 420, label: 'Loïc Feudjou', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Coton Sport', value: 22, group: 17, x: 464.74194, y: 157.333}, + {id: 421, label: 'Loïc Rémy', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Newcastle United', value: 25, group: 16, x: 73.68377, y: -313.17633}, + {id: 422, label: 'Lorenzo Insigne', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Napoli', value: 33, group: 3, x: -68.64961, y: 680.98474}, + {id: 423, label: 'Loukas Vyntra', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'Levante', value: 23, group: 15, x: 1712.4525, y: 526.83075}, + {id: 424, label: 'Lucas Biglia', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Lazio', value: 28, group: 19, x: -845.6186, y: 161.40001}, + {id: 425, label: 'Lucas Digne', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Paris Saint-Germain', value: 29, group: 16, x: -18.416775, y: -111.03686}, + {id: 426, label: 'Luis Garrido', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Olimpia', value: 22, group: 7, x: 1665.7246, y: -1263.9408}, + {id: 427, label: 'Luis López', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Real España', value: 22, group: 7, x: 1610.1837, y: -1129.5691}, + {id: 428, label: 'Luís Neto', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Zenit Saint Petersburg', value: 29, group: 8, x: -787.05585, y: -14.597502}, + {id: 429, label: 'Luis Saritama', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Barcelona', value: 35, group: 4, x: -1546.8987, y: -441.0774}, + {id: 430, label: 'Luis Suárez', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Liverpool', value: 31, group: 6, x: -100.21393, y: -246.37468}, + {id: 431, label: 'Luiz Gustavo', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'VfL Wolfsburg', value: 28, group: 23, x: -456.7165, y: -142.2136}, + {id: 432, label: 'Luka Modric', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Real Madrid', value: 33, group: 25, x: -410.41797, y: 416.6111}, + {id: 433, label: 'Lukas Podolski', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Arsenal', value: 29, group: 13, x: 202.08969, y: -446.2755}, + {id: 434, label: 'Luke Shaw', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Southampton', value: 26, group: 28, x: -92.229225, y: -688.88574}, + {id: 435, label: 'Madjid Bougherra (c)', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Lekhwiya', value: 22, group: 24, x: -1470.3363, y: 1180.3844}, + {id: 436, label: 'Maicon', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Roma', value: 26, group: 23, x: -278.08972, y: -249.45703}, + {id: 437, label: 'Majeed Waris', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Valenciennes', value: 23, group: 5, x: 324.35605, y: 1417.5355}, + {id: 438, label: 'Makoto Hasebe (c)', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + '1. FC Nürnberg', value: 24, group: 27, x: 672.80505, y: 505.12762}, + {id: 439, label: 'Maksim Kanunnikov', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'Rubin Kazan', value: 23, group: 2, x: -1315.3818, y: -1323.4706}, + {id: 440, label: 'Mamadou Sakho', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Liverpool', value: 31, group: 16, x: -55.2884, y: -503.5874}, + {id: 441, label: 'Manabu Saito', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'Yokohama F. Marinos', value: 22, group: 27, x: 700.693, y: 617.4117}, + {id: 442, label: 'Manuel Neuer', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Bayern Munich', value: 29, group: 13, x: 362.29532, y: -299.95224}, + {id: 443, label: 'Marcelo', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Real Madrid', value: 33, group: 23, x: -546.0523, y: -181.72266}, + {id: 444, label: 'Marcelo Brozovic', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Dinamo Zagreb', value: 23, group: 25, x: -406.19418, y: 695.72943}, + {id: 445, label: 'Marcelo Díaz', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Basel', value: 27, group: 18, x: -193.87224, y: 1188.147}, + {id: 446, label: 'Marco Fabián', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'Cruz Azul', value: 23, group: 21, x: -2042.7997, y: 288.54993}, + {id: 447, label: 'Marco Parolo', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Parma', value: 24, group: 3, x: 223.34402, y: 798.16846}, + {id: 448, label: 'Marco Ureña', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Kuban Krasnodar', value: 23, group: 29, x: 2171.2605, y: 406.7075}, + {id: 449, label: 'Marco Verratti', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Paris Saint-Germain', value: 29, group: 3, x: 74.62252, y: 597.4002}, + {id: 450, label: 'Marcos Rojo', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Sporting CP', value: 25, group: 19, x: -1169.2754, y: 359.3405}, + {id: 451, label: 'Mariano Andújar', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Catania', value: 22, group: 19, x: -1186.1453, y: 246.04404}, + {id: 452, label: 'Mario Balotelli', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Milan', value: 27, group: 3, x: 253.78076, y: 886.26984}, + {id: 453, label: 'Mario Gavranovic', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'Zürich', value: 22, group: 0, x: 52.757668, y: 247.96585}, + {id: 454, label: 'Mario Götze', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Bayern Munich', value: 29, group: 13, x: 301.41776, y: -338.43552}, + {id: 455, label: 'Mario Mandžukic', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Bayern Munich', value: 35, group: 25, x: -149.6339, y: 325.6033}, + {id: 456, label: 'Mario Martínez', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Real España', value: 22, group: 7, x: 1689.1534, y: -1223.153}, + {id: 457, label: 'Mario Yepes (c)', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'Atalanta', value: 23, group: 11, x: -719.321, y: 1256.8893}, + {id: 458, label: 'Mark Bresciano', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Al-Gharafa', value: 22, group: 12, x: 2122.0056, y: -604.5107}, + {id: 459, label: 'Mark Milligan', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Melbourne Victory', value: 22, group: 12, x: 2173.8164, y: -588.3221}, + {id: 460, label: 'Marouane Fellaini', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Manchester United', value: 34, group: 28, x: -655.5912, y: -756.77374}, + {id: 461, label: 'Martín Cáceres', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Juventus', value: 33, group: 6, x: -21.211044, y: 343.79504}, + {id: 462, label: 'Martín Demichelis', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Manchester City', value: 29, group: 19, x: -893.08545, y: 82.947815}, + {id: 463, label: 'Martín Silva', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Vasco da Gama', value: 22, group: 6, x: -0.6348668, y: 1.9825428}, + {id: 464, label: 'Marvin Chávez', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Chivas USA', value: 23, group: 7, x: 1429.7988, y: -1179.9895}, + {id: 465, label: 'Masahiko Inoha', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'Jubilo Iwata', value: 22, group: 27, x: 730.9411, y: 583.1111}, + {id: 466, label: 'Masato Morishige', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'F.C. Tokyo', value: 22, group: 27, x: 677.74445, y: 664.5135}, + {id: 467, label: 'Masoud Shojaei', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Las Palmas', value: 22, group: 1, x: 2059.2344, y: 1154.0554}, + {id: 468, label: 'Massimo Luongo', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Swindon Town', value: 22, group: 12, x: 2135.3752, y: -676.93585}, + {id: 469, label: 'Mateo Kovacic', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Internazionale', value: 29, group: 25, x: -492.02667, y: 654.4242}, + {id: 470, label: 'Mathew Leckie', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'FSV Frankfurt', value: 22, group: 12, x: 2138.549, y: -562.8361}, + {id: 471, label: 'Mathew Ryan', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Club Brugge', value: 23, group: 12, x: 2056.7805, y: -519.5844}, + {id: 472, label: 'Mathieu Debuchy', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Newcastle United', value: 25, group: 16, x: 14.882936, y: -313.20358}, + {id: 473, label: 'Mathieu Valbuena', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Marseille', value: 24, group: 16, x: 44.39426, y: -119.345985}, + {id: 474, label: 'Mathis Bolly', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Fortuna Düsseldorf', value: 23, group: 9, x: 651.62463, y: -893.97076}, + {id: 475, label: 'Mats Hummels', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Borussia Dortmund', value: 24, group: 13, x: 607.8975, y: -421.7086}, + {id: 476, label: 'Matt Besler', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'Sporting Kansas City', value: 22, group: 26, x: 861.9521, y: -1604.1628}, + {id: 477, label: 'Matt McKay', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Brisbane Roar', value: 22, group: 12, x: 2090.5696, y: -687.9733}, + {id: 478, label: 'Matteo Darmian', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Torino', value: 23, group: 3, x: 332.64136, y: 846.05145}, + {id: 479, label: 'Matthew Špiranovic', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Western Sydney Wanderers', value: 22, group: 12, x: 2061.1667, y: -656.2603}, + {id: 480, label: 'Matthias Ginter', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'SC Freiburg', value: 25, group: 13, x: 444.28552, y: -312.17847}, + {id: 481, label: 'Mattia De Sciglio', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Milan', value: 27, group: 3, x: 304.86957, y: 920.4894}, + {id: 482, label: 'Mattia Perin', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Genoa', value: 24, group: 3, x: 272.21268, y: 763.70386}, + {id: 483, label: 'Mauricio Isla', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Juventus', value: 32, group: 18, x: -142.68803, y: 1330.8896}, + {id: 484, label: 'Mauricio Pinilla', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Cagliari', value: 23, group: 18, x: -356.0086, y: 1526.6892}, + {id: 485, label: 'Max Gradel', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Saint-Étienne', value: 23, group: 9, x: 486.36218, y: -849.3238}, + {id: 486, label: 'Maxi Pereira', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Benfica', value: 26, group: 6, x: -192.70482, y: 101.33695}, + {id: 487, label: 'Maxi Rodríguez', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Newells Old Boys', value: 22, group: 19, x: -1193.7656, y: 294.7356}, + {id: 488, label: 'Maxim Choupo-Moting', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Mainz 05', value: 26, group: 17, x: 590.2084, y: 305.84305}, + {id: 489, label: 'Máximo Banguera', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Barcelona', value: 35, group: 4, x: -1488.9634, y: -533.33093}, + {id: 490, label: 'Maxwell', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Paris Saint-Germain', value: 30, group: 23, x: -388.1638, y: -99.59259}, + {id: 491, label: 'Maya Yoshida', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'Southampton', value: 28, group: 27, x: 540.3439, y: 427.26245}, + {id: 492, label: 'Maynor Figueroa', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Hull City', value: 23, group: 7, x: 1528.1024, y: -1100.3427}, + {id: 493, label: 'Medhi Lacen', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Getafe', value: 23, group: 24, x: -1321.0677, y: 1173.3302}, + {id: 494, label: 'Mehdi Mostefa', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Ajaccio', value: 23, group: 24, x: -1480.4698, y: 1115.9075}, + {id: 495, label: 'Mehrdad Pouladi', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Persepolis', value: 22, group: 1, x: 1894.8638, y: 1109.2692}, + {id: 496, label: 'Memphis Depay', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'PSV', value: 24, group: 22, x: 929.35187, y: 119.25908}, + {id: 497, label: 'Mensur Mujdža', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'SC Freiburg', value: 25, group: 20, x: 1039.0459, y: -418.09897}, + {id: 498, label: 'Mesut Özil', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Arsenal', value: 29, group: 13, x: 266.21005, y: -466.70053}, + {id: 499, label: 'Michael Arroyo', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Atlante', value: 22, group: 4, x: -1730.8958, y: -727.36395}, + {id: 500, label: 'Michael Babatunde', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Volyn Lutsk', value: 22, group: 14, x: -143.01881, y: -1634.2734}, + {id: 501, label: 'Michael Barrantes', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Aalesund', value: 22, group: 29, x: 2300.9563, y: 256.13895}, + {id: 502, label: 'Michael Bradley', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'Toronto FC', value: 23, group: 26, x: 721.9479, y: -1477.4308}, + {id: 503, label: 'Michael Essien', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Milan', value: 28, group: 5, x: 376.83282, y: 1298.3724}, + {id: 504, label: 'Michael Lang', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'Grasshopper', value: 22, group: 0, x: 29.479486, y: 282.8444}, + {id: 505, label: 'Michael Uchebo', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Cercle Brugge', value: 22, group: 14, x: -95.68781, y: -1656.3585}, + {id: 506, label: 'Michael Umaña', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Saprissa', value: 22, group: 29, x: 2330.0725, y: 379.5474}, + {id: 507, label: 'Michel Vorm', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Swansea City', value: 23, group: 22, x: 868.7987, y: -56.305706}, + {id: 508, label: 'Mickaël Landreau', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Bastia', value: 22, group: 16, x: -46.63565, y: -207.56238}, + {id: 509, label: 'Miguel Ángel Ponce', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'Toluca', value: 22, group: 21, x: -2068.7258, y: 475.15393}, + {id: 510, label: 'Miguel Layún', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'América', value: 22, group: 21, x: -2150.149, y: 351.6338}, + {id: 511, label: 'Miguel Veloso', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Dynamo Kyiv', value: 25, group: 8, x: -552.1939, y: 364.91592}, + {id: 512, label: 'Miiko Albornoz', title: 'Country: ' + 'Chile' + '
' + 'Team: ' + 'Malmö FF', value: 22, group: 18, x: -282.78622, y: 1583.4946}, + {id: 513, label: 'Mikkel Diskerud', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'Rosenborg', value: 23, group: 26, x: 941.49945, y: -1436.3448}, + {id: 514, label: 'Milan Badelj', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Hamburger SV', value: 23, group: 25, x: -271.98166, y: 685.1374}, + {id: 515, label: 'Mile Jedinak (c)', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Crystal Palace', value: 22, group: 12, x: 2075.4526, y: -732.8337}, + {id: 516, label: 'Miralem Pjanic', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'Roma', value: 26, group: 20, x: 1103.221, y: -385.46555}, + {id: 517, label: 'Miroslav Klose', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Lazio', value: 28, group: 13, x: 293.14236, y: -267.2075}, + {id: 518, label: 'Mitchell Langerak', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Borussia Dortmund', value: 27, group: 12, x: 1759.8835, y: -484.94678}, + {id: 519, label: 'Mohamed Zemmamouche', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'USM Alger', value: 22, group: 24, x: -1405.2527, y: 1223.2103}, + {id: 520, label: 'Mohammed Rabiu', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Kuban Krasnodar', value: 23, group: 5, x: 577.33563, y: 1315.1465}, + {id: 521, label: 'Morgan Schneiderlin', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Southampton', value: 28, group: 16, x: 8.893564, y: -207.08623}, + {id: 522, label: 'Mousa Dembélé', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Tottenham Hotspur', value: 25, group: 28, x: -780.7014, y: -765.0794}, + {id: 523, label: 'Moussa Sissoko', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Newcastle United', value: 25, group: 16, x: 49.931614, y: -364.4847}, + {id: 524, label: 'Muhamed Bešic', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'Ferencváros', value: 22, group: 20, x: 1194.7092, y: -510.00156}, + {id: 525, label: 'Nabil Bentaleb', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Tottenham Hotspur', value: 27, group: 24, x: -1282.9584, y: 861.7018}, + {id: 526, label: 'Nabil Ghilas', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Porto', value: 30, group: 24, x: -1331.139, y: 965.7551}, + {id: 527, label: 'Nacer Chadli', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Tottenham Hotspur', value: 25, group: 28, x: -730.6295, y: -798.0246}, + {id: 528, label: 'Nani', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Manchester United', value: 35, group: 8, x: -646.50024, y: 40.378365}, + {id: 529, label: 'Neymar', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Barcelona', value: 36, group: 23, x: -688.3395, y: -195.97823}, + {id: 530, label: 'Nick Rimando', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'Real Salt Lake', value: 22, group: 26, x: 864.0869, y: -1556.7881}, + {id: 531, label: 'Nicolás Lodeiro', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Corinthians', value: 22, group: 6, x: -54.92223, y: 16.616009}, + {id: 532, label: 'Nicolas Lombaerts', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Zenit Saint Petersburg', value: 28, group: 28, x: -803.9264, y: -951.1398}, + {id: 533, label: 'Nicolas N Koulou', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Marseille', value: 24, group: 17, x: 368.89407, y: 227.7929}, + {id: 534, label: 'Nigel de Jong', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Milan', value: 29, group: 22, x: 764.12317, y: 266.0992}, + {id: 535, label: 'Nikica Jelavic', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Hull City', value: 23, group: 25, x: -197.7674, y: 532.7603}, + {id: 536, label: 'Noel Valladares (c)', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Olimpia', value: 22, group: 7, x: 1633.6897, y: -1230.4397}, + {id: 537, label: 'Ogenyi Onazi', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Lazio', value: 28, group: 14, x: -33.871628, y: -1294.2328}, + {id: 538, label: 'Ognjen Vranješ', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'Elaz??spor', value: 22, group: 20, x: 1242.7872, y: -442.58514}, + {id: 539, label: 'Ognjen Vukojevic', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Dynamo Kyiv', value: 24, group: 25, x: -265.94672, y: 620.2862}, + {id: 540, label: 'Oleg Shatov', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'Zenit Saint Petersburg', value: 26, group: 2, x: -1223.3152, y: -1368.6674}, + {id: 541, label: 'Oliver Bozanic', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Luzern', value: 22, group: 12, x: 2198.3757, y: -627.18024}, + {id: 542, label: 'Oliver Zelenika', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Lokomotiva', value: 22, group: 25, x: -310.13934, y: 653.3941}, + {id: 543, label: 'Olivier Giroud', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Arsenal', value: 29, group: 16, x: -51.68798, y: -320.77396}, + {id: 544, label: 'Omar Gonzalez', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'Los Angeles Galaxy', value: 22, group: 26, x: 770.25964, y: -1596.3325}, + {id: 545, label: 'Orestis Karnezis', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'Granada', value: 24, group: 15, x: 1393.8566, y: 576.5566}, + {id: 546, label: 'Oribe Peralta', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'Santos Laguna', value: 22, group: 21, x: -2123.5435, y: 394.2029}, + {id: 547, label: 'Oscar', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Chelsea', value: 30, group: 23, x: -364.28693, y: -412.46796}, + {id: 548, label: 'Óscar Bagüí', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Emelec', value: 22, group: 4, x: -1773.5126, y: -705.2896}, + {id: 549, label: 'Óscar Boniek García', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Houston Dynamo', value: 23, group: 7, x: 1554.0684, y: -1285.4417}, + {id: 550, label: 'Óscar Duarte', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Club Brugge', value: 23, group: 29, x: 2292.3699, y: 190.47668}, + {id: 551, label: 'Osman Chávez', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Qingdao Jonoon', value: 22, group: 7, x: 1657.8716, y: -1139.4136}, + {id: 552, label: 'Oswaldo Minda', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Chivas USA', value: 23, group: 4, x: -1549.4302, y: -719.534}, + {id: 553, label: 'Ousmane Viera', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Çaykur Rizespor', value: 23, group: 9, x: 474.08282, y: -965.51855}, + {id: 554, label: 'Pablo Armero', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'West Ham United', value: 22, group: 11, x: -854.2187, y: 1249.3016}, + {id: 555, label: 'Pablo Zabaleta', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Manchester City', value: 29, group: 19, x: -933.6388, y: 24.648056}, + {id: 556, label: 'Panagiotis Glykos', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'PAOK', value: 22, group: 15, x: 1575.4261, y: 522.7162}, + {id: 557, label: 'Panagiotis Kone', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'Bologna', value: 23, group: 15, x: 1535.2936, y: 466.857}, + {id: 558, label: 'Panagiotis Tachtsidis', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'Torino', value: 25, group: 15, x: 1428.6139, y: 635.1239}, + {id: 559, label: 'Park Chu-young', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Watford', value: 23, group: 10, x: 1047.7448, y: 1576.756}, + {id: 560, label: 'Park Jong-woo', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Guangzhou R&F', value: 22, group: 10, x: 1236.0852, y: 1634.4038}, + {id: 561, label: 'Park Joo-ho', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Mainz 05', value: 25, group: 10, x: 1252.9922, y: 1424.8129}, + {id: 562, label: 'Patrice Evra', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Manchester United', value: 35, group: 16, x: -226.57672, y: -327.5888}, + {id: 563, label: 'Patrick Pemberton', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Alajuelense', value: 23, group: 29, x: 2230.4392, y: 179.53189}, + {id: 564, label: 'Paul Aguilar', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'América', value: 22, group: 21, x: -2114.9287, y: 482.15585}, + {id: 565, label: 'Paul Pogba', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Juventus', value: 33, group: 16, x: 8.138252, y: 94.4195}, + {id: 566, label: 'Paul Verhaegh', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'FC Augsburg', value: 24, group: 22, x: 949.3831, y: 201.00778}, + {id: 567, label: 'Paulinho', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Tottenham Hotspur', value: 27, group: 23, x: -575.7446, y: -298.09418}, + {id: 568, label: 'Pavel Mogilevets', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'Rubin Kazan', value: 23, group: 2, x: -1357.9305, y: -1289.3833}, + {id: 569, label: 'Pedro', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Barcelona', value: 31, group: 23, x: -1064.4056, y: -381.13626}, + {id: 570, label: 'Pejman Montazeri', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Umm Salal', value: 22, group: 1, x: 2022.9941, y: 1015.42993}, + {id: 571, label: 'Pepe', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Real Madrid', value: 31, group: 8, x: -652.3342, y: 226.08397}, + {id: 572, label: 'Pepe Reina', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Napoli', value: 32, group: 23, x: -850.5622, y: -89.60556}, + {id: 573, label: 'Per Mertesacker', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Arsenal', value: 29, group: 13, x: 261.49197, y: -532.3377}, + {id: 574, label: 'Peter Odemwingie', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Stoke City', value: 25, group: 14, x: 110.87254, y: -1595.627}, + {id: 575, label: 'Phil Jagielka', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Everton', value: 25, group: 28, x: -210.36139, y: -1046.034}, + {id: 576, label: 'Phil Jones', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Manchester United', value: 32, group: 28, x: -300.32303, y: -774.0247}, + {id: 577, label: 'Philipp Lahm (c)', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Bayern Munich', value: 29, group: 13, x: 350.3983, y: -483.03665}, + {id: 578, label: 'Philippe Senderos', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'Valencia', value: 26, group: 0, x: -84.25211, y: 385.70135}, + {id: 579, label: 'Pierre Webó', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Fenerbahçe', value: 26, group: 17, x: 292.58267, y: 67.772385}, + {id: 580, label: 'Rafa Silva', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Braga', value: 22, group: 8, x: -692.3677, y: 355.65155}, + {id: 581, label: 'Rafael Márquez (c)', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'León', value: 22, group: 21, x: -2148.7192, y: 446.013}, + {id: 582, label: 'Rafik Halliche', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Académica', value: 22, group: 24, x: -1426.0991, y: 1266.2908}, + {id: 583, label: 'Raheem Sterling', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Liverpool', value: 27, group: 28, x: -93.51011, y: -985.4643}, + {id: 584, label: 'Rahman Ahmadi', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Sepahan', value: 22, group: 1, x: 2011.6289, y: 1143.9183}, + {id: 585, label: 'Raïs M Bolhi', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'CSKA Sofia', value: 22, group: 24, x: -1459.3608, y: 1229.282}, + {id: 586, label: 'Ramires', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Chelsea', value: 30, group: 23, x: -481.02625, y: -469.71396}, + {id: 587, label: 'Ramon Azeez', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Almería', value: 22, group: 14, x: -83.15391, y: -1703.9006}, + {id: 588, label: 'Randall Brenes', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Cartaginés', value: 22, group: 29, x: 2309.6873, y: 299.45453}, + {id: 589, label: 'Raphaël Varane', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Real Madrid', value: 32, group: 16, x: -176.20541, y: -169.91304}, + {id: 590, label: 'Rashid Sumaila', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Mamelodi Sundowns', value: 22, group: 5, x: 457.3916, y: 1442.739}, + {id: 591, label: 'Raúl Albiol', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Napoli', value: 32, group: 23, x: -934.9327, y: -101.35684}, + {id: 592, label: 'Raúl Jiménez', title: 'Country: ' + 'Mexico' + '
' + 'Team: ' + 'América', value: 22, group: 21, x: -2167.434, y: 400.85532}, + {id: 593, label: 'Raul Meireles', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Fenerbahçe', value: 25, group: 8, x: -515.2749, y: 255.22029}, + {id: 594, label: 'Rémy Cabella', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Montpellier', value: 22, group: 16, x: -28.49823, y: -252.28802}, + {id: 595, label: 'Renato Ibarra', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Vitesse', value: 23, group: 4, x: -1613.8063, y: -545.05145}, + {id: 596, label: 'Reto Ziegler', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'Sassuolo', value: 22, group: 0, x: 3.861307, y: 248.17929}, + {id: 597, label: 'Reuben Gabriel', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Waasland-Beveren', value: 22, group: 14, x: -132.04297, y: -1684.2073}, + {id: 598, label: 'Reza Ghoochannejhad', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Charlton Athletic', value: 22, group: 1, x: 2037.9062, y: 1109.297}, + {id: 599, label: 'Reza Haghighi', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Persepolis', value: 22, group: 1, x: 1912.5083, y: 1151.8527}, + {id: 600, label: 'Ricardo Álvarez', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Internazionale', value: 27, group: 19, x: -991.71326, y: 419.20453}, + {id: 601, label: 'Ricardo Costa', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Valencia', value: 25, group: 8, x: -699.53125, y: 481.92715}, + {id: 602, label: 'Ricardo Rodríguez', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'VfL Wolfsburg', value: 27, group: 0, x: -71.65908, y: 197.11438}, + {id: 603, label: 'Rickie Lambert', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Southampton', value: 26, group: 28, x: -64.72023, y: -747.43665}, + {id: 604, label: 'Rio Mavuba', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Lille', value: 25, group: 16, x: -65.83039, y: -421.9733}, + {id: 605, label: 'Riyad Mahrez', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Leicester City', value: 22, group: 24, x: -1375.4896, y: 1263.6211}, + {id: 606, label: 'Robin van Persie (c)', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Manchester United', value: 35, group: 22, x: 425.40573, y: -117.8186}, + {id: 607, label: 'Rodrigo Muñoz', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Libertad', value: 22, group: 6, x: -20.128693, y: 28.408825}, + {id: 608, label: 'Rodrigo Palacio', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Internazionale', value: 27, group: 19, x: -1056.1539, y: 433.82733}, + {id: 609, label: 'Roger Espinoza', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Wigan Athletic', value: 23, group: 7, x: 1525.8236, y: -1042.1475}, + {id: 610, label: 'Roman Bürki', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'Grasshopper', value: 22, group: 0, x: 84.8047, y: 279.10205}, + {id: 611, label: 'Roman Weidenfeller', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Borussia Dortmund', value: 24, group: 13, x: 605.1841, y: -360.4882}, + {id: 612, label: 'Romelu Lukaku', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Everton', value: 26, group: 28, x: -624.76385, y: -965.3788}, + {id: 613, label: 'Ron Vlaar', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Aston Villa', value: 23, group: 22, x: 922.5167, y: -99.8845}, + {id: 614, label: 'Ron-Robert Zieler', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Hannover 96', value: 24, group: 13, x: 479.21454, y: -376.45038}, + {id: 615, label: 'Rony Martínez', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Real Sociedad', value: 25, group: 7, x: 1436.8522, y: -978.24146}, + {id: 616, label: 'Ross Barkley', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Everton', value: 25, group: 28, x: -149.7628, y: -1043.2092}, + {id: 617, label: 'Roy Miller', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'New York Red Bulls', value: 23, group: 29, x: 2341.1836, y: 210.36285}, + {id: 618, label: 'Rúben Amorim', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Benfica', value: 25, group: 8, x: -743.5818, y: 322.5777}, + {id: 619, label: 'Rui Patrício', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Sporting CP', value: 24, group: 8, x: -770.5219, y: 432.82077}, + {id: 620, label: 'Ryan McGowan', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Shandong Luneng Taishan', value: 22, group: 12, x: 2185.5203, y: -671.7802}, + {id: 621, label: 'Salomon Kalou', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Lille', value: 25, group: 9, x: 392.33093, y: -927.2915}, + {id: 622, label: 'Salvatore Sirigu', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Paris Saint-Germain', value: 29, group: 3, x: 133.34747, y: 646.7461}, + {id: 623, label: 'Sami Khedira', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Real Madrid', value: 33, group: 13, x: 147.37221, y: -251.96838}, + {id: 624, label: 'Sammir', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Getafe', value: 23, group: 25, x: -386.4237, y: 741.6884}, + {id: 625, label: 'Sammy Bossut', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Zulte Waregem', value: 23, group: 28, x: -665.6252, y: -835.4098}, + {id: 626, label: 'Sammy N Djock', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Fethiyespor', value: 22, group: 17, x: 341.5248, y: 155.85918}, + {id: 627, label: 'Samuel Etoo (c)', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Chelsea', value: 33, group: 17, x: 207.89883, y: -77.141884}, + {id: 628, label: 'Samuel Inkoom', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Platanias', value: 22, group: 5, x: 406.61176, y: 1441.4194}, + {id: 629, label: 'Santi Cazorla', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Arsenal', value: 31, group: 23, x: -670.40643, y: -383.8588}, + {id: 630, label: 'Santiago Arias', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'PSV', value: 25, group: 11, x: -524.84265, y: 1069.8534}, + {id: 631, label: 'Saphir Taïder', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Internazionale', value: 29, group: 24, x: -1233.4976, y: 1029.0317}, + {id: 632, label: 'Sayouba Mandé', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Stabæk', value: 22, group: 9, x: 565.81647, y: -858.44836}, + {id: 633, label: 'Sead Kolašinac', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'Schalke 04', value: 28, group: 20, x: 1107.5244, y: -303.29904}, + {id: 634, label: 'Sebastián Coates', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Nacional', value: 22, group: 6, x: -52.670105, y: 55.847183}, + {id: 635, label: 'Sejad Salihovic', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + '1899 Hoffenheim', value: 23, group: 20, x: 1178.5911, y: -598.751}, + {id: 636, label: 'Senad Lulic', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'Lazio', value: 28, group: 20, x: 921.65936, y: -424.2279}, + {id: 637, label: 'Senijad Ibricic', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'Kayseri Erciyesspor', value: 22, group: 20, x: 1235.9749, y: -497.09393}, + {id: 638, label: 'Serey Die', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Basel', value: 26, group: 9, x: 467.3826, y: -653.70386}, + {id: 639, label: 'Serge Aurier', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Toulouse', value: 23, group: 9, x: 471.92194, y: -746.91907}, + {id: 640, label: 'Sergei Ignashevich', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'CSKA Moscow', value: 23, group: 2, x: -1314.4222, y: -1444.7848}, + {id: 641, label: 'Sergey Ryzhikov', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'Rubin Kazan', value: 23, group: 2, x: -1292.9913, y: -1369.3878}, + {id: 642, label: 'Sergio Agüero', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'Manchester City', value: 29, group: 19, x: -986.27966, y: 70.57652}, + {id: 643, label: 'Sergio Busquets', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Barcelona', value: 31, group: 23, x: -999.5799, y: -234.1426}, + {id: 644, label: 'Sergio Ramos', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Real Madrid', value: 31, group: 23, x: -838.31433, y: -237.33427}, + {id: 645, label: 'Sergio Romero', title: 'Country: ' + 'Argentina' + '
' + 'Team: ' + 'AS Monaco', value: 25, group: 19, x: -1110.6039, y: 391.88278}, + {id: 646, label: 'Shinji Kagawa', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'Manchester United', value: 35, group: 27, x: 282.65262, y: 314.0348}, + {id: 647, label: 'Shinji Okazaki', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'Mainz 05', value: 26, group: 27, x: 873.3198, y: 703.759}, + {id: 648, label: 'Shkodran Mustafi', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Sampdoria', value: 22, group: 13, x: 459.89215, y: -438.27008}, + {id: 649, label: 'Shola Ameobi', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Newcastle United', value: 27, group: 14, x: 18.686876, y: -1408.742}, + {id: 650, label: 'Shuichi Gonda', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'F.C. Tokyo', value: 22, group: 27, x: 757.8243, y: 624.09985}, + {id: 651, label: 'Shusaku Nishikawa', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'Urawa Red Diamonds', value: 22, group: 27, x: 727.42017, y: 656.2659}, + {id: 652, label: 'Silvestre Varela', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Porto', value: 30, group: 8, x: -839.6357, y: 400.2162}, + {id: 653, label: 'Šime Vrsaljko', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Genoa', value: 24, group: 25, x: -183.16594, y: 697.412}, + {id: 654, label: 'Simon Mignolet', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Liverpool', value: 31, group: 28, x: -491.45493, y: -919.83154}, + {id: 655, label: 'Sofiane Feghouli', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Valencia', value: 26, group: 24, x: -1244.9492, y: 1115.6299}, + {id: 656, label: 'Sokratis Papastathopoulos', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'Borussia Dortmund', value: 27, group: 15, x: 1506.5099, y: 339.67212}, + {id: 657, label: 'Sol Bamba', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Trabzonspor', value: 22, group: 9, x: 570.6759, y: -908.82056}, + {id: 658, label: 'Son Heung-min', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Bayer Leverkusen', value: 24, group: 10, x: 1048.6976, y: 1445.7692}, + {id: 659, label: 'Stefan de Vrij', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Feyenoord', value: 22, group: 22, x: 967.54407, y: 46.134007}, + {id: 660, label: 'Stefanos Kapino', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'Panathinaikos', value: 24, group: 15, x: 1427.7283, y: 531.81995}, + {id: 661, label: 'Stephan Lichtsteiner', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'Juventus', value: 33, group: 0, x: 67.66878, y: 456.67883}, + {id: 662, label: 'Stéphane Mbia', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Sevilla', value: 25, group: 17, x: 284.887, y: 226.59521}, + {id: 663, label: 'Stéphane Ruffier', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Saint-Étienne', value: 24, group: 16, x: 44.785976, y: -265.3774}, + {id: 664, label: 'Stephen Adams', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Aduana Stars', value: 22, group: 5, x: 502.8429, y: 1418.3192}, + {id: 665, label: 'Steve von Bergen', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'Young Boys', value: 22, group: 0, x: 10.2854805, y: 206.53181}, + {id: 666, label: 'Steven Beitashour', title: 'Country: ' + 'Iran' + '
' + 'Team: ' + 'Vancouver Whitecaps FC', value: 22, group: 1, x: 1978.9785, y: 1007.8008}, + {id: 667, label: 'Steven Defour', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Porto', value: 30, group: 28, x: -855.4899, y: -553.74506}, + {id: 668, label: 'Steven Gerrard (c)', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Liverpool', value: 27, group: 28, x: -159.6521, y: -980.6687}, + {id: 669, label: 'Stipe Pletikosa', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Rostov', value: 22, group: 25, x: -333.2818, y: 696.163}, + {id: 670, label: 'Sulley Muntari', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Milan', value: 28, group: 5, x: 435.759, y: 1263.3812}, + {id: 671, label: 'Sylvain Gbohouo', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Séwé Sport', value: 22, group: 9, x: 531.5453, y: -936.86206}, + {id: 672, label: 'Teófilo Gutiérrez', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'River Plate', value: 22, group: 11, x: -811.0555, y: 1271.3983}, + {id: 673, label: 'Terence Kongolo', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Feyenoord', value: 22, group: 22, x: 966.41876, y: -4.162721}, + {id: 674, label: 'Theofanis Gekas', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'Konyaspor', value: 23, group: 15, x: 1527.9011, y: 552.6124}, + {id: 675, label: 'Thiago Motta', title: 'Country: ' + 'Italy' + '
' + 'Team: ' + 'Paris Saint-Germain', value: 29, group: 3, x: 60.09504, y: 671.3873}, + {id: 676, label: 'Thiago Silva (c)', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Paris Saint-Germain', value: 30, group: 23, x: -361.46573, y: -169.68611}, + {id: 677, label: 'Thibaut Courtois', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Atlético Madrid', value: 29, group: 28, x: -784.1882, y: -694.4416}, + {id: 678, label: 'Thomas Müller', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Bayern Munich', value: 29, group: 13, x: 396.2324, y: -434.3364}, + {id: 679, label: 'Thomas Vermaelen', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Arsenal', value: 31, group: 28, x: -482.76413, y: -771.15424}, + {id: 680, label: 'Tim Cahill', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'New York Red Bulls', value: 23, group: 12, x: 2114.505, y: -511.01007}, + {id: 681, label: 'Tim Howard', title: 'Country: ' + 'United States' + '
' + 'Team: ' + 'Everton', value: 27, group: 26, x: 597.101, y: -1458.6305}, + {id: 682, label: 'Tim Krul', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Newcastle United', value: 27, group: 22, x: 749.57495, y: -122.823105}, + {id: 683, label: 'Timothy Chandler', title: 'Country: ' + 'United States' + '
' + 'Team: ' + '1. FC Nürnberg', value: 25, group: 26, x: 803.35706, y: -1282.8247}, + {id: 684, label: 'Tino-Sven Sušic', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'Hajduk Split', value: 22, group: 20, x: 1264.119, y: -534.24}, + {id: 685, label: 'Toby Alderweireld', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Atlético Madrid', value: 29, group: 28, x: -719.4183, y: -665.748}, + {id: 686, label: 'Tommy Oar', title: 'Country: ' + 'Australia' + '
' + 'Team: ' + 'Utrecht', value: 22, group: 12, x: 2165.0227, y: -713.54254}, + {id: 687, label: 'Toni Kroos', title: 'Country: ' + 'Germany' + '
' + 'Team: ' + 'Bayern Munich', value: 29, group: 13, x: 364.47653, y: -371.89417}, + {id: 688, label: 'Toni Šunjic', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'Zorya Luhansk', value: 22, group: 20, x: 1221.8553, y: -554.841}, + {id: 689, label: 'Toshihiro Aoyama', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'Sanfrecce Hiroshima', value: 23, group: 27, x: 774.47, y: 733.8078}, + {id: 690, label: 'Tranquillo Barnetta', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'Eintracht Frankfurt', value: 23, group: 0, x: 73.72464, y: 117.78337}, + {id: 691, label: 'Uche Nwofor', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Heerenveen', value: 22, group: 14, x: -33.31396, y: -1701.1675}, + {id: 692, label: 'Valentin Stocker', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'Basel', value: 25, group: 0, x: 93.94299, y: 165.77863}, + {id: 693, label: 'Valon Behrami', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'Napoli', value: 31, group: 0, x: -152.94186, y: 233.43562}, + {id: 694, label: 'Vangelis Moras', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'Verona', value: 22, group: 15, x: 1602.7228, y: 488.25735}, + {id: 695, label: 'Vasili Berezutski (c)', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'CSKA Moscow', value: 23, group: 2, x: -1323.1439, y: -1494.2708}, + {id: 696, label: 'Vasilis Torosidis', title: 'Country: ' + 'Greece' + '
' + 'Team: ' + 'Roma', value: 26, group: 15, x: 1423.1809, y: 425.1927}, + {id: 697, label: 'Vedad Ibiševic', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'VfB Stuttgart', value: 25, group: 20, x: 1011.34985, y: -507.73672}, + {id: 698, label: 'Vedran Corluka', title: 'Country: ' + 'Croatia' + '
' + 'Team: ' + 'Lokomotiv Moscow', value: 23, group: 25, x: -415.4615, y: 539.5565}, + {id: 699, label: 'Victor', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Atlético Mineiro', value: 22, group: 23, x: -504.1157, y: -310.5912}, + {id: 700, label: 'Víctor Bernárdez', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'San Jose Earthquakes', value: 23, group: 7, x: 1542.3271, y: -1230.5049}, + {id: 701, label: 'Víctor Ibarbo', title: 'Country: ' + 'Colombia' + '
' + 'Team: ' + 'Cagliari', value: 23, group: 11, x: -760.3384, y: 1293.0891}, + {id: 702, label: 'Victor Moses', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Liverpool', value: 31, group: 14, x: -114.12856, y: -1433.1643}, + {id: 703, label: 'Vieirinha', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'VfL Wolfsburg', value: 28, group: 8, x: -584.53986, y: 300.7302}, + {id: 704, label: 'Viktor Fayzulin', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'Zenit Saint Petersburg', value: 26, group: 2, x: -1257.4415, y: -1320.7031}, + {id: 705, label: 'Vincent Aboubakar', title: 'Country: ' + 'Cameroon' + '
' + 'Team: ' + 'Lorient', value: 22, group: 17, x: 458.34485, y: 202.27162}, + {id: 706, label: 'Vincent Enyeama', title: 'Country: ' + 'Nigeria' + '
' + 'Team: ' + 'Lille', value: 25, group: 14, x: -105.49051, y: -1519.4764}, + {id: 707, label: 'Vincent Kompany (c)', title: 'Country: ' + 'Belgium' + '
' + 'Team: ' + 'Manchester City', value: 31, group: 28, x: -575.3739, y: -726.92163}, + {id: 708, label: 'Vladimir Granat', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'Dynamo Moscow', value: 23, group: 2, x: -1378.1497, y: -1417.719}, + {id: 709, label: 'Wakaso Mubarak', title: 'Country: ' + 'Ghana' + '
' + 'Team: ' + 'Rubin Kazan', value: 25, group: 5, x: 209.43652, y: 1057.448}, + {id: 710, label: 'Walter Ayoví', title: 'Country: ' + 'Ecuador' + '
' + 'Team: ' + 'Pachuca', value: 22, group: 4, x: -1792.0483, y: -657.5009}, + {id: 711, label: 'Walter Gargano', title: 'Country: ' + 'Uruguay' + '
' + 'Team: ' + 'Parma', value: 26, group: 6, x: -40.095936, y: 145.01854}, + {id: 712, label: 'Waylon Francis', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Columbus Crew', value: 22, group: 29, x: 2350.4897, y: 280.31845}, + {id: 713, label: 'Wayne Rooney', title: 'Country: ' + 'England' + '
' + 'Team: ' + 'Manchester United', value: 32, group: 28, x: -356.85434, y: -834.0883}, + {id: 714, label: 'Wesley Sneijder', title: 'Country: ' + 'Netherlands' + '
' + 'Team: ' + 'Galatasaray', value: 26, group: 22, x: 805.6672, y: -40.132378}, + {id: 715, label: 'Wilfried Bony', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Swansea City', value: 24, group: 9, x: 607.71, y: -803.1463}, + {id: 716, label: 'William Carvalho', title: 'Country: ' + 'Portugal' + '
' + 'Team: ' + 'Sporting CP', value: 24, group: 8, x: -772.3611, y: 375.09537}, + {id: 717, label: 'Willian', title: 'Country: ' + 'Brazil' + '
' + 'Team: ' + 'Chelsea', value: 30, group: 23, x: -440.73843, y: -410.8239}, + {id: 718, label: 'Wilson Palacios', title: 'Country: ' + 'Honduras' + '
' + 'Team: ' + 'Stoke City', value: 25, group: 7, x: 1475.9537, y: -1233.8828}, + {id: 719, label: 'Xabi Alonso', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Real Madrid', value: 31, group: 23, x: -899.6201, y: -193.28745}, + {id: 720, label: 'Xavi', title: 'Country: ' + 'Spain' + '
' + 'Team: ' + 'Barcelona', value: 31, group: 23, x: -1013.3928, y: -319.86545}, + {id: 721, label: 'Xherdan Shaqiri', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'Bayern Munich', value: 35, group: 0, x: 141.7251, y: 12.289529}, + {id: 722, label: 'Yacine Brahimi', title: 'Country: ' + 'Algeria' + '
' + 'Team: ' + 'Granada', value: 24, group: 24, x: -1176.7251, y: 1144.9346}, + {id: 723, label: 'Yann Sommer', title: 'Country: ' + 'Switzerland' + '
' + 'Team: ' + 'Basel', value: 25, group: 0, x: 110.022545, y: 216.66074}, + {id: 724, label: 'Yasuhito Endo', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'Gamba Osaka', value: 22, group: 27, x: 785.91925, y: 586.32904}, + {id: 725, label: 'Yasuyuki Konno', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'Gamba Osaka', value: 22, group: 27, x: 772.3632, y: 672.5744}, + {id: 726, label: 'Yaya Touré', title: 'Country: ' + 'Ivory Coast' + '
' + 'Team: ' + 'Manchester City', value: 31, group: 9, x: 251.69077, y: -758.7758}, + {id: 727, label: 'Yeltsin Tejeda', title: 'Country: ' + 'Costa Rica' + '
' + 'Team: ' + 'Saprissa', value: 22, group: 29, x: 2354.9373, y: 330.56363}, + {id: 728, label: 'Yohan Cabaye', title: 'Country: ' + 'France' + '
' + 'Team: ' + 'Paris Saint-Germain', value: 29, group: 16, x: -73.94801, y: -145.80449}, + {id: 729, label: 'Yoichiro Kakitani', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'Cerezo Osaka', value: 23, group: 27, x: 646.941, y: 622.23926}, + {id: 730, label: 'Yoshito Okubo', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'Kawasaki Frontale', value: 22, group: 27, x: 717.32806, y: 699.96234}, + {id: 731, label: 'Yun Suk-young', title: 'Country: ' + 'South Korea' + '
' + 'Team: ' + 'Queens Park Rangers', value: 23, group: 10, x: 1131.6682, y: 1494.4373}, + {id: 732, label: 'Yuri Lodygin', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'Zenit Saint Petersburg', value: 26, group: 2, x: -1301.0415, y: -1265.7511}, + {id: 733, label: 'Yuri Zhirkov', title: 'Country: ' + 'Russia' + '
' + 'Team: ' + 'Dynamo Moscow', value: 23, group: 2, x: -1464.4825, y: -1475.7117}, + {id: 734, label: 'Yuto Nagatomo', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + 'Internazionale', value: 29, group: 27, x: 395.00394, y: 607.5659}, + {id: 735, label: 'Yuya Osako', title: 'Country: ' + 'Japan' + '
' + 'Team: ' + '1860 München', value: 22, group: 27, x: 806.69904, y: 633.54565}, + {id: 736, label: 'Zvjezdan Misimovic', title: 'Country: ' + 'Bosnia and Herzegovina' + '
' + 'Team: ' + 'Guizhou Renhe', value: 22, group: 20, x: 1277.4697, y: -479.12265} - ]; + ]; + // create an array with edges + var edges = [ + {from: 1, to: 15}, + {from: 1, to: 97}, + {from: 1, to: 108}, + {from: 1, to: 173}, + {from: 1, to: 195}, + {from: 1, to: 205}, + {from: 1, to: 218}, + {from: 1, to: 274}, + {from: 1, to: 296}, + {from: 1, to: 418}, + {from: 1, to: 435}, + {from: 1, to: 493}, + {from: 1, to: 494}, + {from: 1, to: 519}, + {from: 1, to: 525}, + {from: 1, to: 526}, + {from: 1, to: 582}, + {from: 1, to: 585}, + {from: 1, to: 605}, + {from: 1, to: 631}, + {from: 1, to: 655}, + {from: 1, to: 722}, + {from: 2, to: 10}, + {from: 2, to: 31}, + {from: 2, to: 96}, + {from: 2, to: 99}, + {from: 2, to: 100}, + {from: 2, to: 105}, + {from: 2, to: 106}, + {from: 2, to: 130}, + {from: 2, to: 153}, + {from: 2, to: 181}, + {from: 2, to: 219}, + {from: 2, to: 234}, + {from: 2, to: 304}, + {from: 2, to: 309}, + {from: 2, to: 322}, + {from: 2, to: 366}, + {from: 2, to: 369}, + {from: 2, to: 370}, + {from: 2, to: 457}, + {from: 2, to: 554}, + {from: 2, to: 630}, + {from: 2, to: 639}, + {from: 2, to: 672}, + {from: 2, to: 701}, + {from: 3, to: 38}, + {from: 3, to: 39}, + {from: 3, to: 121}, + {from: 3, to: 129}, + {from: 3, to: 165}, + {from: 3, to: 166}, + {from: 3, to: 167}, + {from: 3, to: 168}, + {from: 3, to: 185}, + {from: 3, to: 191}, + {from: 3, to: 226}, + {from: 3, to: 240}, + {from: 3, to: 352}, + {from: 3, to: 359}, + {from: 3, to: 430}, + {from: 3, to: 461}, + {from: 3, to: 463}, + {from: 3, to: 486}, + {from: 3, to: 531}, + {from: 3, to: 607}, + {from: 3, to: 634}, + {from: 3, to: 711}, + {from: 4, to: 11}, + {from: 4, to: 18}, + {from: 4, to: 43}, + {from: 4, to: 65}, + {from: 4, to: 118}, + {from: 4, to: 138}, + {from: 4, to: 199}, + {from: 4, to: 220}, + {from: 4, to: 272}, + {from: 4, to: 340}, + {from: 4, to: 346}, + {from: 4, to: 347}, + {from: 4, to: 387}, + {from: 4, to: 404}, + {from: 4, to: 437}, + {from: 4, to: 503}, + {from: 4, to: 520}, + {from: 4, to: 590}, + {from: 4, to: 628}, + {from: 4, to: 664}, + {from: 4, to: 670}, + {from: 4, to: 709}, + {from: 5, to: 27}, + {from: 5, to: 80}, + {from: 5, to: 116}, + {from: 5, to: 139}, + {from: 5, to: 144}, + {from: 5, to: 157}, + {from: 5, to: 231}, + {from: 5, to: 232}, + {from: 5, to: 238}, + {from: 5, to: 240}, + {from: 5, to: 258}, + {from: 5, to: 303}, + {from: 5, to: 308}, + {from: 5, to: 335}, + {from: 5, to: 348}, + {from: 5, to: 415}, + {from: 5, to: 434}, + {from: 5, to: 491}, + {from: 5, to: 521}, + {from: 5, to: 575}, + {from: 5, to: 576}, + {from: 5, to: 583}, + {from: 5, to: 603}, + {from: 5, to: 616}, + {from: 5, to: 668}, + {from: 5, to: 713}, + {from: 6, to: 29}, + {from: 6, to: 77}, + {from: 6, to: 81}, + {from: 6, to: 148}, + {from: 6, to: 208}, + {from: 6, to: 298}, + {from: 6, to: 307}, + {from: 6, to: 310}, + {from: 6, to: 313}, + {from: 6, to: 458}, + {from: 6, to: 459}, + {from: 6, to: 468}, + {from: 6, to: 470}, + {from: 6, to: 471}, + {from: 6, to: 477}, + {from: 6, to: 479}, + {from: 6, to: 515}, + {from: 6, to: 518}, + {from: 6, to: 541}, + {from: 6, to: 620}, + {from: 6, to: 680}, + {from: 6, to: 686}, + {from: 7, to: 88}, + {from: 7, to: 162}, + {from: 7, to: 215}, + {from: 7, to: 241}, + {from: 7, to: 260}, + {from: 7, to: 266}, + {from: 7, to: 271}, + {from: 7, to: 339}, + {from: 7, to: 364}, + {from: 7, to: 453}, + {from: 7, to: 480}, + {from: 7, to: 497}, + {from: 7, to: 504}, + {from: 7, to: 578}, + {from: 7, to: 596}, + {from: 7, to: 602}, + {from: 7, to: 610}, + {from: 7, to: 661}, + {from: 7, to: 665}, + {from: 7, to: 690}, + {from: 7, to: 692}, + {from: 7, to: 693}, + {from: 7, to: 721}, + {from: 7, to: 723}, + {from: 8, to: 56}, + {from: 8, to: 60}, + {from: 8, to: 74}, + {from: 8, to: 116}, + {from: 8, to: 140}, + {from: 8, to: 144}, + {from: 8, to: 150}, + {from: 8, to: 172}, + {from: 8, to: 177}, + {from: 8, to: 179}, + {from: 8, to: 311}, + {from: 8, to: 318}, + {from: 8, to: 371}, + {from: 8, to: 384}, + {from: 8, to: 386}, + {from: 8, to: 408}, + {from: 8, to: 460}, + {from: 8, to: 522}, + {from: 8, to: 527}, + {from: 8, to: 528}, + {from: 8, to: 532}, + {from: 8, to: 562}, + {from: 8, to: 576}, + {from: 8, to: 606}, + {from: 8, to: 612}, + {from: 8, to: 625}, + {from: 8, to: 646}, + {from: 8, to: 654}, + {from: 8, to: 667}, + {from: 8, to: 677}, + {from: 8, to: 679}, + {from: 8, to: 685}, + {from: 8, to: 707}, + {from: 8, to: 713}, + {from: 9, to: 30}, + {from: 9, to: 60}, + {from: 9, to: 102}, + {from: 9, to: 120}, + {from: 9, to: 186}, + {from: 9, to: 201}, + {from: 9, to: 222}, + {from: 9, to: 228}, + {from: 9, to: 235}, + {from: 9, to: 236}, + {from: 9, to: 305}, + {from: 9, to: 324}, + {from: 9, to: 334}, + {from: 9, to: 353}, + {from: 9, to: 368}, + {from: 9, to: 429}, + {from: 9, to: 489}, + {from: 9, to: 499}, + {from: 9, to: 548}, + {from: 9, to: 552}, + {from: 9, to: 595}, + {from: 9, to: 710}, + {from: 10, to: 31}, + {from: 10, to: 96}, + {from: 10, to: 99}, + {from: 10, to: 100}, + {from: 10, to: 105}, + {from: 10, to: 106}, + {from: 10, to: 130}, + {from: 10, to: 153}, + {from: 10, to: 181}, + {from: 10, to: 219}, + {from: 10, to: 234}, + {from: 10, to: 304}, + {from: 10, to: 309}, + {from: 10, to: 341}, + {from: 10, to: 366}, + {from: 10, to: 369}, + {from: 10, to: 370}, + {from: 10, to: 457}, + {from: 10, to: 554}, + {from: 10, to: 630}, + {from: 10, to: 672}, + {from: 10, to: 701}, + {from: 11, to: 18}, + {from: 11, to: 43}, + {from: 11, to: 59}, + {from: 11, to: 65}, + {from: 11, to: 118}, + {from: 11, to: 138}, + {from: 11, to: 199}, + {from: 11, to: 220}, + {from: 11, to: 237}, + {from: 11, to: 272}, + {from: 11, to: 340}, + {from: 11, to: 346}, + {from: 11, to: 347}, + {from: 11, to: 387}, + {from: 11, to: 404}, + {from: 11, to: 437}, + {from: 11, to: 447}, + {from: 11, to: 503}, + {from: 11, to: 520}, + {from: 11, to: 590}, + {from: 11, to: 628}, + {from: 11, to: 664}, + {from: 11, to: 670}, + {from: 11, to: 709}, + {from: 11, to: 711}, + {from: 12, to: 54}, + {from: 12, to: 70}, + {from: 12, to: 202}, + {from: 12, to: 211}, + {from: 12, to: 212}, + {from: 12, to: 221}, + {from: 12, to: 225}, + {from: 12, to: 261}, + {from: 12, to: 287}, + {from: 12, to: 319}, + {from: 12, to: 358}, + {from: 12, to: 419}, + {from: 12, to: 424}, + {from: 12, to: 450}, + {from: 12, to: 451}, + {from: 12, to: 462}, + {from: 12, to: 487}, + {from: 12, to: 555}, + {from: 12, to: 600}, + {from: 12, to: 608}, + {from: 12, to: 642}, + {from: 12, to: 645}, + {from: 13, to: 35}, + {from: 13, to: 36}, + {from: 13, to: 40}, + {from: 13, to: 41}, + {from: 13, to: 66}, + {from: 13, to: 78}, + {from: 13, to: 137}, + {from: 13, to: 192}, + {from: 13, to: 247}, + {from: 13, to: 273}, + {from: 13, to: 284}, + {from: 13, to: 306}, + {from: 13, to: 315}, + {from: 13, to: 380}, + {from: 13, to: 389}, + {from: 13, to: 467}, + {from: 13, to: 495}, + {from: 13, to: 570}, + {from: 13, to: 584}, + {from: 13, to: 598}, + {from: 13, to: 599}, + {from: 13, to: 666}, + {from: 14, to: 16}, + {from: 14, to: 72}, + {from: 14, to: 75}, + {from: 14, to: 115}, + {from: 14, to: 190}, + {from: 14, to: 194}, + {from: 14, to: 200}, + {from: 14, to: 243}, + {from: 14, to: 259}, + {from: 14, to: 292}, + {from: 14, to: 342}, + {from: 14, to: 363}, + {from: 14, to: 379}, + {from: 14, to: 383}, + {from: 14, to: 403}, + {from: 14, to: 500}, + {from: 14, to: 505}, + {from: 14, to: 537}, + {from: 14, to: 574}, + {from: 14, to: 587}, + {from: 14, to: 597}, + {from: 14, to: 640}, + {from: 14, to: 649}, + {from: 14, to: 691}, + {from: 14, to: 695}, + {from: 14, to: 702}, + {from: 14, to: 706}, + {from: 15, to: 97}, + {from: 15, to: 108}, + {from: 15, to: 173}, + {from: 15, to: 195}, + {from: 15, to: 205}, + {from: 15, to: 218}, + {from: 15, to: 274}, + {from: 15, to: 296}, + {from: 15, to: 418}, + {from: 15, to: 435}, + {from: 15, to: 493}, + {from: 15, to: 494}, + {from: 15, to: 519}, + {from: 15, to: 525}, + {from: 15, to: 526}, + {from: 15, to: 582}, + {from: 15, to: 585}, + {from: 15, to: 605}, + {from: 15, to: 631}, + {from: 15, to: 655}, + {from: 15, to: 722}, + {from: 16, to: 21}, + {from: 16, to: 22}, + {from: 16, to: 23}, + {from: 16, to: 24}, + {from: 16, to: 25}, + {from: 16, to: 48}, + {from: 16, to: 51}, + {from: 16, to: 158}, + {from: 16, to: 174}, + {from: 16, to: 243}, + {from: 16, to: 292}, + {from: 16, to: 293}, + {from: 16, to: 439}, + {from: 16, to: 540}, + {from: 16, to: 568}, + {from: 16, to: 640}, + {from: 16, to: 641}, + {from: 16, to: 695}, + {from: 16, to: 704}, + {from: 16, to: 708}, + {from: 16, to: 732}, + {from: 16, to: 733}, + {from: 17, to: 34}, + {from: 17, to: 49}, + {from: 17, to: 103}, + {from: 17, to: 104}, + {from: 17, to: 169}, + {from: 17, to: 229}, + {from: 17, to: 256}, + {from: 17, to: 267}, + {from: 17, to: 275}, + {from: 17, to: 276}, + {from: 17, to: 295}, + {from: 17, to: 317}, + {from: 17, to: 318}, + {from: 17, to: 355}, + {from: 17, to: 357}, + {from: 17, to: 446}, + {from: 17, to: 509}, + {from: 17, to: 510}, + {from: 17, to: 546}, + {from: 17, to: 564}, + {from: 17, to: 581}, + {from: 17, to: 592}, + {from: 18, to: 43}, + {from: 18, to: 65}, + {from: 18, to: 118}, + {from: 18, to: 138}, + {from: 18, to: 199}, + {from: 18, to: 220}, + {from: 18, to: 272}, + {from: 18, to: 340}, + {from: 18, to: 346}, + {from: 18, to: 347}, + {from: 18, to: 383}, + {from: 18, to: 387}, + {from: 18, to: 404}, + {from: 18, to: 437}, + {from: 18, to: 503}, + {from: 18, to: 520}, + {from: 18, to: 590}, + {from: 18, to: 628}, + {from: 18, to: 664}, + {from: 18, to: 670}, + {from: 18, to: 709}, + {from: 19, to: 26}, + {from: 19, to: 45}, + {from: 19, to: 46}, + {from: 19, to: 55}, + {from: 19, to: 58}, + {from: 19, to: 59}, + {from: 19, to: 123}, + {from: 19, to: 125}, + {from: 19, to: 141}, + {from: 19, to: 237}, + {from: 19, to: 249}, + {from: 19, to: 252}, + {from: 19, to: 291}, + {from: 19, to: 370}, + {from: 19, to: 416}, + {from: 19, to: 422}, + {from: 19, to: 447}, + {from: 19, to: 449}, + {from: 19, to: 452}, + {from: 19, to: 478}, + {from: 19, to: 481}, + {from: 19, to: 482}, + {from: 19, to: 622}, + {from: 19, to: 675}, + {from: 20, to: 62}, + {from: 20, to: 90}, + {from: 20, to: 91}, + {from: 20, to: 117}, + {from: 20, to: 126}, + {from: 20, to: 134}, + {from: 20, to: 156}, + {from: 20, to: 213}, + {from: 20, to: 242}, + {from: 20, to: 265}, + {from: 20, to: 326}, + {from: 20, to: 341}, + {from: 20, to: 365}, + {from: 20, to: 375}, + {from: 20, to: 406}, + {from: 20, to: 476}, + {from: 20, to: 502}, + {from: 20, to: 513}, + {from: 20, to: 530}, + {from: 20, to: 544}, + {from: 20, to: 681}, + {from: 20, to: 683}, + {from: 21, to: 22}, + {from: 21, to: 23}, + {from: 21, to: 24}, + {from: 21, to: 25}, + {from: 21, to: 48}, + {from: 21, to: 51}, + {from: 21, to: 74}, + {from: 21, to: 158}, + {from: 21, to: 174}, + {from: 21, to: 243}, + {from: 21, to: 289}, + {from: 21, to: 292}, + {from: 21, to: 293}, + {from: 21, to: 428}, + {from: 21, to: 439}, + {from: 21, to: 532}, + {from: 21, to: 540}, + {from: 21, to: 568}, + {from: 21, to: 640}, + {from: 21, to: 641}, + {from: 21, to: 695}, + {from: 21, to: 704}, + {from: 21, to: 708}, + {from: 21, to: 732}, + {from: 21, to: 733}, + {from: 22, to: 23}, + {from: 22, to: 24}, + {from: 22, to: 25}, + {from: 22, to: 48}, + {from: 22, to: 51}, + {from: 22, to: 120}, + {from: 22, to: 158}, + {from: 22, to: 174}, + {from: 22, to: 243}, + {from: 22, to: 292}, + {from: 22, to: 293}, + {from: 22, to: 439}, + {from: 22, to: 540}, + {from: 22, to: 568}, + {from: 22, to: 640}, + {from: 22, to: 641}, + {from: 22, to: 695}, + {from: 22, to: 704}, + {from: 22, to: 708}, + {from: 22, to: 732}, + {from: 22, to: 733}, + {from: 23, to: 24}, + {from: 23, to: 25}, + {from: 23, to: 48}, + {from: 23, to: 51}, + {from: 23, to: 158}, + {from: 23, to: 174}, + {from: 23, to: 243}, + {from: 23, to: 292}, + {from: 23, to: 293}, + {from: 23, to: 439}, + {from: 23, to: 540}, + {from: 23, to: 568}, + {from: 23, to: 640}, + {from: 23, to: 641}, + {from: 23, to: 695}, + {from: 23, to: 698}, + {from: 23, to: 704}, + {from: 23, to: 708}, + {from: 23, to: 732}, + {from: 23, to: 733}, + {from: 24, to: 25}, + {from: 24, to: 48}, + {from: 24, to: 51}, + {from: 24, to: 120}, + {from: 24, to: 158}, + {from: 24, to: 174}, + {from: 24, to: 243}, + {from: 24, to: 292}, + {from: 24, to: 293}, + {from: 24, to: 439}, + {from: 24, to: 540}, + {from: 24, to: 568}, + {from: 24, to: 640}, + {from: 24, to: 641}, + {from: 24, to: 695}, + {from: 24, to: 704}, + {from: 24, to: 708}, + {from: 24, to: 732}, + {from: 24, to: 733}, + {from: 25, to: 48}, + {from: 25, to: 51}, + {from: 25, to: 120}, + {from: 25, to: 158}, + {from: 25, to: 174}, + {from: 25, to: 243}, + {from: 25, to: 292}, + {from: 25, to: 293}, + {from: 25, to: 439}, + {from: 25, to: 540}, + {from: 25, to: 568}, + {from: 25, to: 640}, + {from: 25, to: 641}, + {from: 25, to: 695}, + {from: 25, to: 704}, + {from: 25, to: 708}, + {from: 25, to: 732}, + {from: 25, to: 733}, + {from: 26, to: 45}, + {from: 26, to: 46}, + {from: 26, to: 58}, + {from: 26, to: 59}, + {from: 26, to: 123}, + {from: 26, to: 125}, + {from: 26, to: 141}, + {from: 26, to: 237}, + {from: 26, to: 249}, + {from: 26, to: 252}, + {from: 26, to: 291}, + {from: 26, to: 416}, + {from: 26, to: 422}, + {from: 26, to: 447}, + {from: 26, to: 449}, + {from: 26, to: 452}, + {from: 26, to: 478}, + {from: 26, to: 481}, + {from: 26, to: 482}, + {from: 26, to: 558}, + {from: 26, to: 622}, + {from: 26, to: 675}, + {from: 27, to: 76}, + {from: 27, to: 80}, + {from: 27, to: 116}, + {from: 27, to: 139}, + {from: 27, to: 144}, + {from: 27, to: 231}, + {from: 27, to: 232}, + {from: 27, to: 238}, + {from: 27, to: 258}, + {from: 27, to: 303}, + {from: 27, to: 308}, + {from: 27, to: 335}, + {from: 27, to: 348}, + {from: 27, to: 409}, + {from: 27, to: 415}, + {from: 27, to: 433}, + {from: 27, to: 434}, + {from: 27, to: 498}, + {from: 27, to: 543}, + {from: 27, to: 573}, + {from: 27, to: 575}, + {from: 27, to: 576}, + {from: 27, to: 583}, + {from: 27, to: 603}, + {from: 27, to: 616}, + {from: 27, to: 629}, + {from: 27, to: 668}, + {from: 27, to: 679}, + {from: 27, to: 713}, + {from: 28, to: 33}, + {from: 28, to: 37}, + {from: 28, to: 50}, + {from: 28, to: 71}, + {from: 28, to: 83}, + {from: 28, to: 84}, + {from: 28, to: 107}, + {from: 28, to: 111}, + {from: 28, to: 113}, + {from: 28, to: 135}, + {from: 28, to: 146}, + {from: 28, to: 182}, + {from: 28, to: 210}, + {from: 28, to: 217}, + {from: 28, to: 245}, + {from: 28, to: 278}, + {from: 28, to: 319}, + {from: 28, to: 321}, + {from: 28, to: 337}, + {from: 28, to: 349}, + {from: 28, to: 368}, + {from: 28, to: 407}, + {from: 28, to: 419}, + {from: 28, to: 420}, + {from: 28, to: 429}, + {from: 28, to: 488}, + {from: 28, to: 489}, + {from: 28, to: 529}, + {from: 28, to: 533}, + {from: 28, to: 569}, + {from: 28, to: 579}, + {from: 28, to: 626}, + {from: 28, to: 627}, + {from: 28, to: 643}, + {from: 28, to: 662}, + {from: 28, to: 705}, + {from: 28, to: 720}, + {from: 29, to: 77}, + {from: 29, to: 81}, + {from: 29, to: 148}, + {from: 29, to: 208}, + {from: 29, to: 298}, + {from: 29, to: 307}, + {from: 29, to: 310}, + {from: 29, to: 313}, + {from: 29, to: 458}, + {from: 29, to: 459}, + {from: 29, to: 468}, + {from: 29, to: 470}, + {from: 29, to: 471}, + {from: 29, to: 477}, + {from: 29, to: 479}, + {from: 29, to: 515}, + {from: 29, to: 518}, + {from: 29, to: 541}, + {from: 29, to: 620}, + {from: 29, to: 680}, + {from: 29, to: 686}, + {from: 30, to: 60}, + {from: 30, to: 102}, + {from: 30, to: 120}, + {from: 30, to: 186}, + {from: 30, to: 201}, + {from: 30, to: 222}, + {from: 30, to: 228}, + {from: 30, to: 235}, + {from: 30, to: 236}, + {from: 30, to: 305}, + {from: 30, to: 324}, + {from: 30, to: 334}, + {from: 30, to: 353}, + {from: 30, to: 368}, + {from: 30, to: 429}, + {from: 30, to: 489}, + {from: 30, to: 499}, + {from: 30, to: 548}, + {from: 30, to: 552}, + {from: 30, to: 595}, + {from: 30, to: 710}, + {from: 31, to: 96}, + {from: 31, to: 99}, + {from: 31, to: 100}, + {from: 31, to: 105}, + {from: 31, to: 106}, + {from: 31, to: 130}, + {from: 31, to: 153}, + {from: 31, to: 181}, + {from: 31, to: 219}, + {from: 31, to: 234}, + {from: 31, to: 304}, + {from: 31, to: 309}, + {from: 31, to: 366}, + {from: 31, to: 369}, + {from: 31, to: 370}, + {from: 31, to: 457}, + {from: 31, to: 554}, + {from: 31, to: 630}, + {from: 31, to: 672}, + {from: 31, to: 701}, + {from: 32, to: 47}, + {from: 32, to: 170}, + {from: 32, to: 250}, + {from: 32, to: 251}, + {from: 32, to: 253}, + {from: 32, to: 254}, + {from: 32, to: 255}, + {from: 32, to: 356}, + {from: 32, to: 400}, + {from: 32, to: 401}, + {from: 32, to: 402}, + {from: 32, to: 410}, + {from: 32, to: 423}, + {from: 32, to: 545}, + {from: 32, to: 556}, + {from: 32, to: 557}, + {from: 32, to: 558}, + {from: 32, to: 656}, + {from: 32, to: 660}, + {from: 32, to: 674}, + {from: 32, to: 694}, + {from: 32, to: 696}, + {from: 33, to: 50}, + {from: 33, to: 64}, + {from: 33, to: 101}, + {from: 33, to: 111}, + {from: 33, to: 112}, + {from: 33, to: 124}, + {from: 33, to: 132}, + {from: 33, to: 135}, + {from: 33, to: 189}, + {from: 33, to: 207}, + {from: 33, to: 209}, + {from: 33, to: 214}, + {from: 33, to: 223}, + {from: 33, to: 230}, + {from: 33, to: 239}, + {from: 33, to: 245}, + {from: 33, to: 262}, + {from: 33, to: 319}, + {from: 33, to: 320}, + {from: 33, to: 344}, + {from: 33, to: 349}, + {from: 33, to: 354}, + {from: 33, to: 361}, + {from: 33, to: 362}, + {from: 33, to: 368}, + {from: 33, to: 419}, + {from: 33, to: 429}, + {from: 33, to: 445}, + {from: 33, to: 483}, + {from: 33, to: 484}, + {from: 33, to: 489}, + {from: 33, to: 512}, + {from: 33, to: 529}, + {from: 33, to: 569}, + {from: 33, to: 643}, + {from: 33, to: 720}, + {from: 34, to: 49}, + {from: 34, to: 103}, + {from: 34, to: 104}, + {from: 34, to: 169}, + {from: 34, to: 229}, + {from: 34, to: 256}, + {from: 34, to: 267}, + {from: 34, to: 275}, + {from: 34, to: 276}, + {from: 34, to: 295}, + {from: 34, to: 317}, + {from: 34, to: 318}, + {from: 34, to: 355}, + {from: 34, to: 357}, + {from: 34, to: 446}, + {from: 34, to: 509}, + {from: 34, to: 510}, + {from: 34, to: 546}, + {from: 34, to: 564}, + {from: 34, to: 581}, + {from: 34, to: 592}, + {from: 35, to: 36}, + {from: 35, to: 40}, + {from: 35, to: 41}, + {from: 35, to: 66}, + {from: 35, to: 78}, + {from: 35, to: 137}, + {from: 35, to: 192}, + {from: 35, to: 247}, + {from: 35, to: 273}, + {from: 35, to: 284}, + {from: 35, to: 306}, + {from: 35, to: 315}, + {from: 35, to: 380}, + {from: 35, to: 389}, + {from: 35, to: 467}, + {from: 35, to: 495}, + {from: 35, to: 570}, + {from: 35, to: 584}, + {from: 35, to: 598}, + {from: 35, to: 599}, + {from: 35, to: 666}, + {from: 36, to: 40}, + {from: 36, to: 41}, + {from: 36, to: 66}, + {from: 36, to: 78}, + {from: 36, to: 137}, + {from: 36, to: 192}, + {from: 36, to: 247}, + {from: 36, to: 273}, + {from: 36, to: 284}, + {from: 36, to: 306}, + {from: 36, to: 315}, + {from: 36, to: 380}, + {from: 36, to: 389}, + {from: 36, to: 467}, + {from: 36, to: 495}, + {from: 36, to: 570}, + {from: 36, to: 584}, + {from: 36, to: 598}, + {from: 36, to: 599}, + {from: 36, to: 666}, + {from: 37, to: 71}, + {from: 37, to: 83}, + {from: 37, to: 84}, + {from: 37, to: 107}, + {from: 37, to: 113}, + {from: 37, to: 146}, + {from: 37, to: 182}, + {from: 37, to: 210}, + {from: 37, to: 217}, + {from: 37, to: 278}, + {from: 37, to: 321}, + {from: 37, to: 337}, + {from: 37, to: 407}, + {from: 37, to: 420}, + {from: 37, to: 488}, + {from: 37, to: 533}, + {from: 37, to: 545}, + {from: 37, to: 579}, + {from: 37, to: 626}, + {from: 37, to: 627}, + {from: 37, to: 662}, + {from: 37, to: 705}, + {from: 37, to: 722}, + {from: 38, to: 39}, + {from: 38, to: 58}, + {from: 38, to: 121}, + {from: 38, to: 129}, + {from: 38, to: 165}, + {from: 38, to: 166}, + {from: 38, to: 167}, + {from: 38, to: 168}, + {from: 38, to: 185}, + {from: 38, to: 191}, + {from: 38, to: 226}, + {from: 38, to: 240}, + {from: 38, to: 277}, + {from: 38, to: 352}, + {from: 38, to: 359}, + {from: 38, to: 424}, + {from: 38, to: 430}, + {from: 38, to: 461}, + {from: 38, to: 463}, + {from: 38, to: 486}, + {from: 38, to: 517}, + {from: 38, to: 531}, + {from: 38, to: 537}, + {from: 38, to: 607}, + {from: 38, to: 634}, + {from: 38, to: 636}, + {from: 38, to: 711}, + {from: 39, to: 121}, + {from: 39, to: 129}, + {from: 39, to: 165}, + {from: 39, to: 166}, + {from: 39, to: 167}, + {from: 39, to: 168}, + {from: 39, to: 185}, + {from: 39, to: 191}, + {from: 39, to: 226}, + {from: 39, to: 240}, + {from: 39, to: 352}, + {from: 39, to: 359}, + {from: 39, to: 430}, + {from: 39, to: 461}, + {from: 39, to: 463}, + {from: 39, to: 486}, + {from: 39, to: 531}, + {from: 39, to: 607}, + {from: 39, to: 634}, + {from: 39, to: 711}, + {from: 40, to: 41}, + {from: 40, to: 66}, + {from: 40, to: 78}, + {from: 40, to: 137}, + {from: 40, to: 192}, + {from: 40, to: 247}, + {from: 40, to: 273}, + {from: 40, to: 284}, + {from: 40, to: 306}, + {from: 40, to: 315}, + {from: 40, to: 380}, + {from: 40, to: 389}, + {from: 40, to: 467}, + {from: 40, to: 495}, + {from: 40, to: 570}, + {from: 40, to: 584}, + {from: 40, to: 598}, + {from: 40, to: 599}, + {from: 40, to: 666}, + {from: 41, to: 66}, + {from: 41, to: 78}, + {from: 41, to: 137}, + {from: 41, to: 192}, + {from: 41, to: 247}, + {from: 41, to: 273}, + {from: 41, to: 284}, + {from: 41, to: 306}, + {from: 41, to: 315}, + {from: 41, to: 380}, + {from: 41, to: 389}, + {from: 41, to: 467}, + {from: 41, to: 495}, + {from: 41, to: 570}, + {from: 41, to: 584}, + {from: 41, to: 598}, + {from: 41, to: 599}, + {from: 41, to: 666}, + {from: 42, to: 86}, + {from: 42, to: 93}, + {from: 42, to: 131}, + {from: 42, to: 180}, + {from: 42, to: 188}, + {from: 42, to: 202}, + {from: 42, to: 211}, + {from: 42, to: 216}, + {from: 42, to: 277}, + {from: 42, to: 286}, + {from: 42, to: 332}, + {from: 42, to: 333}, + {from: 42, to: 428}, + {from: 42, to: 486}, + {from: 42, to: 511}, + {from: 42, to: 528}, + {from: 42, to: 571}, + {from: 42, to: 580}, + {from: 42, to: 593}, + {from: 42, to: 601}, + {from: 42, to: 618}, + {from: 42, to: 619}, + {from: 42, to: 652}, + {from: 42, to: 703}, + {from: 42, to: 716}, + {from: 43, to: 65}, + {from: 43, to: 118}, + {from: 43, to: 138}, + {from: 43, to: 199}, + {from: 43, to: 220}, + {from: 43, to: 272}, + {from: 43, to: 340}, + {from: 43, to: 346}, + {from: 43, to: 347}, + {from: 43, to: 387}, + {from: 43, to: 404}, + {from: 43, to: 437}, + {from: 43, to: 473}, + {from: 43, to: 503}, + {from: 43, to: 520}, + {from: 43, to: 533}, + {from: 43, to: 590}, + {from: 43, to: 628}, + {from: 43, to: 664}, + {from: 43, to: 670}, + {from: 43, to: 709}, + {from: 44, to: 79}, + {from: 44, to: 82}, + {from: 44, to: 110}, + {from: 44, to: 122}, + {from: 44, to: 151}, + {from: 44, to: 179}, + {from: 44, to: 203}, + {from: 44, to: 227}, + {from: 44, to: 231}, + {from: 44, to: 238}, + {from: 44, to: 327}, + {from: 44, to: 342}, + {from: 44, to: 374}, + {from: 44, to: 385}, + {from: 44, to: 433}, + {from: 44, to: 442}, + {from: 44, to: 454}, + {from: 44, to: 475}, + {from: 44, to: 480}, + {from: 44, to: 498}, + {from: 44, to: 517}, + {from: 44, to: 547}, + {from: 44, to: 573}, + {from: 44, to: 577}, + {from: 44, to: 586}, + {from: 44, to: 611}, + {from: 44, to: 614}, + {from: 44, to: 623}, + {from: 44, to: 627}, + {from: 44, to: 648}, + {from: 44, to: 678}, + {from: 44, to: 687}, + {from: 44, to: 717}, + {from: 45, to: 46}, + {from: 45, to: 58}, + {from: 45, to: 59}, + {from: 45, to: 64}, + {from: 45, to: 123}, + {from: 45, to: 125}, + {from: 45, to: 141}, + {from: 45, to: 237}, + {from: 45, to: 249}, + {from: 45, to: 252}, + {from: 45, to: 291}, + {from: 45, to: 404}, + {from: 45, to: 416}, + {from: 45, to: 422}, + {from: 45, to: 447}, + {from: 45, to: 449}, + {from: 45, to: 452}, + {from: 45, to: 461}, + {from: 45, to: 478}, + {from: 45, to: 481}, + {from: 45, to: 482}, + {from: 45, to: 483}, + {from: 45, to: 565}, + {from: 45, to: 622}, + {from: 45, to: 661}, + {from: 45, to: 675}, + {from: 46, to: 58}, + {from: 46, to: 59}, + {from: 46, to: 64}, + {from: 46, to: 123}, + {from: 46, to: 125}, + {from: 46, to: 141}, + {from: 46, to: 237}, + {from: 46, to: 249}, + {from: 46, to: 252}, + {from: 46, to: 291}, + {from: 46, to: 404}, + {from: 46, to: 416}, + {from: 46, to: 422}, + {from: 46, to: 447}, + {from: 46, to: 449}, + {from: 46, to: 452}, + {from: 46, to: 461}, + {from: 46, to: 478}, + {from: 46, to: 481}, + {from: 46, to: 482}, + {from: 46, to: 483}, + {from: 46, to: 565}, + {from: 46, to: 622}, + {from: 46, to: 661}, + {from: 46, to: 675}, + {from: 47, to: 170}, + {from: 47, to: 250}, + {from: 47, to: 251}, + {from: 47, to: 253}, + {from: 47, to: 254}, + {from: 47, to: 255}, + {from: 47, to: 336}, + {from: 47, to: 356}, + {from: 47, to: 400}, + {from: 47, to: 401}, + {from: 47, to: 402}, + {from: 47, to: 410}, + {from: 47, to: 423}, + {from: 47, to: 545}, + {from: 47, to: 556}, + {from: 47, to: 557}, + {from: 47, to: 558}, + {from: 47, to: 656}, + {from: 47, to: 660}, + {from: 47, to: 674}, + {from: 47, to: 694}, + {from: 47, to: 696}, + {from: 48, to: 51}, + {from: 48, to: 158}, + {from: 48, to: 174}, + {from: 48, to: 243}, + {from: 48, to: 292}, + {from: 48, to: 293}, + {from: 48, to: 439}, + {from: 48, to: 540}, + {from: 48, to: 568}, + {from: 48, to: 640}, + {from: 48, to: 641}, + {from: 48, to: 695}, + {from: 48, to: 704}, + {from: 48, to: 708}, + {from: 48, to: 732}, + {from: 48, to: 733}, + {from: 49, to: 103}, + {from: 49, to: 104}, + {from: 49, to: 169}, + {from: 49, to: 198}, + {from: 49, to: 229}, + {from: 49, to: 256}, + {from: 49, to: 267}, + {from: 49, to: 275}, + {from: 49, to: 276}, + {from: 49, to: 295}, + {from: 49, to: 317}, + {from: 49, to: 318}, + {from: 49, to: 355}, + {from: 49, to: 357}, + {from: 49, to: 446}, + {from: 49, to: 509}, + {from: 49, to: 510}, + {from: 49, to: 546}, + {from: 49, to: 564}, + {from: 49, to: 581}, + {from: 49, to: 592}, + {from: 49, to: 658}, + {from: 50, to: 110}, + {from: 50, to: 111}, + {from: 50, to: 135}, + {from: 50, to: 150}, + {from: 50, to: 154}, + {from: 50, to: 155}, + {from: 50, to: 164}, + {from: 50, to: 227}, + {from: 50, to: 245}, + {from: 50, to: 294}, + {from: 50, to: 316}, + {from: 50, to: 319}, + {from: 50, to: 349}, + {from: 50, to: 368}, + {from: 50, to: 371}, + {from: 50, to: 373}, + {from: 50, to: 397}, + {from: 50, to: 419}, + {from: 50, to: 429}, + {from: 50, to: 489}, + {from: 50, to: 529}, + {from: 50, to: 569}, + {from: 50, to: 572}, + {from: 50, to: 591}, + {from: 50, to: 629}, + {from: 50, to: 643}, + {from: 50, to: 644}, + {from: 50, to: 719}, + {from: 50, to: 720}, + {from: 51, to: 158}, + {from: 51, to: 174}, + {from: 51, to: 243}, + {from: 51, to: 292}, + {from: 51, to: 293}, + {from: 51, to: 439}, + {from: 51, to: 540}, + {from: 51, to: 568}, + {from: 51, to: 640}, + {from: 51, to: 641}, + {from: 51, to: 695}, + {from: 51, to: 704}, + {from: 51, to: 708}, + {from: 51, to: 732}, + {from: 51, to: 733}, + {from: 52, to: 56}, + {from: 52, to: 92}, + {from: 52, to: 98}, + {from: 52, to: 176}, + {from: 52, to: 178}, + {from: 52, to: 197}, + {from: 52, to: 328}, + {from: 52, to: 329}, + {from: 52, to: 351}, + {from: 52, to: 367}, + {from: 52, to: 372}, + {from: 52, to: 426}, + {from: 52, to: 427}, + {from: 52, to: 456}, + {from: 52, to: 464}, + {from: 52, to: 492}, + {from: 52, to: 536}, + {from: 52, to: 549}, + {from: 52, to: 551}, + {from: 52, to: 609}, + {from: 52, to: 615}, + {from: 52, to: 700}, + {from: 52, to: 718}, + {from: 53, to: 67}, + {from: 53, to: 68}, + {from: 53, to: 73}, + {from: 53, to: 183}, + {from: 53, to: 184}, + {from: 53, to: 198}, + {from: 53, to: 204}, + {from: 53, to: 270}, + {from: 53, to: 302}, + {from: 53, to: 312}, + {from: 53, to: 497}, + {from: 53, to: 516}, + {from: 53, to: 524}, + {from: 53, to: 538}, + {from: 53, to: 633}, + {from: 53, to: 635}, + {from: 53, to: 636}, + {from: 53, to: 637}, + {from: 53, to: 684}, + {from: 53, to: 688}, + {from: 53, to: 697}, + {from: 53, to: 736}, + {from: 54, to: 70}, + {from: 54, to: 131}, + {from: 54, to: 202}, + {from: 54, to: 211}, + {from: 54, to: 212}, + {from: 54, to: 216}, + {from: 54, to: 221}, + {from: 54, to: 225}, + {from: 54, to: 261}, + {from: 54, to: 287}, + {from: 54, to: 294}, + {from: 54, to: 319}, + {from: 54, to: 358}, + {from: 54, to: 381}, + {from: 54, to: 419}, + {from: 54, to: 424}, + {from: 54, to: 432}, + {from: 54, to: 443}, + {from: 54, to: 450}, + {from: 54, to: 451}, + {from: 54, to: 462}, + {from: 54, to: 487}, + {from: 54, to: 555}, + {from: 54, to: 571}, + {from: 54, to: 589}, + {from: 54, to: 600}, + {from: 54, to: 608}, + {from: 54, to: 623}, + {from: 54, to: 642}, + {from: 54, to: 644}, + {from: 54, to: 645}, + {from: 54, to: 719}, + {from: 55, to: 142}, + {from: 55, to: 143}, + {from: 55, to: 147}, + {from: 55, to: 157}, + {from: 55, to: 175}, + {from: 55, to: 187}, + {from: 55, to: 263}, + {from: 55, to: 299}, + {from: 55, to: 300}, + {from: 55, to: 301}, + {from: 55, to: 370}, + {from: 55, to: 432}, + {from: 55, to: 444}, + {from: 55, to: 455}, + {from: 55, to: 469}, + {from: 55, to: 514}, + {from: 55, to: 535}, + {from: 55, to: 539}, + {from: 55, to: 542}, + {from: 55, to: 624}, + {from: 55, to: 653}, + {from: 55, to: 669}, + {from: 55, to: 698}, + {from: 56, to: 74}, + {from: 56, to: 140}, + {from: 56, to: 172}, + {from: 56, to: 177}, + {from: 56, to: 179}, + {from: 56, to: 311}, + {from: 56, to: 384}, + {from: 56, to: 386}, + {from: 56, to: 408}, + {from: 56, to: 460}, + {from: 56, to: 522}, + {from: 56, to: 527}, + {from: 56, to: 532}, + {from: 56, to: 612}, + {from: 56, to: 625}, + {from: 56, to: 654}, + {from: 56, to: 667}, + {from: 56, to: 677}, + {from: 56, to: 679}, + {from: 56, to: 685}, + {from: 56, to: 707}, + {from: 57, to: 76}, + {from: 57, to: 87}, + {from: 57, to: 124}, + {from: 57, to: 196}, + {from: 57, to: 271}, + {from: 57, to: 288}, + {from: 57, to: 381}, + {from: 57, to: 409}, + {from: 57, to: 421}, + {from: 57, to: 425}, + {from: 57, to: 440}, + {from: 57, to: 472}, + {from: 57, to: 473}, + {from: 57, to: 508}, + {from: 57, to: 521}, + {from: 57, to: 523}, + {from: 57, to: 543}, + {from: 57, to: 562}, + {from: 57, to: 565}, + {from: 57, to: 589}, + {from: 57, to: 594}, + {from: 57, to: 604}, + {from: 57, to: 615}, + {from: 57, to: 663}, + {from: 57, to: 728}, + {from: 58, to: 59}, + {from: 58, to: 123}, + {from: 58, to: 125}, + {from: 58, to: 141}, + {from: 58, to: 237}, + {from: 58, to: 249}, + {from: 58, to: 252}, + {from: 58, to: 277}, + {from: 58, to: 291}, + {from: 58, to: 416}, + {from: 58, to: 422}, + {from: 58, to: 424}, + {from: 58, to: 447}, + {from: 58, to: 449}, + {from: 58, to: 452}, + {from: 58, to: 478}, + {from: 58, to: 481}, + {from: 58, to: 482}, + {from: 58, to: 517}, + {from: 58, to: 537}, + {from: 58, to: 622}, + {from: 58, to: 636}, + {from: 58, to: 675}, + {from: 59, to: 123}, + {from: 59, to: 125}, + {from: 59, to: 141}, + {from: 59, to: 237}, + {from: 59, to: 249}, + {from: 59, to: 252}, + {from: 59, to: 291}, + {from: 59, to: 416}, + {from: 59, to: 422}, + {from: 59, to: 447}, + {from: 59, to: 449}, + {from: 59, to: 452}, + {from: 59, to: 478}, + {from: 59, to: 481}, + {from: 59, to: 482}, + {from: 59, to: 622}, + {from: 59, to: 675}, + {from: 59, to: 711}, + {from: 60, to: 102}, + {from: 60, to: 116}, + {from: 60, to: 120}, + {from: 60, to: 144}, + {from: 60, to: 150}, + {from: 60, to: 186}, + {from: 60, to: 201}, + {from: 60, to: 222}, + {from: 60, to: 228}, + {from: 60, to: 235}, + {from: 60, to: 236}, + {from: 60, to: 305}, + {from: 60, to: 318}, + {from: 60, to: 324}, + {from: 60, to: 334}, + {from: 60, to: 353}, + {from: 60, to: 368}, + {from: 60, to: 371}, + {from: 60, to: 429}, + {from: 60, to: 460}, + {from: 60, to: 489}, + {from: 60, to: 499}, + {from: 60, to: 528}, + {from: 60, to: 548}, + {from: 60, to: 552}, + {from: 60, to: 562}, + {from: 60, to: 576}, + {from: 60, to: 595}, + {from: 60, to: 606}, + {from: 60, to: 646}, + {from: 60, to: 710}, + {from: 60, to: 713}, + {from: 61, to: 79}, + {from: 61, to: 94}, + {from: 61, to: 133}, + {from: 61, to: 140}, + {from: 61, to: 145}, + {from: 61, to: 149}, + {from: 61, to: 171}, + {from: 61, to: 244}, + {from: 61, to: 314}, + {from: 61, to: 316}, + {from: 61, to: 325}, + {from: 61, to: 327}, + {from: 61, to: 338}, + {from: 61, to: 345}, + {from: 61, to: 350}, + {from: 61, to: 375}, + {from: 61, to: 396}, + {from: 61, to: 417}, + {from: 61, to: 442}, + {from: 61, to: 454}, + {from: 61, to: 455}, + {from: 61, to: 496}, + {from: 61, to: 507}, + {from: 61, to: 534}, + {from: 61, to: 566}, + {from: 61, to: 577}, + {from: 61, to: 606}, + {from: 61, to: 613}, + {from: 61, to: 659}, + {from: 61, to: 673}, + {from: 61, to: 678}, + {from: 61, to: 682}, + {from: 61, to: 687}, + {from: 61, to: 714}, + {from: 61, to: 721}, + {from: 62, to: 90}, + {from: 62, to: 91}, + {from: 62, to: 117}, + {from: 62, to: 126}, + {from: 62, to: 134}, + {from: 62, to: 156}, + {from: 62, to: 213}, + {from: 62, to: 242}, + {from: 62, to: 265}, + {from: 62, to: 326}, + {from: 62, to: 341}, + {from: 62, to: 365}, + {from: 62, to: 375}, + {from: 62, to: 406}, + {from: 62, to: 476}, + {from: 62, to: 502}, + {from: 62, to: 513}, + {from: 62, to: 530}, + {from: 62, to: 544}, + {from: 62, to: 681}, + {from: 62, to: 683}, + {from: 63, to: 89}, + {from: 63, to: 102}, + {from: 63, to: 114}, + {from: 63, to: 127}, + {from: 63, to: 159}, + {from: 63, to: 160}, + {from: 63, to: 161}, + {from: 63, to: 246}, + {from: 63, to: 257}, + {from: 63, to: 264}, + {from: 63, to: 297}, + {from: 63, to: 322}, + {from: 63, to: 398}, + {from: 63, to: 474}, + {from: 63, to: 485}, + {from: 63, to: 553}, + {from: 63, to: 621}, + {from: 63, to: 632}, + {from: 63, to: 638}, + {from: 63, to: 639}, + {from: 63, to: 657}, + {from: 63, to: 671}, + {from: 63, to: 697}, + {from: 63, to: 715}, + {from: 63, to: 726}, + {from: 64, to: 101}, + {from: 64, to: 112}, + {from: 64, to: 124}, + {from: 64, to: 125}, + {from: 64, to: 132}, + {from: 64, to: 189}, + {from: 64, to: 207}, + {from: 64, to: 209}, + {from: 64, to: 214}, + {from: 64, to: 223}, + {from: 64, to: 230}, + {from: 64, to: 239}, + {from: 64, to: 249}, + {from: 64, to: 252}, + {from: 64, to: 262}, + {from: 64, to: 320}, + {from: 64, to: 344}, + {from: 64, to: 354}, + {from: 64, to: 361}, + {from: 64, to: 362}, + {from: 64, to: 404}, + {from: 64, to: 416}, + {from: 64, to: 445}, + {from: 64, to: 461}, + {from: 64, to: 483}, + {from: 64, to: 484}, + {from: 64, to: 512}, + {from: 64, to: 565}, + {from: 64, to: 661}, + {from: 65, to: 118}, + {from: 65, to: 138}, + {from: 65, to: 199}, + {from: 65, to: 220}, + {from: 65, to: 272}, + {from: 65, to: 340}, + {from: 65, to: 346}, + {from: 65, to: 347}, + {from: 65, to: 387}, + {from: 65, to: 404}, + {from: 65, to: 437}, + {from: 65, to: 503}, + {from: 65, to: 520}, + {from: 65, to: 590}, + {from: 65, to: 628}, + {from: 65, to: 664}, + {from: 65, to: 670}, + {from: 65, to: 709}, + {from: 66, to: 78}, + {from: 66, to: 137}, + {from: 66, to: 192}, + {from: 66, to: 247}, + {from: 66, to: 253}, + {from: 66, to: 273}, + {from: 66, to: 284}, + {from: 66, to: 306}, + {from: 66, to: 315}, + {from: 66, to: 380}, + {from: 66, to: 389}, + {from: 66, to: 402}, + {from: 66, to: 467}, + {from: 66, to: 495}, + {from: 66, to: 570}, + {from: 66, to: 584}, + {from: 66, to: 598}, + {from: 66, to: 599}, + {from: 66, to: 666}, + {from: 67, to: 68}, + {from: 67, to: 73}, + {from: 67, to: 183}, + {from: 67, to: 184}, + {from: 67, to: 198}, + {from: 67, to: 204}, + {from: 67, to: 270}, + {from: 67, to: 302}, + {from: 67, to: 312}, + {from: 67, to: 497}, + {from: 67, to: 516}, + {from: 67, to: 524}, + {from: 67, to: 538}, + {from: 67, to: 633}, + {from: 67, to: 635}, + {from: 67, to: 636}, + {from: 67, to: 637}, + {from: 67, to: 684}, + {from: 67, to: 688}, + {from: 67, to: 697}, + {from: 67, to: 736}, + {from: 68, to: 73}, + {from: 68, to: 183}, + {from: 68, to: 184}, + {from: 68, to: 198}, + {from: 68, to: 204}, + {from: 68, to: 242}, + {from: 68, to: 270}, + {from: 68, to: 302}, + {from: 68, to: 312}, + {from: 68, to: 497}, + {from: 68, to: 516}, + {from: 68, to: 524}, + {from: 68, to: 538}, + {from: 68, to: 574}, + {from: 68, to: 633}, + {from: 68, to: 635}, + {from: 68, to: 636}, + {from: 68, to: 637}, + {from: 68, to: 684}, + {from: 68, to: 688}, + {from: 68, to: 697}, + {from: 68, to: 718}, + {from: 68, to: 736}, + {from: 69, to: 82}, + {from: 69, to: 193}, + {from: 69, to: 264}, + {from: 69, to: 281}, + {from: 69, to: 282}, + {from: 69, to: 285}, + {from: 69, to: 337}, + {from: 69, to: 374}, + {from: 69, to: 382}, + {from: 69, to: 387}, + {from: 69, to: 396}, + {from: 69, to: 438}, + {from: 69, to: 441}, + {from: 69, to: 465}, + {from: 69, to: 466}, + {from: 69, to: 491}, + {from: 69, to: 633}, + {from: 69, to: 646}, + {from: 69, to: 647}, + {from: 69, to: 650}, + {from: 69, to: 651}, + {from: 69, to: 689}, + {from: 69, to: 724}, + {from: 69, to: 725}, + {from: 69, to: 729}, + {from: 69, to: 730}, + {from: 69, to: 734}, + {from: 69, to: 735}, + {from: 70, to: 202}, + {from: 70, to: 211}, + {from: 70, to: 212}, + {from: 70, to: 214}, + {from: 70, to: 221}, + {from: 70, to: 225}, + {from: 70, to: 261}, + {from: 70, to: 287}, + {from: 70, to: 319}, + {from: 70, to: 358}, + {from: 70, to: 419}, + {from: 70, to: 424}, + {from: 70, to: 450}, + {from: 70, to: 451}, + {from: 70, to: 462}, + {from: 70, to: 487}, + {from: 70, to: 555}, + {from: 70, to: 600}, + {from: 70, to: 608}, + {from: 70, to: 642}, + {from: 70, to: 645}, + {from: 71, to: 83}, + {from: 71, to: 84}, + {from: 71, to: 107}, + {from: 71, to: 113}, + {from: 71, to: 146}, + {from: 71, to: 159}, + {from: 71, to: 182}, + {from: 71, to: 210}, + {from: 71, to: 217}, + {from: 71, to: 226}, + {from: 71, to: 278}, + {from: 71, to: 302}, + {from: 71, to: 321}, + {from: 71, to: 337}, + {from: 71, to: 407}, + {from: 71, to: 420}, + {from: 71, to: 488}, + {from: 71, to: 533}, + {from: 71, to: 579}, + {from: 71, to: 626}, + {from: 71, to: 627}, + {from: 71, to: 662}, + {from: 71, to: 705}, + {from: 71, to: 714}, + {from: 72, to: 75}, + {from: 72, to: 115}, + {from: 72, to: 190}, + {from: 72, to: 194}, + {from: 72, to: 200}, + {from: 72, to: 259}, + {from: 72, to: 342}, + {from: 72, to: 363}, + {from: 72, to: 379}, + {from: 72, to: 383}, + {from: 72, to: 403}, + {from: 72, to: 500}, + {from: 72, to: 505}, + {from: 72, to: 537}, + {from: 72, to: 574}, + {from: 72, to: 587}, + {from: 72, to: 597}, + {from: 72, to: 649}, + {from: 72, to: 691}, + {from: 72, to: 702}, + {from: 72, to: 706}, + {from: 73, to: 183}, + {from: 73, to: 184}, + {from: 73, to: 198}, + {from: 73, to: 204}, + {from: 73, to: 270}, + {from: 73, to: 302}, + {from: 73, to: 312}, + {from: 73, to: 497}, + {from: 73, to: 516}, + {from: 73, to: 524}, + {from: 73, to: 538}, + {from: 73, to: 633}, + {from: 73, to: 635}, + {from: 73, to: 636}, + {from: 73, to: 637}, + {from: 73, to: 684}, + {from: 73, to: 688}, + {from: 73, to: 697}, + {from: 73, to: 736}, + {from: 74, to: 140}, + {from: 74, to: 172}, + {from: 74, to: 177}, + {from: 74, to: 179}, + {from: 74, to: 289}, + {from: 74, to: 311}, + {from: 74, to: 384}, + {from: 74, to: 386}, + {from: 74, to: 408}, + {from: 74, to: 428}, + {from: 74, to: 460}, + {from: 74, to: 522}, + {from: 74, to: 527}, + {from: 74, to: 532}, + {from: 74, to: 540}, + {from: 74, to: 612}, + {from: 74, to: 625}, + {from: 74, to: 654}, + {from: 74, to: 667}, + {from: 74, to: 677}, + {from: 74, to: 679}, + {from: 74, to: 685}, + {from: 74, to: 704}, + {from: 74, to: 707}, + {from: 74, to: 732}, + {from: 75, to: 115}, + {from: 75, to: 190}, + {from: 75, to: 194}, + {from: 75, to: 200}, + {from: 75, to: 259}, + {from: 75, to: 342}, + {from: 75, to: 363}, + {from: 75, to: 379}, + {from: 75, to: 383}, + {from: 75, to: 403}, + {from: 75, to: 500}, + {from: 75, to: 505}, + {from: 75, to: 537}, + {from: 75, to: 574}, + {from: 75, to: 587}, + {from: 75, to: 597}, + {from: 75, to: 649}, + {from: 75, to: 691}, + {from: 75, to: 702}, + {from: 75, to: 706}, + {from: 76, to: 87}, + {from: 76, to: 196}, + {from: 76, to: 288}, + {from: 76, to: 303}, + {from: 76, to: 381}, + {from: 76, to: 409}, + {from: 76, to: 421}, + {from: 76, to: 425}, + {from: 76, to: 433}, + {from: 76, to: 440}, + {from: 76, to: 472}, + {from: 76, to: 473}, + {from: 76, to: 498}, + {from: 76, to: 508}, + {from: 76, to: 521}, + {from: 76, to: 523}, + {from: 76, to: 543}, + {from: 76, to: 562}, + {from: 76, to: 565}, + {from: 76, to: 573}, + {from: 76, to: 589}, + {from: 76, to: 594}, + {from: 76, to: 604}, + {from: 76, to: 629}, + {from: 76, to: 663}, + {from: 76, to: 679}, + {from: 76, to: 728}, + {from: 77, to: 81}, + {from: 77, to: 148}, + {from: 77, to: 208}, + {from: 77, to: 298}, + {from: 77, to: 307}, + {from: 77, to: 310}, + {from: 77, to: 313}, + {from: 77, to: 458}, + {from: 77, to: 459}, + {from: 77, to: 468}, + {from: 77, to: 470}, + {from: 77, to: 471}, + {from: 77, to: 477}, + {from: 77, to: 479}, + {from: 77, to: 515}, + {from: 77, to: 518}, + {from: 77, to: 541}, + {from: 77, to: 620}, + {from: 77, to: 680}, + {from: 77, to: 686}, + {from: 78, to: 137}, + {from: 78, to: 192}, + {from: 78, to: 247}, + {from: 78, to: 273}, + {from: 78, to: 284}, + {from: 78, to: 306}, + {from: 78, to: 315}, + {from: 78, to: 380}, + {from: 78, to: 389}, + {from: 78, to: 467}, + {from: 78, to: 495}, + {from: 78, to: 570}, + {from: 78, to: 584}, + {from: 78, to: 598}, + {from: 78, to: 599}, + {from: 78, to: 666}, + {from: 79, to: 82}, + {from: 79, to: 122}, + {from: 79, to: 140}, + {from: 79, to: 145}, + {from: 79, to: 203}, + {from: 79, to: 316}, + {from: 79, to: 327}, + {from: 79, to: 374}, + {from: 79, to: 375}, + {from: 79, to: 385}, + {from: 79, to: 433}, + {from: 79, to: 442}, + {from: 79, to: 454}, + {from: 79, to: 455}, + {from: 79, to: 475}, + {from: 79, to: 480}, + {from: 79, to: 498}, + {from: 79, to: 517}, + {from: 79, to: 573}, + {from: 79, to: 577}, + {from: 79, to: 611}, + {from: 79, to: 614}, + {from: 79, to: 623}, + {from: 79, to: 648}, + {from: 79, to: 678}, + {from: 79, to: 687}, + {from: 79, to: 721}, + {from: 80, to: 116}, + {from: 80, to: 139}, + {from: 80, to: 144}, + {from: 80, to: 167}, + {from: 80, to: 231}, + {from: 80, to: 232}, + {from: 80, to: 238}, + {from: 80, to: 258}, + {from: 80, to: 303}, + {from: 80, to: 308}, + {from: 80, to: 335}, + {from: 80, to: 348}, + {from: 80, to: 415}, + {from: 80, to: 434}, + {from: 80, to: 575}, + {from: 80, to: 576}, + {from: 80, to: 583}, + {from: 80, to: 603}, + {from: 80, to: 616}, + {from: 80, to: 668}, + {from: 80, to: 713}, + {from: 81, to: 148}, + {from: 81, to: 208}, + {from: 81, to: 298}, + {from: 81, to: 307}, + {from: 81, to: 310}, + {from: 81, to: 313}, + {from: 81, to: 458}, + {from: 81, to: 459}, + {from: 81, to: 468}, + {from: 81, to: 470}, + {from: 81, to: 471}, + {from: 81, to: 474}, + {from: 81, to: 477}, + {from: 81, to: 479}, + {from: 81, to: 515}, + {from: 81, to: 518}, + {from: 81, to: 541}, + {from: 81, to: 620}, + {from: 81, to: 680}, + {from: 81, to: 686}, + {from: 82, to: 122}, + {from: 82, to: 203}, + {from: 82, to: 327}, + {from: 82, to: 337}, + {from: 82, to: 374}, + {from: 82, to: 385}, + {from: 82, to: 387}, + {from: 82, to: 396}, + {from: 82, to: 433}, + {from: 82, to: 442}, + {from: 82, to: 454}, + {from: 82, to: 475}, + {from: 82, to: 480}, + {from: 82, to: 498}, + {from: 82, to: 517}, + {from: 82, to: 573}, + {from: 82, to: 577}, + {from: 82, to: 611}, + {from: 82, to: 614}, + {from: 82, to: 623}, + {from: 82, to: 633}, + {from: 82, to: 648}, + {from: 82, to: 678}, + {from: 82, to: 687}, + {from: 83, to: 84}, + {from: 83, to: 107}, + {from: 83, to: 113}, + {from: 83, to: 146}, + {from: 83, to: 182}, + {from: 83, to: 210}, + {from: 83, to: 217}, + {from: 83, to: 278}, + {from: 83, to: 321}, + {from: 83, to: 337}, + {from: 83, to: 407}, + {from: 83, to: 420}, + {from: 83, to: 488}, + {from: 83, to: 533}, + {from: 83, to: 579}, + {from: 83, to: 626}, + {from: 83, to: 627}, + {from: 83, to: 662}, + {from: 83, to: 705}, + {from: 84, to: 107}, + {from: 84, to: 113}, + {from: 84, to: 146}, + {from: 84, to: 182}, + {from: 84, to: 210}, + {from: 84, to: 217}, + {from: 84, to: 278}, + {from: 84, to: 321}, + {from: 84, to: 337}, + {from: 84, to: 407}, + {from: 84, to: 420}, + {from: 84, to: 488}, + {from: 84, to: 533}, + {from: 84, to: 579}, + {from: 84, to: 626}, + {from: 84, to: 627}, + {from: 84, to: 662}, + {from: 84, to: 705}, + {from: 84, to: 731}, + {from: 85, to: 135}, + {from: 85, to: 145}, + {from: 85, to: 147}, + {from: 85, to: 151}, + {from: 85, to: 187}, + {from: 85, to: 224}, + {from: 85, to: 233}, + {from: 85, to: 279}, + {from: 85, to: 280}, + {from: 85, to: 289}, + {from: 85, to: 323}, + {from: 85, to: 331}, + {from: 85, to: 376}, + {from: 85, to: 431}, + {from: 85, to: 436}, + {from: 85, to: 443}, + {from: 85, to: 490}, + {from: 85, to: 529}, + {from: 85, to: 547}, + {from: 85, to: 567}, + {from: 85, to: 586}, + {from: 85, to: 676}, + {from: 85, to: 699}, + {from: 85, to: 717}, + {from: 86, to: 93}, + {from: 86, to: 99}, + {from: 86, to: 131}, + {from: 86, to: 180}, + {from: 86, to: 188}, + {from: 86, to: 216}, + {from: 86, to: 277}, + {from: 86, to: 286}, + {from: 86, to: 300}, + {from: 86, to: 332}, + {from: 86, to: 333}, + {from: 86, to: 428}, + {from: 86, to: 511}, + {from: 86, to: 528}, + {from: 86, to: 571}, + {from: 86, to: 580}, + {from: 86, to: 593}, + {from: 86, to: 601}, + {from: 86, to: 618}, + {from: 86, to: 619}, + {from: 86, to: 652}, + {from: 86, to: 662}, + {from: 86, to: 703}, + {from: 86, to: 716}, + {from: 87, to: 185}, + {from: 87, to: 196}, + {from: 87, to: 212}, + {from: 87, to: 288}, + {from: 87, to: 381}, + {from: 87, to: 409}, + {from: 87, to: 421}, + {from: 87, to: 425}, + {from: 87, to: 440}, + {from: 87, to: 449}, + {from: 87, to: 472}, + {from: 87, to: 473}, + {from: 87, to: 490}, + {from: 87, to: 508}, + {from: 87, to: 521}, + {from: 87, to: 523}, + {from: 87, to: 543}, + {from: 87, to: 562}, + {from: 87, to: 565}, + {from: 87, to: 589}, + {from: 87, to: 594}, + {from: 87, to: 604}, + {from: 87, to: 622}, + {from: 87, to: 663}, + {from: 87, to: 675}, + {from: 87, to: 676}, + {from: 87, to: 728}, + {from: 88, to: 162}, + {from: 88, to: 177}, + {from: 88, to: 215}, + {from: 88, to: 218}, + {from: 88, to: 221}, + {from: 88, to: 241}, + {from: 88, to: 260}, + {from: 88, to: 261}, + {from: 88, to: 266}, + {from: 88, to: 271}, + {from: 88, to: 279}, + {from: 88, to: 339}, + {from: 88, to: 364}, + {from: 88, to: 366}, + {from: 88, to: 422}, + {from: 88, to: 453}, + {from: 88, to: 504}, + {from: 88, to: 572}, + {from: 88, to: 578}, + {from: 88, to: 591}, + {from: 88, to: 596}, + {from: 88, to: 602}, + {from: 88, to: 610}, + {from: 88, to: 661}, + {from: 88, to: 665}, + {from: 88, to: 690}, + {from: 88, to: 692}, + {from: 88, to: 693}, + {from: 88, to: 721}, + {from: 88, to: 723}, + {from: 89, to: 114}, + {from: 89, to: 127}, + {from: 89, to: 159}, + {from: 89, to: 160}, + {from: 89, to: 161}, + {from: 89, to: 246}, + {from: 89, to: 257}, + {from: 89, to: 297}, + {from: 89, to: 322}, + {from: 89, to: 398}, + {from: 89, to: 474}, + {from: 89, to: 485}, + {from: 89, to: 553}, + {from: 89, to: 621}, + {from: 89, to: 632}, + {from: 89, to: 638}, + {from: 89, to: 639}, + {from: 89, to: 657}, + {from: 89, to: 671}, + {from: 89, to: 715}, + {from: 89, to: 726}, + {from: 90, to: 91}, + {from: 90, to: 117}, + {from: 90, to: 126}, + {from: 90, to: 134}, + {from: 90, to: 156}, + {from: 90, to: 213}, + {from: 90, to: 242}, + {from: 90, to: 265}, + {from: 90, to: 326}, + {from: 90, to: 341}, + {from: 90, to: 365}, + {from: 90, to: 375}, + {from: 90, to: 406}, + {from: 90, to: 476}, + {from: 90, to: 502}, + {from: 90, to: 513}, + {from: 90, to: 530}, + {from: 90, to: 544}, + {from: 90, to: 549}, + {from: 90, to: 681}, + {from: 90, to: 683}, + {from: 91, to: 117}, + {from: 91, to: 126}, + {from: 91, to: 134}, + {from: 91, to: 156}, + {from: 91, to: 213}, + {from: 91, to: 242}, + {from: 91, to: 265}, + {from: 91, to: 326}, + {from: 91, to: 341}, + {from: 91, to: 365}, + {from: 91, to: 375}, + {from: 91, to: 406}, + {from: 91, to: 476}, + {from: 91, to: 502}, + {from: 91, to: 513}, + {from: 91, to: 530}, + {from: 91, to: 544}, + {from: 91, to: 613}, + {from: 91, to: 681}, + {from: 91, to: 683}, + {from: 92, to: 98}, + {from: 92, to: 176}, + {from: 92, to: 178}, + {from: 92, to: 197}, + {from: 92, to: 328}, + {from: 92, to: 329}, + {from: 92, to: 351}, + {from: 92, to: 367}, + {from: 92, to: 372}, + {from: 92, to: 426}, + {from: 92, to: 427}, + {from: 92, to: 456}, + {from: 92, to: 464}, + {from: 92, to: 492}, + {from: 92, to: 536}, + {from: 92, to: 549}, + {from: 92, to: 551}, + {from: 92, to: 609}, + {from: 92, to: 615}, + {from: 92, to: 700}, + {from: 92, to: 718}, + {from: 93, to: 131}, + {from: 93, to: 171}, + {from: 93, to: 180}, + {from: 93, to: 188}, + {from: 93, to: 200}, + {from: 93, to: 216}, + {from: 93, to: 277}, + {from: 93, to: 286}, + {from: 93, to: 332}, + {from: 93, to: 333}, + {from: 93, to: 428}, + {from: 93, to: 511}, + {from: 93, to: 528}, + {from: 93, to: 571}, + {from: 93, to: 579}, + {from: 93, to: 580}, + {from: 93, to: 593}, + {from: 93, to: 601}, + {from: 93, to: 618}, + {from: 93, to: 619}, + {from: 93, to: 652}, + {from: 93, to: 703}, + {from: 93, to: 716}, + {from: 94, to: 133}, + {from: 94, to: 149}, + {from: 94, to: 171}, + {from: 94, to: 244}, + {from: 94, to: 314}, + {from: 94, to: 325}, + {from: 94, to: 338}, + {from: 94, to: 345}, + {from: 94, to: 350}, + {from: 94, to: 396}, + {from: 94, to: 417}, + {from: 94, to: 496}, + {from: 94, to: 507}, + {from: 94, to: 534}, + {from: 94, to: 566}, + {from: 94, to: 606}, + {from: 94, to: 613}, + {from: 94, to: 659}, + {from: 94, to: 673}, + {from: 94, to: 682}, + {from: 94, to: 714}, + {from: 95, to: 109}, + {from: 95, to: 119}, + {from: 95, to: 128}, + {from: 95, to: 136}, + {from: 95, to: 152}, + {from: 95, to: 163}, + {from: 95, to: 206}, + {from: 95, to: 244}, + {from: 95, to: 248}, + {from: 95, to: 336}, + {from: 95, to: 343}, + {from: 95, to: 360}, + {from: 95, to: 378}, + {from: 95, to: 388}, + {from: 95, to: 448}, + {from: 95, to: 496}, + {from: 95, to: 501}, + {from: 95, to: 506}, + {from: 95, to: 550}, + {from: 95, to: 563}, + {from: 95, to: 588}, + {from: 95, to: 617}, + {from: 95, to: 630}, + {from: 95, to: 712}, + {from: 95, to: 727}, + {from: 96, to: 99}, + {from: 96, to: 100}, + {from: 96, to: 105}, + {from: 96, to: 106}, + {from: 96, to: 130}, + {from: 96, to: 153}, + {from: 96, to: 181}, + {from: 96, to: 186}, + {from: 96, to: 219}, + {from: 96, to: 234}, + {from: 96, to: 304}, + {from: 96, to: 309}, + {from: 96, to: 366}, + {from: 96, to: 369}, + {from: 96, to: 370}, + {from: 96, to: 457}, + {from: 96, to: 554}, + {from: 96, to: 630}, + {from: 96, to: 672}, + {from: 96, to: 701}, + {from: 97, to: 108}, + {from: 97, to: 173}, + {from: 97, to: 195}, + {from: 97, to: 205}, + {from: 97, to: 218}, + {from: 97, to: 274}, + {from: 97, to: 296}, + {from: 97, to: 418}, + {from: 97, to: 435}, + {from: 97, to: 437}, + {from: 97, to: 493}, + {from: 97, to: 494}, + {from: 97, to: 519}, + {from: 97, to: 525}, + {from: 97, to: 526}, + {from: 97, to: 582}, + {from: 97, to: 585}, + {from: 97, to: 605}, + {from: 97, to: 631}, + {from: 97, to: 655}, + {from: 97, to: 722}, + {from: 98, to: 176}, + {from: 98, to: 178}, + {from: 98, to: 197}, + {from: 98, to: 328}, + {from: 98, to: 329}, + {from: 98, to: 351}, + {from: 98, to: 367}, + {from: 98, to: 372}, + {from: 98, to: 426}, + {from: 98, to: 427}, + {from: 98, to: 456}, + {from: 98, to: 464}, + {from: 98, to: 492}, + {from: 98, to: 536}, + {from: 98, to: 549}, + {from: 98, to: 551}, + {from: 98, to: 609}, + {from: 98, to: 615}, + {from: 98, to: 700}, + {from: 98, to: 718}, + {from: 99, to: 100}, + {from: 99, to: 105}, + {from: 99, to: 106}, + {from: 99, to: 130}, + {from: 99, to: 153}, + {from: 99, to: 181}, + {from: 99, to: 219}, + {from: 99, to: 234}, + {from: 99, to: 300}, + {from: 99, to: 304}, + {from: 99, to: 309}, + {from: 99, to: 366}, + {from: 99, to: 369}, + {from: 99, to: 370}, + {from: 99, to: 457}, + {from: 99, to: 554}, + {from: 99, to: 630}, + {from: 99, to: 662}, + {from: 99, to: 672}, + {from: 99, to: 701}, + {from: 100, to: 105}, + {from: 100, to: 106}, + {from: 100, to: 130}, + {from: 100, to: 153}, + {from: 100, to: 181}, + {from: 100, to: 219}, + {from: 100, to: 234}, + {from: 100, to: 304}, + {from: 100, to: 309}, + {from: 100, to: 366}, + {from: 100, to: 369}, + {from: 100, to: 370}, + {from: 100, to: 457}, + {from: 100, to: 554}, + {from: 100, to: 630}, + {from: 100, to: 672}, + {from: 100, to: 701}, + {from: 101, to: 112}, + {from: 101, to: 124}, + {from: 101, to: 132}, + {from: 101, to: 189}, + {from: 101, to: 207}, + {from: 101, to: 209}, + {from: 101, to: 214}, + {from: 101, to: 223}, + {from: 101, to: 230}, + {from: 101, to: 239}, + {from: 101, to: 262}, + {from: 101, to: 320}, + {from: 101, to: 344}, + {from: 101, to: 354}, + {from: 101, to: 361}, + {from: 101, to: 362}, + {from: 101, to: 445}, + {from: 101, to: 457}, + {from: 101, to: 483}, + {from: 101, to: 484}, + {from: 101, to: 512}, + {from: 102, to: 120}, + {from: 102, to: 186}, + {from: 102, to: 201}, + {from: 102, to: 222}, + {from: 102, to: 228}, + {from: 102, to: 235}, + {from: 102, to: 236}, + {from: 102, to: 264}, + {from: 102, to: 305}, + {from: 102, to: 324}, + {from: 102, to: 334}, + {from: 102, to: 353}, + {from: 102, to: 368}, + {from: 102, to: 429}, + {from: 102, to: 489}, + {from: 102, to: 499}, + {from: 102, to: 548}, + {from: 102, to: 552}, + {from: 102, to: 595}, + {from: 102, to: 697}, + {from: 102, to: 710}, + {from: 103, to: 104}, + {from: 103, to: 169}, + {from: 103, to: 229}, + {from: 103, to: 256}, + {from: 103, to: 267}, + {from: 103, to: 275}, + {from: 103, to: 276}, + {from: 103, to: 295}, + {from: 103, to: 317}, + {from: 103, to: 318}, + {from: 103, to: 355}, + {from: 103, to: 357}, + {from: 103, to: 446}, + {from: 103, to: 509}, + {from: 103, to: 510}, + {from: 103, to: 546}, + {from: 103, to: 564}, + {from: 103, to: 581}, + {from: 103, to: 592}, + {from: 104, to: 169}, + {from: 104, to: 229}, + {from: 104, to: 256}, + {from: 104, to: 267}, + {from: 104, to: 275}, + {from: 104, to: 276}, + {from: 104, to: 295}, + {from: 104, to: 317}, + {from: 104, to: 318}, + {from: 104, to: 355}, + {from: 104, to: 357}, + {from: 104, to: 446}, + {from: 104, to: 509}, + {from: 104, to: 510}, + {from: 104, to: 546}, + {from: 104, to: 564}, + {from: 104, to: 581}, + {from: 104, to: 592}, + {from: 105, to: 106}, + {from: 105, to: 130}, + {from: 105, to: 153}, + {from: 105, to: 181}, + {from: 105, to: 219}, + {from: 105, to: 234}, + {from: 105, to: 304}, + {from: 105, to: 309}, + {from: 105, to: 366}, + {from: 105, to: 369}, + {from: 105, to: 370}, + {from: 105, to: 457}, + {from: 105, to: 554}, + {from: 105, to: 630}, + {from: 105, to: 672}, + {from: 105, to: 701}, + {from: 106, to: 130}, + {from: 106, to: 153}, + {from: 106, to: 181}, + {from: 106, to: 219}, + {from: 106, to: 234}, + {from: 106, to: 304}, + {from: 106, to: 309}, + {from: 106, to: 366}, + {from: 106, to: 369}, + {from: 106, to: 370}, + {from: 106, to: 457}, + {from: 106, to: 554}, + {from: 106, to: 630}, + {from: 106, to: 672}, + {from: 106, to: 701}, + {from: 107, to: 113}, + {from: 107, to: 146}, + {from: 107, to: 182}, + {from: 107, to: 210}, + {from: 107, to: 217}, + {from: 107, to: 278}, + {from: 107, to: 321}, + {from: 107, to: 337}, + {from: 107, to: 407}, + {from: 107, to: 420}, + {from: 107, to: 488}, + {from: 107, to: 533}, + {from: 107, to: 579}, + {from: 107, to: 626}, + {from: 107, to: 627}, + {from: 107, to: 662}, + {from: 107, to: 705}, + {from: 108, to: 173}, + {from: 108, to: 195}, + {from: 108, to: 205}, + {from: 108, to: 218}, + {from: 108, to: 274}, + {from: 108, to: 296}, + {from: 108, to: 418}, + {from: 108, to: 435}, + {from: 108, to: 493}, + {from: 108, to: 494}, + {from: 108, to: 519}, + {from: 108, to: 525}, + {from: 108, to: 526}, + {from: 108, to: 582}, + {from: 108, to: 585}, + {from: 108, to: 605}, + {from: 108, to: 631}, + {from: 108, to: 655}, + {from: 108, to: 722}, + {from: 109, to: 119}, + {from: 109, to: 128}, + {from: 109, to: 136}, + {from: 109, to: 152}, + {from: 109, to: 163}, + {from: 109, to: 206}, + {from: 109, to: 248}, + {from: 109, to: 336}, + {from: 109, to: 343}, + {from: 109, to: 360}, + {from: 109, to: 378}, + {from: 109, to: 388}, + {from: 109, to: 448}, + {from: 109, to: 501}, + {from: 109, to: 506}, + {from: 109, to: 550}, + {from: 109, to: 563}, + {from: 109, to: 588}, + {from: 109, to: 617}, + {from: 109, to: 712}, + {from: 109, to: 727}, + {from: 110, to: 111}, + {from: 110, to: 150}, + {from: 110, to: 151}, + {from: 110, to: 154}, + {from: 110, to: 155}, + {from: 110, to: 164}, + {from: 110, to: 179}, + {from: 110, to: 227}, + {from: 110, to: 231}, + {from: 110, to: 238}, + {from: 110, to: 245}, + {from: 110, to: 294}, + {from: 110, to: 316}, + {from: 110, to: 342}, + {from: 110, to: 349}, + {from: 110, to: 371}, + {from: 110, to: 373}, + {from: 110, to: 397}, + {from: 110, to: 547}, + {from: 110, to: 569}, + {from: 110, to: 572}, + {from: 110, to: 586}, + {from: 110, to: 591}, + {from: 110, to: 627}, + {from: 110, to: 629}, + {from: 110, to: 643}, + {from: 110, to: 644}, + {from: 110, to: 717}, + {from: 110, to: 719}, + {from: 110, to: 720}, + {from: 111, to: 135}, + {from: 111, to: 150}, + {from: 111, to: 154}, + {from: 111, to: 155}, + {from: 111, to: 164}, + {from: 111, to: 227}, + {from: 111, to: 245}, + {from: 111, to: 294}, + {from: 111, to: 316}, + {from: 111, to: 319}, + {from: 111, to: 349}, + {from: 111, to: 368}, + {from: 111, to: 371}, + {from: 111, to: 373}, + {from: 111, to: 397}, + {from: 111, to: 419}, + {from: 111, to: 429}, + {from: 111, to: 489}, + {from: 111, to: 529}, + {from: 111, to: 569}, + {from: 111, to: 572}, + {from: 111, to: 591}, + {from: 111, to: 629}, + {from: 111, to: 643}, + {from: 111, to: 644}, + {from: 111, to: 719}, + {from: 111, to: 720}, + {from: 112, to: 124}, + {from: 112, to: 132}, + {from: 112, to: 189}, + {from: 112, to: 207}, + {from: 112, to: 209}, + {from: 112, to: 214}, + {from: 112, to: 223}, + {from: 112, to: 230}, + {from: 112, to: 239}, + {from: 112, to: 262}, + {from: 112, to: 320}, + {from: 112, to: 344}, + {from: 112, to: 354}, + {from: 112, to: 361}, + {from: 112, to: 362}, + {from: 112, to: 445}, + {from: 112, to: 483}, + {from: 112, to: 484}, + {from: 112, to: 512}, + {from: 113, to: 146}, + {from: 113, to: 182}, + {from: 113, to: 210}, + {from: 113, to: 217}, + {from: 113, to: 278}, + {from: 113, to: 321}, + {from: 113, to: 337}, + {from: 113, to: 407}, + {from: 113, to: 420}, + {from: 113, to: 488}, + {from: 113, to: 533}, + {from: 113, to: 579}, + {from: 113, to: 626}, + {from: 113, to: 627}, + {from: 113, to: 662}, + {from: 113, to: 674}, + {from: 113, to: 705}, + {from: 114, to: 127}, + {from: 114, to: 159}, + {from: 114, to: 160}, + {from: 114, to: 161}, + {from: 114, to: 246}, + {from: 114, to: 257}, + {from: 114, to: 297}, + {from: 114, to: 322}, + {from: 114, to: 398}, + {from: 114, to: 421}, + {from: 114, to: 472}, + {from: 114, to: 474}, + {from: 114, to: 485}, + {from: 114, to: 523}, + {from: 114, to: 553}, + {from: 114, to: 621}, + {from: 114, to: 632}, + {from: 114, to: 638}, + {from: 114, to: 639}, + {from: 114, to: 649}, + {from: 114, to: 657}, + {from: 114, to: 671}, + {from: 114, to: 682}, + {from: 114, to: 715}, + {from: 114, to: 726}, + {from: 115, to: 190}, + {from: 115, to: 194}, + {from: 115, to: 200}, + {from: 115, to: 259}, + {from: 115, to: 342}, + {from: 115, to: 363}, + {from: 115, to: 379}, + {from: 115, to: 383}, + {from: 115, to: 403}, + {from: 115, to: 500}, + {from: 115, to: 505}, + {from: 115, to: 537}, + {from: 115, to: 574}, + {from: 115, to: 587}, + {from: 115, to: 597}, + {from: 115, to: 649}, + {from: 115, to: 691}, + {from: 115, to: 702}, + {from: 115, to: 706}, + {from: 116, to: 139}, + {from: 116, to: 144}, + {from: 116, to: 150}, + {from: 116, to: 231}, + {from: 116, to: 232}, + {from: 116, to: 238}, + {from: 116, to: 258}, + {from: 116, to: 303}, + {from: 116, to: 308}, + {from: 116, to: 318}, + {from: 116, to: 335}, + {from: 116, to: 348}, + {from: 116, to: 371}, + {from: 116, to: 415}, + {from: 116, to: 434}, + {from: 116, to: 460}, + {from: 116, to: 528}, + {from: 116, to: 562}, + {from: 116, to: 575}, + {from: 116, to: 576}, + {from: 116, to: 583}, + {from: 116, to: 603}, + {from: 116, to: 606}, + {from: 116, to: 616}, + {from: 116, to: 646}, + {from: 116, to: 668}, + {from: 116, to: 713}, + {from: 117, to: 126}, + {from: 117, to: 134}, + {from: 117, to: 156}, + {from: 117, to: 213}, + {from: 117, to: 242}, + {from: 117, to: 265}, + {from: 117, to: 326}, + {from: 117, to: 341}, + {from: 117, to: 365}, + {from: 117, to: 375}, + {from: 117, to: 406}, + {from: 117, to: 476}, + {from: 117, to: 502}, + {from: 117, to: 513}, + {from: 117, to: 530}, + {from: 117, to: 544}, + {from: 117, to: 681}, + {from: 117, to: 683}, + {from: 117, to: 700}, + {from: 118, to: 138}, + {from: 118, to: 199}, + {from: 118, to: 220}, + {from: 118, to: 272}, + {from: 118, to: 340}, + {from: 118, to: 346}, + {from: 118, to: 347}, + {from: 118, to: 387}, + {from: 118, to: 404}, + {from: 118, to: 437}, + {from: 118, to: 503}, + {from: 118, to: 520}, + {from: 118, to: 590}, + {from: 118, to: 595}, + {from: 118, to: 628}, + {from: 118, to: 664}, + {from: 118, to: 670}, + {from: 118, to: 709}, + {from: 119, to: 128}, + {from: 119, to: 136}, + {from: 119, to: 152}, + {from: 119, to: 163}, + {from: 119, to: 206}, + {from: 119, to: 248}, + {from: 119, to: 336}, + {from: 119, to: 343}, + {from: 119, to: 360}, + {from: 119, to: 378}, + {from: 119, to: 388}, + {from: 119, to: 448}, + {from: 119, to: 501}, + {from: 119, to: 506}, + {from: 119, to: 550}, + {from: 119, to: 563}, + {from: 119, to: 588}, + {from: 119, to: 617}, + {from: 119, to: 712}, + {from: 119, to: 727}, + {from: 120, to: 186}, + {from: 120, to: 201}, + {from: 120, to: 222}, + {from: 120, to: 228}, + {from: 120, to: 235}, + {from: 120, to: 236}, + {from: 120, to: 293}, + {from: 120, to: 305}, + {from: 120, to: 324}, + {from: 120, to: 334}, + {from: 120, to: 353}, + {from: 120, to: 368}, + {from: 120, to: 429}, + {from: 120, to: 489}, + {from: 120, to: 499}, + {from: 120, to: 548}, + {from: 120, to: 552}, + {from: 120, to: 595}, + {from: 120, to: 708}, + {from: 120, to: 710}, + {from: 120, to: 733}, + {from: 121, to: 129}, + {from: 121, to: 165}, + {from: 121, to: 166}, + {from: 121, to: 167}, + {from: 121, to: 168}, + {from: 121, to: 185}, + {from: 121, to: 191}, + {from: 121, to: 226}, + {from: 121, to: 240}, + {from: 121, to: 276}, + {from: 121, to: 352}, + {from: 121, to: 359}, + {from: 121, to: 430}, + {from: 121, to: 461}, + {from: 121, to: 463}, + {from: 121, to: 486}, + {from: 121, to: 531}, + {from: 121, to: 607}, + {from: 121, to: 634}, + {from: 121, to: 711}, + {from: 122, to: 203}, + {from: 122, to: 266}, + {from: 122, to: 327}, + {from: 122, to: 374}, + {from: 122, to: 385}, + {from: 122, to: 433}, + {from: 122, to: 442}, + {from: 122, to: 454}, + {from: 122, to: 475}, + {from: 122, to: 480}, + {from: 122, to: 498}, + {from: 122, to: 517}, + {from: 122, to: 573}, + {from: 122, to: 577}, + {from: 122, to: 611}, + {from: 122, to: 614}, + {from: 122, to: 623}, + {from: 122, to: 648}, + {from: 122, to: 678}, + {from: 122, to: 687}, + {from: 123, to: 125}, + {from: 123, to: 141}, + {from: 123, to: 237}, + {from: 123, to: 249}, + {from: 123, to: 252}, + {from: 123, to: 291}, + {from: 123, to: 416}, + {from: 123, to: 422}, + {from: 123, to: 447}, + {from: 123, to: 449}, + {from: 123, to: 452}, + {from: 123, to: 478}, + {from: 123, to: 481}, + {from: 123, to: 482}, + {from: 123, to: 558}, + {from: 123, to: 622}, + {from: 123, to: 675}, + {from: 124, to: 132}, + {from: 124, to: 189}, + {from: 124, to: 207}, + {from: 124, to: 209}, + {from: 124, to: 214}, + {from: 124, to: 223}, + {from: 124, to: 230}, + {from: 124, to: 239}, + {from: 124, to: 262}, + {from: 124, to: 271}, + {from: 124, to: 320}, + {from: 124, to: 344}, + {from: 124, to: 354}, + {from: 124, to: 361}, + {from: 124, to: 362}, + {from: 124, to: 445}, + {from: 124, to: 483}, + {from: 124, to: 484}, + {from: 124, to: 512}, + {from: 124, to: 615}, + {from: 125, to: 141}, + {from: 125, to: 237}, + {from: 125, to: 249}, + {from: 125, to: 252}, + {from: 125, to: 291}, + {from: 125, to: 404}, + {from: 125, to: 416}, + {from: 125, to: 422}, + {from: 125, to: 447}, + {from: 125, to: 449}, + {from: 125, to: 452}, + {from: 125, to: 461}, + {from: 125, to: 478}, + {from: 125, to: 481}, + {from: 125, to: 482}, + {from: 125, to: 483}, + {from: 125, to: 565}, + {from: 125, to: 622}, + {from: 125, to: 661}, + {from: 125, to: 675}, + {from: 126, to: 134}, + {from: 126, to: 156}, + {from: 126, to: 213}, + {from: 126, to: 242}, + {from: 126, to: 265}, + {from: 126, to: 326}, + {from: 126, to: 341}, + {from: 126, to: 365}, + {from: 126, to: 375}, + {from: 126, to: 406}, + {from: 126, to: 476}, + {from: 126, to: 502}, + {from: 126, to: 513}, + {from: 126, to: 530}, + {from: 126, to: 544}, + {from: 126, to: 681}, + {from: 126, to: 683}, + {from: 127, to: 159}, + {from: 127, to: 160}, + {from: 127, to: 161}, + {from: 127, to: 246}, + {from: 127, to: 257}, + {from: 127, to: 297}, + {from: 127, to: 322}, + {from: 127, to: 398}, + {from: 127, to: 474}, + {from: 127, to: 485}, + {from: 127, to: 553}, + {from: 127, to: 621}, + {from: 127, to: 632}, + {from: 127, to: 638}, + {from: 127, to: 639}, + {from: 127, to: 657}, + {from: 127, to: 671}, + {from: 127, to: 690}, + {from: 127, to: 715}, + {from: 127, to: 726}, + {from: 128, to: 136}, + {from: 128, to: 152}, + {from: 128, to: 163}, + {from: 128, to: 206}, + {from: 128, to: 248}, + {from: 128, to: 336}, + {from: 128, to: 343}, + {from: 128, to: 360}, + {from: 128, to: 378}, + {from: 128, to: 388}, + {from: 128, to: 448}, + {from: 128, to: 501}, + {from: 128, to: 506}, + {from: 128, to: 513}, + {from: 128, to: 550}, + {from: 128, to: 563}, + {from: 128, to: 588}, + {from: 128, to: 617}, + {from: 128, to: 712}, + {from: 128, to: 727}, + {from: 129, to: 155}, + {from: 129, to: 164}, + {from: 129, to: 165}, + {from: 129, to: 166}, + {from: 129, to: 167}, + {from: 129, to: 168}, + {from: 129, to: 185}, + {from: 129, to: 191}, + {from: 129, to: 226}, + {from: 129, to: 240}, + {from: 129, to: 352}, + {from: 129, to: 359}, + {from: 129, to: 373}, + {from: 129, to: 397}, + {from: 129, to: 430}, + {from: 129, to: 461}, + {from: 129, to: 463}, + {from: 129, to: 486}, + {from: 129, to: 531}, + {from: 129, to: 607}, + {from: 129, to: 634}, + {from: 129, to: 677}, + {from: 129, to: 685}, + {from: 129, to: 711}, + {from: 130, to: 153}, + {from: 130, to: 181}, + {from: 130, to: 219}, + {from: 130, to: 234}, + {from: 130, to: 291}, + {from: 130, to: 304}, + {from: 130, to: 309}, + {from: 130, to: 366}, + {from: 130, to: 369}, + {from: 130, to: 370}, + {from: 130, to: 382}, + {from: 130, to: 452}, + {from: 130, to: 457}, + {from: 130, to: 481}, + {from: 130, to: 503}, + {from: 130, to: 534}, + {from: 130, to: 554}, + {from: 130, to: 630}, + {from: 130, to: 670}, + {from: 130, to: 672}, + {from: 130, to: 701}, + {from: 131, to: 180}, + {from: 131, to: 188}, + {from: 131, to: 216}, + {from: 131, to: 277}, + {from: 131, to: 286}, + {from: 131, to: 294}, + {from: 131, to: 332}, + {from: 131, to: 333}, + {from: 131, to: 381}, + {from: 131, to: 428}, + {from: 131, to: 432}, + {from: 131, to: 443}, + {from: 131, to: 511}, + {from: 131, to: 528}, + {from: 131, to: 571}, + {from: 131, to: 580}, + {from: 131, to: 589}, + {from: 131, to: 593}, + {from: 131, to: 601}, + {from: 131, to: 618}, + {from: 131, to: 619}, + {from: 131, to: 623}, + {from: 131, to: 644}, + {from: 131, to: 652}, + {from: 131, to: 703}, + {from: 131, to: 716}, + {from: 131, to: 719}, + {from: 132, to: 189}, + {from: 132, to: 207}, + {from: 132, to: 209}, + {from: 132, to: 214}, + {from: 132, to: 223}, + {from: 132, to: 230}, + {from: 132, to: 239}, + {from: 132, to: 262}, + {from: 132, to: 320}, + {from: 132, to: 344}, + {from: 132, to: 354}, + {from: 132, to: 361}, + {from: 132, to: 362}, + {from: 132, to: 445}, + {from: 132, to: 483}, + {from: 132, to: 484}, + {from: 132, to: 512}, + {from: 133, to: 149}, + {from: 133, to: 171}, + {from: 133, to: 244}, + {from: 133, to: 314}, + {from: 133, to: 325}, + {from: 133, to: 338}, + {from: 133, to: 345}, + {from: 133, to: 350}, + {from: 133, to: 396}, + {from: 133, to: 417}, + {from: 133, to: 496}, + {from: 133, to: 507}, + {from: 133, to: 534}, + {from: 133, to: 566}, + {from: 133, to: 606}, + {from: 133, to: 613}, + {from: 133, to: 659}, + {from: 133, to: 673}, + {from: 133, to: 682}, + {from: 133, to: 714}, + {from: 134, to: 156}, + {from: 134, to: 213}, + {from: 134, to: 242}, + {from: 134, to: 265}, + {from: 134, to: 326}, + {from: 134, to: 341}, + {from: 134, to: 365}, + {from: 134, to: 375}, + {from: 134, to: 406}, + {from: 134, to: 476}, + {from: 134, to: 502}, + {from: 134, to: 513}, + {from: 134, to: 530}, + {from: 134, to: 544}, + {from: 134, to: 681}, + {from: 134, to: 683}, + {from: 135, to: 145}, + {from: 135, to: 151}, + {from: 135, to: 224}, + {from: 135, to: 233}, + {from: 135, to: 245}, + {from: 135, to: 279}, + {from: 135, to: 280}, + {from: 135, to: 289}, + {from: 135, to: 319}, + {from: 135, to: 323}, + {from: 135, to: 331}, + {from: 135, to: 349}, + {from: 135, to: 368}, + {from: 135, to: 376}, + {from: 135, to: 419}, + {from: 135, to: 429}, + {from: 135, to: 431}, + {from: 135, to: 436}, + {from: 135, to: 443}, + {from: 135, to: 489}, + {from: 135, to: 490}, + {from: 135, to: 529}, + {from: 135, to: 547}, + {from: 135, to: 567}, + {from: 135, to: 569}, + {from: 135, to: 586}, + {from: 135, to: 643}, + {from: 135, to: 676}, + {from: 135, to: 699}, + {from: 135, to: 717}, + {from: 135, to: 720}, + {from: 136, to: 152}, + {from: 136, to: 163}, + {from: 136, to: 206}, + {from: 136, to: 248}, + {from: 136, to: 336}, + {from: 136, to: 343}, + {from: 136, to: 360}, + {from: 136, to: 378}, + {from: 136, to: 388}, + {from: 136, to: 448}, + {from: 136, to: 501}, + {from: 136, to: 506}, + {from: 136, to: 550}, + {from: 136, to: 563}, + {from: 136, to: 588}, + {from: 136, to: 617}, + {from: 136, to: 712}, + {from: 136, to: 727}, + {from: 137, to: 192}, + {from: 137, to: 204}, + {from: 137, to: 247}, + {from: 137, to: 273}, + {from: 137, to: 284}, + {from: 137, to: 306}, + {from: 137, to: 315}, + {from: 137, to: 380}, + {from: 137, to: 389}, + {from: 137, to: 467}, + {from: 137, to: 495}, + {from: 137, to: 570}, + {from: 137, to: 584}, + {from: 137, to: 598}, + {from: 137, to: 599}, + {from: 137, to: 666}, + {from: 138, to: 193}, + {from: 138, to: 199}, + {from: 138, to: 220}, + {from: 138, to: 272}, + {from: 138, to: 340}, + {from: 138, to: 346}, + {from: 138, to: 347}, + {from: 138, to: 387}, + {from: 138, to: 404}, + {from: 138, to: 408}, + {from: 138, to: 437}, + {from: 138, to: 503}, + {from: 138, to: 520}, + {from: 138, to: 590}, + {from: 138, to: 628}, + {from: 138, to: 664}, + {from: 138, to: 670}, + {from: 138, to: 709}, + {from: 139, to: 144}, + {from: 139, to: 231}, + {from: 139, to: 232}, + {from: 139, to: 238}, + {from: 139, to: 258}, + {from: 139, to: 303}, + {from: 139, to: 308}, + {from: 139, to: 335}, + {from: 139, to: 348}, + {from: 139, to: 398}, + {from: 139, to: 415}, + {from: 139, to: 430}, + {from: 139, to: 434}, + {from: 139, to: 440}, + {from: 139, to: 575}, + {from: 139, to: 576}, + {from: 139, to: 583}, + {from: 139, to: 603}, + {from: 139, to: 616}, + {from: 139, to: 654}, + {from: 139, to: 668}, + {from: 139, to: 702}, + {from: 139, to: 713}, + {from: 140, to: 145}, + {from: 140, to: 172}, + {from: 140, to: 177}, + {from: 140, to: 179}, + {from: 140, to: 311}, + {from: 140, to: 316}, + {from: 140, to: 327}, + {from: 140, to: 375}, + {from: 140, to: 384}, + {from: 140, to: 386}, + {from: 140, to: 408}, + {from: 140, to: 442}, + {from: 140, to: 454}, + {from: 140, to: 455}, + {from: 140, to: 460}, + {from: 140, to: 522}, + {from: 140, to: 527}, + {from: 140, to: 532}, + {from: 140, to: 577}, + {from: 140, to: 612}, + {from: 140, to: 625}, + {from: 140, to: 654}, + {from: 140, to: 667}, + {from: 140, to: 677}, + {from: 140, to: 678}, + {from: 140, to: 679}, + {from: 140, to: 685}, + {from: 140, to: 687}, + {from: 140, to: 707}, + {from: 140, to: 721}, + {from: 141, to: 237}, + {from: 141, to: 246}, + {from: 141, to: 249}, + {from: 141, to: 252}, + {from: 141, to: 291}, + {from: 141, to: 416}, + {from: 141, to: 422}, + {from: 141, to: 436}, + {from: 141, to: 447}, + {from: 141, to: 449}, + {from: 141, to: 452}, + {from: 141, to: 478}, + {from: 141, to: 481}, + {from: 141, to: 482}, + {from: 141, to: 516}, + {from: 141, to: 622}, + {from: 141, to: 675}, + {from: 141, to: 696}, + {from: 142, to: 143}, + {from: 142, to: 147}, + {from: 142, to: 157}, + {from: 142, to: 175}, + {from: 142, to: 187}, + {from: 142, to: 263}, + {from: 142, to: 299}, + {from: 142, to: 300}, + {from: 142, to: 301}, + {from: 142, to: 432}, + {from: 142, to: 444}, + {from: 142, to: 455}, + {from: 142, to: 469}, + {from: 142, to: 514}, + {from: 142, to: 535}, + {from: 142, to: 539}, + {from: 142, to: 542}, + {from: 142, to: 624}, + {from: 142, to: 653}, + {from: 142, to: 660}, + {from: 142, to: 669}, + {from: 142, to: 698}, + {from: 143, to: 147}, + {from: 143, to: 157}, + {from: 143, to: 175}, + {from: 143, to: 187}, + {from: 143, to: 263}, + {from: 143, to: 299}, + {from: 143, to: 300}, + {from: 143, to: 301}, + {from: 143, to: 309}, + {from: 143, to: 332}, + {from: 143, to: 432}, + {from: 143, to: 444}, + {from: 143, to: 455}, + {from: 143, to: 469}, + {from: 143, to: 514}, + {from: 143, to: 535}, + {from: 143, to: 539}, + {from: 143, to: 542}, + {from: 143, to: 624}, + {from: 143, to: 645}, + {from: 143, to: 653}, + {from: 143, to: 669}, + {from: 143, to: 698}, + {from: 144, to: 150}, + {from: 144, to: 231}, + {from: 144, to: 232}, + {from: 144, to: 238}, + {from: 144, to: 258}, + {from: 144, to: 303}, + {from: 144, to: 308}, + {from: 144, to: 318}, + {from: 144, to: 335}, + {from: 144, to: 348}, + {from: 144, to: 371}, + {from: 144, to: 415}, + {from: 144, to: 434}, + {from: 144, to: 460}, + {from: 144, to: 528}, + {from: 144, to: 562}, + {from: 144, to: 575}, + {from: 144, to: 576}, + {from: 144, to: 583}, + {from: 144, to: 603}, + {from: 144, to: 606}, + {from: 144, to: 616}, + {from: 144, to: 646}, + {from: 144, to: 668}, + {from: 144, to: 713}, + {from: 145, to: 151}, + {from: 145, to: 224}, + {from: 145, to: 233}, + {from: 145, to: 279}, + {from: 145, to: 280}, + {from: 145, to: 289}, + {from: 145, to: 316}, + {from: 145, to: 323}, + {from: 145, to: 327}, + {from: 145, to: 331}, + {from: 145, to: 375}, + {from: 145, to: 376}, + {from: 145, to: 431}, + {from: 145, to: 436}, + {from: 145, to: 442}, + {from: 145, to: 443}, + {from: 145, to: 454}, + {from: 145, to: 455}, + {from: 145, to: 490}, + {from: 145, to: 529}, + {from: 145, to: 547}, + {from: 145, to: 567}, + {from: 145, to: 577}, + {from: 145, to: 586}, + {from: 145, to: 676}, + {from: 145, to: 678}, + {from: 145, to: 687}, + {from: 145, to: 699}, + {from: 145, to: 717}, + {from: 145, to: 721}, + {from: 146, to: 182}, + {from: 146, to: 210}, + {from: 146, to: 217}, + {from: 146, to: 278}, + {from: 146, to: 286}, + {from: 146, to: 321}, + {from: 146, to: 326}, + {from: 146, to: 337}, + {from: 146, to: 407}, + {from: 146, to: 420}, + {from: 146, to: 488}, + {from: 146, to: 533}, + {from: 146, to: 579}, + {from: 146, to: 626}, + {from: 146, to: 627}, + {from: 146, to: 662}, + {from: 146, to: 705}, + {from: 147, to: 157}, + {from: 147, to: 175}, + {from: 147, to: 187}, + {from: 147, to: 263}, + {from: 147, to: 299}, + {from: 147, to: 300}, + {from: 147, to: 301}, + {from: 147, to: 432}, + {from: 147, to: 444}, + {from: 147, to: 455}, + {from: 147, to: 469}, + {from: 147, to: 514}, + {from: 147, to: 535}, + {from: 147, to: 539}, + {from: 147, to: 542}, + {from: 147, to: 624}, + {from: 147, to: 653}, + {from: 147, to: 669}, + {from: 147, to: 698}, + {from: 148, to: 208}, + {from: 148, to: 298}, + {from: 148, to: 307}, + {from: 148, to: 310}, + {from: 148, to: 313}, + {from: 148, to: 458}, + {from: 148, to: 459}, + {from: 148, to: 468}, + {from: 148, to: 470}, + {from: 148, to: 471}, + {from: 148, to: 477}, + {from: 148, to: 479}, + {from: 148, to: 515}, + {from: 148, to: 518}, + {from: 148, to: 541}, + {from: 148, to: 620}, + {from: 148, to: 680}, + {from: 148, to: 686}, + {from: 149, to: 171}, + {from: 149, to: 244}, + {from: 149, to: 314}, + {from: 149, to: 325}, + {from: 149, to: 338}, + {from: 149, to: 345}, + {from: 149, to: 350}, + {from: 149, to: 396}, + {from: 149, to: 417}, + {from: 149, to: 496}, + {from: 149, to: 507}, + {from: 149, to: 534}, + {from: 149, to: 566}, + {from: 149, to: 606}, + {from: 149, to: 613}, + {from: 149, to: 659}, + {from: 149, to: 673}, + {from: 149, to: 682}, + {from: 149, to: 714}, + {from: 150, to: 154}, + {from: 150, to: 155}, + {from: 150, to: 164}, + {from: 150, to: 227}, + {from: 150, to: 245}, + {from: 150, to: 294}, + {from: 150, to: 316}, + {from: 150, to: 318}, + {from: 150, to: 349}, + {from: 150, to: 371}, + {from: 150, to: 373}, + {from: 150, to: 397}, + {from: 150, to: 460}, + {from: 150, to: 528}, + {from: 150, to: 562}, + {from: 150, to: 569}, + {from: 150, to: 572}, + {from: 150, to: 576}, + {from: 150, to: 591}, + {from: 150, to: 606}, + {from: 150, to: 629}, + {from: 150, to: 643}, + {from: 150, to: 644}, + {from: 150, to: 646}, + {from: 150, to: 713}, + {from: 150, to: 719}, + {from: 150, to: 720}, + {from: 151, to: 179}, + {from: 151, to: 224}, + {from: 151, to: 227}, + {from: 151, to: 231}, + {from: 151, to: 233}, + {from: 151, to: 238}, + {from: 151, to: 279}, + {from: 151, to: 280}, + {from: 151, to: 289}, + {from: 151, to: 323}, + {from: 151, to: 331}, + {from: 151, to: 342}, + {from: 151, to: 376}, + {from: 151, to: 431}, + {from: 151, to: 436}, + {from: 151, to: 443}, + {from: 151, to: 490}, + {from: 151, to: 529}, + {from: 151, to: 547}, + {from: 151, to: 567}, + {from: 151, to: 586}, + {from: 151, to: 627}, + {from: 151, to: 676}, + {from: 151, to: 699}, + {from: 151, to: 717}, + {from: 152, to: 163}, + {from: 152, to: 206}, + {from: 152, to: 248}, + {from: 152, to: 336}, + {from: 152, to: 343}, + {from: 152, to: 360}, + {from: 152, to: 378}, + {from: 152, to: 388}, + {from: 152, to: 448}, + {from: 152, to: 501}, + {from: 152, to: 506}, + {from: 152, to: 550}, + {from: 152, to: 563}, + {from: 152, to: 588}, + {from: 152, to: 617}, + {from: 152, to: 712}, + {from: 152, to: 727}, + {from: 153, to: 181}, + {from: 153, to: 219}, + {from: 153, to: 234}, + {from: 153, to: 304}, + {from: 153, to: 309}, + {from: 153, to: 366}, + {from: 153, to: 369}, + {from: 153, to: 370}, + {from: 153, to: 457}, + {from: 153, to: 554}, + {from: 153, to: 630}, + {from: 153, to: 672}, + {from: 153, to: 701}, + {from: 154, to: 155}, + {from: 154, to: 164}, + {from: 154, to: 183}, + {from: 154, to: 224}, + {from: 154, to: 227}, + {from: 154, to: 245}, + {from: 154, to: 294}, + {from: 154, to: 308}, + {from: 154, to: 316}, + {from: 154, to: 335}, + {from: 154, to: 349}, + {from: 154, to: 371}, + {from: 154, to: 373}, + {from: 154, to: 397}, + {from: 154, to: 462}, + {from: 154, to: 555}, + {from: 154, to: 569}, + {from: 154, to: 572}, + {from: 154, to: 591}, + {from: 154, to: 629}, + {from: 154, to: 642}, + {from: 154, to: 643}, + {from: 154, to: 644}, + {from: 154, to: 707}, + {from: 154, to: 719}, + {from: 154, to: 720}, + {from: 154, to: 726}, + {from: 155, to: 164}, + {from: 155, to: 166}, + {from: 155, to: 227}, + {from: 155, to: 245}, + {from: 155, to: 294}, + {from: 155, to: 316}, + {from: 155, to: 349}, + {from: 155, to: 359}, + {from: 155, to: 371}, + {from: 155, to: 373}, + {from: 155, to: 397}, + {from: 155, to: 569}, + {from: 155, to: 572}, + {from: 155, to: 591}, + {from: 155, to: 629}, + {from: 155, to: 643}, + {from: 155, to: 644}, + {from: 155, to: 677}, + {from: 155, to: 685}, + {from: 155, to: 719}, + {from: 155, to: 720}, + {from: 156, to: 213}, + {from: 156, to: 242}, + {from: 156, to: 265}, + {from: 156, to: 326}, + {from: 156, to: 341}, + {from: 156, to: 365}, + {from: 156, to: 375}, + {from: 156, to: 406}, + {from: 156, to: 476}, + {from: 156, to: 502}, + {from: 156, to: 513}, + {from: 156, to: 530}, + {from: 156, to: 544}, + {from: 156, to: 681}, + {from: 156, to: 683}, + {from: 157, to: 175}, + {from: 157, to: 187}, + {from: 157, to: 240}, + {from: 157, to: 263}, + {from: 157, to: 299}, + {from: 157, to: 300}, + {from: 157, to: 301}, + {from: 157, to: 432}, + {from: 157, to: 434}, + {from: 157, to: 444}, + {from: 157, to: 455}, + {from: 157, to: 469}, + {from: 157, to: 491}, + {from: 157, to: 514}, + {from: 157, to: 521}, + {from: 157, to: 535}, + {from: 157, to: 539}, + {from: 157, to: 542}, + {from: 157, to: 603}, + {from: 157, to: 624}, + {from: 157, to: 653}, + {from: 157, to: 669}, + {from: 157, to: 698}, + {from: 158, to: 174}, + {from: 158, to: 243}, + {from: 158, to: 292}, + {from: 158, to: 293}, + {from: 158, to: 439}, + {from: 158, to: 540}, + {from: 158, to: 568}, + {from: 158, to: 640}, + {from: 158, to: 641}, + {from: 158, to: 695}, + {from: 158, to: 704}, + {from: 158, to: 708}, + {from: 158, to: 732}, + {from: 158, to: 733}, + {from: 159, to: 160}, + {from: 159, to: 161}, + {from: 159, to: 226}, + {from: 159, to: 246}, + {from: 159, to: 257}, + {from: 159, to: 297}, + {from: 159, to: 302}, + {from: 159, to: 322}, + {from: 159, to: 398}, + {from: 159, to: 474}, + {from: 159, to: 485}, + {from: 159, to: 553}, + {from: 159, to: 621}, + {from: 159, to: 632}, + {from: 159, to: 638}, + {from: 159, to: 639}, + {from: 159, to: 657}, + {from: 159, to: 671}, + {from: 159, to: 714}, + {from: 159, to: 715}, + {from: 159, to: 726}, + {from: 160, to: 161}, + {from: 160, to: 246}, + {from: 160, to: 257}, + {from: 160, to: 281}, + {from: 160, to: 297}, + {from: 160, to: 322}, + {from: 160, to: 398}, + {from: 160, to: 474}, + {from: 160, to: 485}, + {from: 160, to: 553}, + {from: 160, to: 614}, + {from: 160, to: 621}, + {from: 160, to: 632}, + {from: 160, to: 638}, + {from: 160, to: 639}, + {from: 160, to: 657}, + {from: 160, to: 671}, + {from: 160, to: 715}, + {from: 160, to: 726}, + {from: 161, to: 246}, + {from: 161, to: 257}, + {from: 161, to: 297}, + {from: 161, to: 322}, + {from: 161, to: 398}, + {from: 161, to: 474}, + {from: 161, to: 485}, + {from: 161, to: 553}, + {from: 161, to: 621}, + {from: 161, to: 632}, + {from: 161, to: 638}, + {from: 161, to: 639}, + {from: 161, to: 657}, + {from: 161, to: 671}, + {from: 161, to: 715}, + {from: 161, to: 726}, + {from: 162, to: 215}, + {from: 162, to: 241}, + {from: 162, to: 260}, + {from: 162, to: 266}, + {from: 162, to: 271}, + {from: 162, to: 299}, + {from: 162, to: 301}, + {from: 162, to: 339}, + {from: 162, to: 364}, + {from: 162, to: 384}, + {from: 162, to: 431}, + {from: 162, to: 453}, + {from: 162, to: 504}, + {from: 162, to: 578}, + {from: 162, to: 596}, + {from: 162, to: 602}, + {from: 162, to: 610}, + {from: 162, to: 661}, + {from: 162, to: 665}, + {from: 162, to: 690}, + {from: 162, to: 692}, + {from: 162, to: 693}, + {from: 162, to: 703}, + {from: 162, to: 721}, + {from: 162, to: 723}, + {from: 163, to: 206}, + {from: 163, to: 248}, + {from: 163, to: 336}, + {from: 163, to: 343}, + {from: 163, to: 360}, + {from: 163, to: 378}, + {from: 163, to: 388}, + {from: 163, to: 448}, + {from: 163, to: 501}, + {from: 163, to: 506}, + {from: 163, to: 550}, + {from: 163, to: 563}, + {from: 163, to: 588}, + {from: 163, to: 617}, + {from: 163, to: 712}, + {from: 163, to: 727}, + {from: 164, to: 166}, + {from: 164, to: 227}, + {from: 164, to: 245}, + {from: 164, to: 294}, + {from: 164, to: 316}, + {from: 164, to: 349}, + {from: 164, to: 359}, + {from: 164, to: 371}, + {from: 164, to: 373}, + {from: 164, to: 397}, + {from: 164, to: 569}, + {from: 164, to: 572}, + {from: 164, to: 591}, + {from: 164, to: 629}, + {from: 164, to: 643}, + {from: 164, to: 644}, + {from: 164, to: 677}, + {from: 164, to: 685}, + {from: 164, to: 719}, + {from: 164, to: 720}, + {from: 165, to: 166}, + {from: 165, to: 167}, + {from: 165, to: 168}, + {from: 165, to: 185}, + {from: 165, to: 191}, + {from: 165, to: 226}, + {from: 165, to: 240}, + {from: 165, to: 285}, + {from: 165, to: 352}, + {from: 165, to: 359}, + {from: 165, to: 430}, + {from: 165, to: 461}, + {from: 165, to: 463}, + {from: 165, to: 486}, + {from: 165, to: 531}, + {from: 165, to: 607}, + {from: 165, to: 634}, + {from: 165, to: 711}, + {from: 165, to: 729}, + {from: 166, to: 167}, + {from: 166, to: 168}, + {from: 166, to: 185}, + {from: 166, to: 191}, + {from: 166, to: 226}, + {from: 166, to: 240}, + {from: 166, to: 352}, + {from: 166, to: 359}, + {from: 166, to: 373}, + {from: 166, to: 397}, + {from: 166, to: 430}, + {from: 166, to: 461}, + {from: 166, to: 463}, + {from: 166, to: 486}, + {from: 166, to: 531}, + {from: 166, to: 607}, + {from: 166, to: 634}, + {from: 166, to: 677}, + {from: 166, to: 685}, + {from: 166, to: 711}, + {from: 167, to: 168}, + {from: 167, to: 185}, + {from: 167, to: 191}, + {from: 167, to: 226}, + {from: 167, to: 240}, + {from: 167, to: 352}, + {from: 167, to: 359}, + {from: 167, to: 430}, + {from: 167, to: 461}, + {from: 167, to: 463}, + {from: 167, to: 486}, + {from: 167, to: 531}, + {from: 167, to: 607}, + {from: 167, to: 634}, + {from: 167, to: 711}, + {from: 168, to: 185}, + {from: 168, to: 191}, + {from: 168, to: 226}, + {from: 168, to: 240}, + {from: 168, to: 352}, + {from: 168, to: 359}, + {from: 168, to: 410}, + {from: 168, to: 430}, + {from: 168, to: 461}, + {from: 168, to: 463}, + {from: 168, to: 486}, + {from: 168, to: 531}, + {from: 168, to: 557}, + {from: 168, to: 607}, + {from: 168, to: 634}, + {from: 168, to: 711}, + {from: 169, to: 196}, + {from: 169, to: 229}, + {from: 169, to: 256}, + {from: 169, to: 267}, + {from: 169, to: 275}, + {from: 169, to: 276}, + {from: 169, to: 295}, + {from: 169, to: 304}, + {from: 169, to: 317}, + {from: 169, to: 318}, + {from: 169, to: 352}, + {from: 169, to: 355}, + {from: 169, to: 357}, + {from: 169, to: 369}, + {from: 169, to: 446}, + {from: 169, to: 509}, + {from: 169, to: 510}, + {from: 169, to: 526}, + {from: 169, to: 546}, + {from: 169, to: 564}, + {from: 169, to: 581}, + {from: 169, to: 592}, + {from: 169, to: 652}, + {from: 169, to: 667}, + {from: 170, to: 250}, + {from: 170, to: 251}, + {from: 170, to: 253}, + {from: 170, to: 254}, + {from: 170, to: 255}, + {from: 170, to: 356}, + {from: 170, to: 400}, + {from: 170, to: 401}, + {from: 170, to: 402}, + {from: 170, to: 410}, + {from: 170, to: 423}, + {from: 170, to: 545}, + {from: 170, to: 556}, + {from: 170, to: 557}, + {from: 170, to: 558}, + {from: 170, to: 656}, + {from: 170, to: 660}, + {from: 170, to: 674}, + {from: 170, to: 694}, + {from: 170, to: 696}, + {from: 171, to: 200}, + {from: 171, to: 244}, + {from: 171, to: 314}, + {from: 171, to: 325}, + {from: 171, to: 338}, + {from: 171, to: 345}, + {from: 171, to: 350}, + {from: 171, to: 396}, + {from: 171, to: 417}, + {from: 171, to: 496}, + {from: 171, to: 507}, + {from: 171, to: 534}, + {from: 171, to: 566}, + {from: 171, to: 579}, + {from: 171, to: 593}, + {from: 171, to: 606}, + {from: 171, to: 613}, + {from: 171, to: 659}, + {from: 171, to: 673}, + {from: 171, to: 682}, + {from: 171, to: 714}, + {from: 172, to: 177}, + {from: 172, to: 179}, + {from: 172, to: 311}, + {from: 172, to: 384}, + {from: 172, to: 386}, + {from: 172, to: 408}, + {from: 172, to: 460}, + {from: 172, to: 522}, + {from: 172, to: 527}, + {from: 172, to: 532}, + {from: 172, to: 604}, + {from: 172, to: 612}, + {from: 172, to: 621}, + {from: 172, to: 625}, + {from: 172, to: 654}, + {from: 172, to: 667}, + {from: 172, to: 677}, + {from: 172, to: 679}, + {from: 172, to: 685}, + {from: 172, to: 706}, + {from: 172, to: 707}, + {from: 173, to: 195}, + {from: 173, to: 205}, + {from: 173, to: 218}, + {from: 173, to: 274}, + {from: 173, to: 296}, + {from: 173, to: 418}, + {from: 173, to: 435}, + {from: 173, to: 493}, + {from: 173, to: 494}, + {from: 173, to: 519}, + {from: 173, to: 525}, + {from: 173, to: 526}, + {from: 173, to: 582}, + {from: 173, to: 585}, + {from: 173, to: 605}, + {from: 173, to: 631}, + {from: 173, to: 655}, + {from: 173, to: 722}, + {from: 174, to: 243}, + {from: 174, to: 292}, + {from: 174, to: 293}, + {from: 174, to: 439}, + {from: 174, to: 540}, + {from: 174, to: 568}, + {from: 174, to: 640}, + {from: 174, to: 641}, + {from: 174, to: 695}, + {from: 174, to: 704}, + {from: 174, to: 708}, + {from: 174, to: 732}, + {from: 174, to: 733}, + {from: 175, to: 187}, + {from: 175, to: 263}, + {from: 175, to: 299}, + {from: 175, to: 300}, + {from: 175, to: 301}, + {from: 175, to: 325}, + {from: 175, to: 432}, + {from: 175, to: 444}, + {from: 175, to: 455}, + {from: 175, to: 469}, + {from: 175, to: 511}, + {from: 175, to: 514}, + {from: 175, to: 535}, + {from: 175, to: 539}, + {from: 175, to: 542}, + {from: 175, to: 624}, + {from: 175, to: 653}, + {from: 175, to: 669}, + {from: 175, to: 698}, + {from: 176, to: 178}, + {from: 176, to: 197}, + {from: 176, to: 328}, + {from: 176, to: 329}, + {from: 176, to: 351}, + {from: 176, to: 367}, + {from: 176, to: 372}, + {from: 176, to: 426}, + {from: 176, to: 427}, + {from: 176, to: 456}, + {from: 176, to: 464}, + {from: 176, to: 492}, + {from: 176, to: 536}, + {from: 176, to: 549}, + {from: 176, to: 551}, + {from: 176, to: 609}, + {from: 176, to: 615}, + {from: 176, to: 700}, + {from: 176, to: 718}, + {from: 177, to: 179}, + {from: 177, to: 218}, + {from: 177, to: 221}, + {from: 177, to: 260}, + {from: 177, to: 261}, + {from: 177, to: 279}, + {from: 177, to: 311}, + {from: 177, to: 366}, + {from: 177, to: 384}, + {from: 177, to: 386}, + {from: 177, to: 408}, + {from: 177, to: 422}, + {from: 177, to: 460}, + {from: 177, to: 522}, + {from: 177, to: 527}, + {from: 177, to: 532}, + {from: 177, to: 572}, + {from: 177, to: 591}, + {from: 177, to: 612}, + {from: 177, to: 625}, + {from: 177, to: 654}, + {from: 177, to: 667}, + {from: 177, to: 677}, + {from: 177, to: 679}, + {from: 177, to: 685}, + {from: 177, to: 693}, + {from: 177, to: 707}, + {from: 178, to: 197}, + {from: 178, to: 328}, + {from: 178, to: 329}, + {from: 178, to: 351}, + {from: 178, to: 367}, + {from: 178, to: 372}, + {from: 178, to: 426}, + {from: 178, to: 427}, + {from: 178, to: 456}, + {from: 178, to: 464}, + {from: 178, to: 492}, + {from: 178, to: 536}, + {from: 178, to: 549}, + {from: 178, to: 551}, + {from: 178, to: 609}, + {from: 178, to: 615}, + {from: 178, to: 700}, + {from: 178, to: 718}, + {from: 179, to: 227}, + {from: 179, to: 231}, + {from: 179, to: 238}, + {from: 179, to: 311}, + {from: 179, to: 342}, + {from: 179, to: 384}, + {from: 179, to: 386}, + {from: 179, to: 408}, + {from: 179, to: 460}, + {from: 179, to: 522}, + {from: 179, to: 527}, + {from: 179, to: 532}, + {from: 179, to: 547}, + {from: 179, to: 586}, + {from: 179, to: 612}, + {from: 179, to: 625}, + {from: 179, to: 627}, + {from: 179, to: 654}, + {from: 179, to: 667}, + {from: 179, to: 677}, + {from: 179, to: 679}, + {from: 179, to: 685}, + {from: 179, to: 707}, + {from: 179, to: 717}, + {from: 180, to: 188}, + {from: 180, to: 216}, + {from: 180, to: 277}, + {from: 180, to: 286}, + {from: 180, to: 332}, + {from: 180, to: 333}, + {from: 180, to: 428}, + {from: 180, to: 511}, + {from: 180, to: 528}, + {from: 180, to: 571}, + {from: 180, to: 580}, + {from: 180, to: 593}, + {from: 180, to: 601}, + {from: 180, to: 618}, + {from: 180, to: 619}, + {from: 180, to: 652}, + {from: 180, to: 703}, + {from: 180, to: 716}, + {from: 181, to: 219}, + {from: 181, to: 234}, + {from: 181, to: 304}, + {from: 181, to: 309}, + {from: 181, to: 366}, + {from: 181, to: 369}, + {from: 181, to: 370}, + {from: 181, to: 457}, + {from: 181, to: 554}, + {from: 181, to: 630}, + {from: 181, to: 672}, + {from: 181, to: 701}, + {from: 182, to: 210}, + {from: 182, to: 217}, + {from: 182, to: 278}, + {from: 182, to: 321}, + {from: 182, to: 337}, + {from: 182, to: 407}, + {from: 182, to: 420}, + {from: 182, to: 488}, + {from: 182, to: 533}, + {from: 182, to: 579}, + {from: 182, to: 626}, + {from: 182, to: 627}, + {from: 182, to: 662}, + {from: 182, to: 705}, + {from: 183, to: 184}, + {from: 183, to: 198}, + {from: 183, to: 204}, + {from: 183, to: 224}, + {from: 183, to: 270}, + {from: 183, to: 302}, + {from: 183, to: 308}, + {from: 183, to: 312}, + {from: 183, to: 335}, + {from: 183, to: 462}, + {from: 183, to: 497}, + {from: 183, to: 516}, + {from: 183, to: 524}, + {from: 183, to: 538}, + {from: 183, to: 555}, + {from: 183, to: 633}, + {from: 183, to: 635}, + {from: 183, to: 636}, + {from: 183, to: 637}, + {from: 183, to: 642}, + {from: 183, to: 684}, + {from: 183, to: 688}, + {from: 183, to: 697}, + {from: 183, to: 707}, + {from: 183, to: 726}, + {from: 183, to: 736}, + {from: 184, to: 198}, + {from: 184, to: 204}, + {from: 184, to: 270}, + {from: 184, to: 302}, + {from: 184, to: 312}, + {from: 184, to: 497}, + {from: 184, to: 516}, + {from: 184, to: 524}, + {from: 184, to: 538}, + {from: 184, to: 633}, + {from: 184, to: 635}, + {from: 184, to: 636}, + {from: 184, to: 637}, + {from: 184, to: 684}, + {from: 184, to: 688}, + {from: 184, to: 697}, + {from: 184, to: 736}, + {from: 185, to: 191}, + {from: 185, to: 212}, + {from: 185, to: 226}, + {from: 185, to: 240}, + {from: 185, to: 352}, + {from: 185, to: 359}, + {from: 185, to: 425}, + {from: 185, to: 430}, + {from: 185, to: 449}, + {from: 185, to: 461}, + {from: 185, to: 463}, + {from: 185, to: 486}, + {from: 185, to: 490}, + {from: 185, to: 531}, + {from: 185, to: 607}, + {from: 185, to: 622}, + {from: 185, to: 634}, + {from: 185, to: 675}, + {from: 185, to: 676}, + {from: 185, to: 711}, + {from: 185, to: 728}, + {from: 186, to: 201}, + {from: 186, to: 222}, + {from: 186, to: 228}, + {from: 186, to: 235}, + {from: 186, to: 236}, + {from: 186, to: 305}, + {from: 186, to: 324}, + {from: 186, to: 334}, + {from: 186, to: 353}, + {from: 186, to: 368}, + {from: 186, to: 429}, + {from: 186, to: 489}, + {from: 186, to: 499}, + {from: 186, to: 548}, + {from: 186, to: 552}, + {from: 186, to: 595}, + {from: 186, to: 710}, + {from: 187, to: 263}, + {from: 187, to: 299}, + {from: 187, to: 300}, + {from: 187, to: 301}, + {from: 187, to: 432}, + {from: 187, to: 444}, + {from: 187, to: 455}, + {from: 187, to: 469}, + {from: 187, to: 514}, + {from: 187, to: 535}, + {from: 187, to: 539}, + {from: 187, to: 542}, + {from: 187, to: 624}, + {from: 187, to: 653}, + {from: 187, to: 669}, + {from: 187, to: 698}, + {from: 188, to: 216}, + {from: 188, to: 277}, + {from: 188, to: 286}, + {from: 188, to: 332}, + {from: 188, to: 333}, + {from: 188, to: 428}, + {from: 188, to: 511}, + {from: 188, to: 528}, + {from: 188, to: 571}, + {from: 188, to: 580}, + {from: 188, to: 593}, + {from: 188, to: 601}, + {from: 188, to: 618}, + {from: 188, to: 619}, + {from: 188, to: 652}, + {from: 188, to: 703}, + {from: 188, to: 716}, + {from: 189, to: 207}, + {from: 189, to: 209}, + {from: 189, to: 214}, + {from: 189, to: 223}, + {from: 189, to: 230}, + {from: 189, to: 239}, + {from: 189, to: 262}, + {from: 189, to: 320}, + {from: 189, to: 333}, + {from: 189, to: 344}, + {from: 189, to: 354}, + {from: 189, to: 361}, + {from: 189, to: 362}, + {from: 189, to: 445}, + {from: 189, to: 483}, + {from: 189, to: 484}, + {from: 189, to: 512}, + {from: 189, to: 578}, + {from: 189, to: 601}, + {from: 189, to: 655}, + {from: 190, to: 194}, + {from: 190, to: 197}, + {from: 190, to: 200}, + {from: 190, to: 232}, + {from: 190, to: 254}, + {from: 190, to: 259}, + {from: 190, to: 342}, + {from: 190, to: 363}, + {from: 190, to: 379}, + {from: 190, to: 383}, + {from: 190, to: 403}, + {from: 190, to: 500}, + {from: 190, to: 505}, + {from: 190, to: 537}, + {from: 190, to: 574}, + {from: 190, to: 587}, + {from: 190, to: 597}, + {from: 190, to: 649}, + {from: 190, to: 691}, + {from: 190, to: 702}, + {from: 190, to: 706}, + {from: 191, to: 226}, + {from: 191, to: 240}, + {from: 191, to: 324}, + {from: 191, to: 352}, + {from: 191, to: 359}, + {from: 191, to: 430}, + {from: 191, to: 461}, + {from: 191, to: 463}, + {from: 191, to: 486}, + {from: 191, to: 531}, + {from: 191, to: 607}, + {from: 191, to: 634}, + {from: 191, to: 711}, + {from: 192, to: 247}, + {from: 192, to: 273}, + {from: 192, to: 284}, + {from: 192, to: 306}, + {from: 192, to: 315}, + {from: 192, to: 380}, + {from: 192, to: 389}, + {from: 192, to: 467}, + {from: 192, to: 495}, + {from: 192, to: 570}, + {from: 192, to: 584}, + {from: 192, to: 598}, + {from: 192, to: 599}, + {from: 192, to: 666}, + {from: 193, to: 264}, + {from: 193, to: 281}, + {from: 193, to: 282}, + {from: 193, to: 285}, + {from: 193, to: 382}, + {from: 193, to: 408}, + {from: 193, to: 438}, + {from: 193, to: 441}, + {from: 193, to: 465}, + {from: 193, to: 466}, + {from: 193, to: 491}, + {from: 193, to: 646}, + {from: 193, to: 647}, + {from: 193, to: 650}, + {from: 193, to: 651}, + {from: 193, to: 689}, + {from: 193, to: 724}, + {from: 193, to: 725}, + {from: 193, to: 729}, + {from: 193, to: 730}, + {from: 193, to: 734}, + {from: 193, to: 735}, + {from: 194, to: 200}, + {from: 194, to: 259}, + {from: 194, to: 342}, + {from: 194, to: 363}, + {from: 194, to: 379}, + {from: 194, to: 383}, + {from: 194, to: 403}, + {from: 194, to: 500}, + {from: 194, to: 505}, + {from: 194, to: 537}, + {from: 194, to: 574}, + {from: 194, to: 587}, + {from: 194, to: 597}, + {from: 194, to: 649}, + {from: 194, to: 691}, + {from: 194, to: 702}, + {from: 194, to: 706}, + {from: 195, to: 205}, + {from: 195, to: 218}, + {from: 195, to: 274}, + {from: 195, to: 296}, + {from: 195, to: 418}, + {from: 195, to: 435}, + {from: 195, to: 444}, + {from: 195, to: 493}, + {from: 195, to: 494}, + {from: 195, to: 519}, + {from: 195, to: 525}, + {from: 195, to: 526}, + {from: 195, to: 582}, + {from: 195, to: 585}, + {from: 195, to: 605}, + {from: 195, to: 631}, + {from: 195, to: 655}, + {from: 195, to: 722}, + {from: 196, to: 275}, + {from: 196, to: 288}, + {from: 196, to: 304}, + {from: 196, to: 352}, + {from: 196, to: 369}, + {from: 196, to: 381}, + {from: 196, to: 409}, + {from: 196, to: 421}, + {from: 196, to: 425}, + {from: 196, to: 440}, + {from: 196, to: 472}, + {from: 196, to: 473}, + {from: 196, to: 508}, + {from: 196, to: 521}, + {from: 196, to: 523}, + {from: 196, to: 526}, + {from: 196, to: 543}, + {from: 196, to: 562}, + {from: 196, to: 565}, + {from: 196, to: 589}, + {from: 196, to: 594}, + {from: 196, to: 604}, + {from: 196, to: 652}, + {from: 196, to: 663}, + {from: 196, to: 667}, + {from: 196, to: 728}, + {from: 197, to: 232}, + {from: 197, to: 254}, + {from: 197, to: 328}, + {from: 197, to: 329}, + {from: 197, to: 351}, + {from: 197, to: 367}, + {from: 197, to: 372}, + {from: 197, to: 426}, + {from: 197, to: 427}, + {from: 197, to: 456}, + {from: 197, to: 464}, + {from: 197, to: 492}, + {from: 197, to: 536}, + {from: 197, to: 549}, + {from: 197, to: 551}, + {from: 197, to: 609}, + {from: 197, to: 615}, + {from: 197, to: 700}, + {from: 197, to: 718}, + {from: 198, to: 204}, + {from: 198, to: 270}, + {from: 198, to: 302}, + {from: 198, to: 312}, + {from: 198, to: 497}, + {from: 198, to: 516}, + {from: 198, to: 524}, + {from: 198, to: 538}, + {from: 198, to: 633}, + {from: 198, to: 635}, + {from: 198, to: 636}, + {from: 198, to: 637}, + {from: 198, to: 658}, + {from: 198, to: 684}, + {from: 198, to: 688}, + {from: 198, to: 697}, + {from: 198, to: 736}, + {from: 199, to: 220}, + {from: 199, to: 272}, + {from: 199, to: 274}, + {from: 199, to: 340}, + {from: 199, to: 346}, + {from: 199, to: 347}, + {from: 199, to: 387}, + {from: 199, to: 404}, + {from: 199, to: 437}, + {from: 199, to: 503}, + {from: 199, to: 520}, + {from: 199, to: 590}, + {from: 199, to: 628}, + {from: 199, to: 664}, + {from: 199, to: 670}, + {from: 199, to: 709}, + {from: 200, to: 259}, + {from: 200, to: 342}, + {from: 200, to: 363}, + {from: 200, to: 379}, + {from: 200, to: 383}, + {from: 200, to: 403}, + {from: 200, to: 500}, + {from: 200, to: 505}, + {from: 200, to: 537}, + {from: 200, to: 574}, + {from: 200, to: 579}, + {from: 200, to: 587}, + {from: 200, to: 593}, + {from: 200, to: 597}, + {from: 200, to: 649}, + {from: 200, to: 691}, + {from: 200, to: 702}, + {from: 200, to: 706}, + {from: 201, to: 222}, + {from: 201, to: 228}, + {from: 201, to: 235}, + {from: 201, to: 236}, + {from: 201, to: 305}, + {from: 201, to: 324}, + {from: 201, to: 334}, + {from: 201, to: 353}, + {from: 201, to: 368}, + {from: 201, to: 429}, + {from: 201, to: 489}, + {from: 201, to: 499}, + {from: 201, to: 548}, + {from: 201, to: 552}, + {from: 201, to: 595}, + {from: 201, to: 710}, + {from: 202, to: 211}, + {from: 202, to: 212}, + {from: 202, to: 221}, + {from: 202, to: 225}, + {from: 202, to: 261}, + {from: 202, to: 287}, + {from: 202, to: 319}, + {from: 202, to: 358}, + {from: 202, to: 419}, + {from: 202, to: 424}, + {from: 202, to: 450}, + {from: 202, to: 451}, + {from: 202, to: 462}, + {from: 202, to: 486}, + {from: 202, to: 487}, + {from: 202, to: 555}, + {from: 202, to: 600}, + {from: 202, to: 608}, + {from: 202, to: 618}, + {from: 202, to: 642}, + {from: 202, to: 645}, + {from: 203, to: 327}, + {from: 203, to: 374}, + {from: 203, to: 385}, + {from: 203, to: 433}, + {from: 203, to: 442}, + {from: 203, to: 454}, + {from: 203, to: 475}, + {from: 203, to: 480}, + {from: 203, to: 498}, + {from: 203, to: 517}, + {from: 203, to: 518}, + {from: 203, to: 573}, + {from: 203, to: 577}, + {from: 203, to: 611}, + {from: 203, to: 614}, + {from: 203, to: 623}, + {from: 203, to: 648}, + {from: 203, to: 656}, + {from: 203, to: 678}, + {from: 203, to: 687}, + {from: 204, to: 270}, + {from: 204, to: 302}, + {from: 204, to: 312}, + {from: 204, to: 497}, + {from: 204, to: 516}, + {from: 204, to: 524}, + {from: 204, to: 538}, + {from: 204, to: 633}, + {from: 204, to: 635}, + {from: 204, to: 636}, + {from: 204, to: 637}, + {from: 204, to: 684}, + {from: 204, to: 688}, + {from: 204, to: 697}, + {from: 204, to: 736}, + {from: 205, to: 218}, + {from: 205, to: 274}, + {from: 205, to: 296}, + {from: 205, to: 418}, + {from: 205, to: 435}, + {from: 205, to: 493}, + {from: 205, to: 494}, + {from: 205, to: 519}, + {from: 205, to: 525}, + {from: 205, to: 526}, + {from: 205, to: 559}, + {from: 205, to: 582}, + {from: 205, to: 585}, + {from: 205, to: 605}, + {from: 205, to: 631}, + {from: 205, to: 655}, + {from: 205, to: 722}, + {from: 206, to: 248}, + {from: 206, to: 336}, + {from: 206, to: 343}, + {from: 206, to: 360}, + {from: 206, to: 378}, + {from: 206, to: 388}, + {from: 206, to: 448}, + {from: 206, to: 501}, + {from: 206, to: 506}, + {from: 206, to: 550}, + {from: 206, to: 563}, + {from: 206, to: 588}, + {from: 206, to: 617}, + {from: 206, to: 712}, + {from: 206, to: 727}, + {from: 207, to: 209}, + {from: 207, to: 214}, + {from: 207, to: 223}, + {from: 207, to: 230}, + {from: 207, to: 239}, + {from: 207, to: 262}, + {from: 207, to: 320}, + {from: 207, to: 344}, + {from: 207, to: 354}, + {from: 207, to: 361}, + {from: 207, to: 362}, + {from: 207, to: 445}, + {from: 207, to: 483}, + {from: 207, to: 484}, + {from: 207, to: 512}, + {from: 208, to: 298}, + {from: 208, to: 307}, + {from: 208, to: 310}, + {from: 208, to: 313}, + {from: 208, to: 458}, + {from: 208, to: 459}, + {from: 208, to: 468}, + {from: 208, to: 470}, + {from: 208, to: 471}, + {from: 208, to: 477}, + {from: 208, to: 479}, + {from: 208, to: 515}, + {from: 208, to: 518}, + {from: 208, to: 541}, + {from: 208, to: 620}, + {from: 208, to: 680}, + {from: 208, to: 686}, + {from: 209, to: 214}, + {from: 209, to: 223}, + {from: 209, to: 230}, + {from: 209, to: 239}, + {from: 209, to: 262}, + {from: 209, to: 320}, + {from: 209, to: 344}, + {from: 209, to: 354}, + {from: 209, to: 361}, + {from: 209, to: 362}, + {from: 209, to: 445}, + {from: 209, to: 483}, + {from: 209, to: 484}, + {from: 209, to: 512}, + {from: 210, to: 217}, + {from: 210, to: 278}, + {from: 210, to: 321}, + {from: 210, to: 337}, + {from: 210, to: 407}, + {from: 210, to: 420}, + {from: 210, to: 488}, + {from: 210, to: 533}, + {from: 210, to: 579}, + {from: 210, to: 626}, + {from: 210, to: 627}, + {from: 210, to: 662}, + {from: 210, to: 705}, + {from: 211, to: 212}, + {from: 211, to: 221}, + {from: 211, to: 225}, + {from: 211, to: 261}, + {from: 211, to: 287}, + {from: 211, to: 319}, + {from: 211, to: 358}, + {from: 211, to: 419}, + {from: 211, to: 424}, + {from: 211, to: 450}, + {from: 211, to: 451}, + {from: 211, to: 462}, + {from: 211, to: 486}, + {from: 211, to: 487}, + {from: 211, to: 555}, + {from: 211, to: 600}, + {from: 211, to: 608}, + {from: 211, to: 618}, + {from: 211, to: 642}, + {from: 211, to: 645}, + {from: 212, to: 221}, + {from: 212, to: 225}, + {from: 212, to: 261}, + {from: 212, to: 287}, + {from: 212, to: 319}, + {from: 212, to: 358}, + {from: 212, to: 419}, + {from: 212, to: 424}, + {from: 212, to: 425}, + {from: 212, to: 449}, + {from: 212, to: 450}, + {from: 212, to: 451}, + {from: 212, to: 462}, + {from: 212, to: 487}, + {from: 212, to: 490}, + {from: 212, to: 555}, + {from: 212, to: 600}, + {from: 212, to: 608}, + {from: 212, to: 622}, + {from: 212, to: 642}, + {from: 212, to: 645}, + {from: 212, to: 675}, + {from: 212, to: 676}, + {from: 212, to: 728}, + {from: 213, to: 242}, + {from: 213, to: 265}, + {from: 213, to: 326}, + {from: 213, to: 341}, + {from: 213, to: 365}, + {from: 213, to: 375}, + {from: 213, to: 406}, + {from: 213, to: 476}, + {from: 213, to: 502}, + {from: 213, to: 513}, + {from: 213, to: 530}, + {from: 213, to: 544}, + {from: 213, to: 635}, + {from: 213, to: 681}, + {from: 213, to: 683}, + {from: 214, to: 223}, + {from: 214, to: 230}, + {from: 214, to: 239}, + {from: 214, to: 262}, + {from: 214, to: 320}, + {from: 214, to: 344}, + {from: 214, to: 354}, + {from: 214, to: 361}, + {from: 214, to: 362}, + {from: 214, to: 445}, + {from: 214, to: 483}, + {from: 214, to: 484}, + {from: 214, to: 512}, + {from: 215, to: 241}, + {from: 215, to: 257}, + {from: 215, to: 260}, + {from: 215, to: 266}, + {from: 215, to: 271}, + {from: 215, to: 339}, + {from: 215, to: 364}, + {from: 215, to: 445}, + {from: 215, to: 453}, + {from: 215, to: 504}, + {from: 215, to: 578}, + {from: 215, to: 596}, + {from: 215, to: 602}, + {from: 215, to: 610}, + {from: 215, to: 638}, + {from: 215, to: 661}, + {from: 215, to: 665}, + {from: 215, to: 690}, + {from: 215, to: 692}, + {from: 215, to: 693}, + {from: 215, to: 721}, + {from: 215, to: 723}, + {from: 216, to: 277}, + {from: 216, to: 286}, + {from: 216, to: 294}, + {from: 216, to: 332}, + {from: 216, to: 333}, + {from: 216, to: 381}, + {from: 216, to: 428}, + {from: 216, to: 432}, + {from: 216, to: 443}, + {from: 216, to: 511}, + {from: 216, to: 528}, + {from: 216, to: 571}, + {from: 216, to: 580}, + {from: 216, to: 589}, + {from: 216, to: 593}, + {from: 216, to: 601}, + {from: 216, to: 618}, + {from: 216, to: 619}, + {from: 216, to: 623}, + {from: 216, to: 644}, + {from: 216, to: 652}, + {from: 216, to: 703}, + {from: 216, to: 716}, + {from: 216, to: 719}, + {from: 217, to: 278}, + {from: 217, to: 321}, + {from: 217, to: 337}, + {from: 217, to: 407}, + {from: 217, to: 420}, + {from: 217, to: 488}, + {from: 217, to: 533}, + {from: 217, to: 579}, + {from: 217, to: 625}, + {from: 217, to: 626}, + {from: 217, to: 627}, + {from: 217, to: 662}, + {from: 217, to: 705}, + {from: 218, to: 221}, + {from: 218, to: 260}, + {from: 218, to: 261}, + {from: 218, to: 274}, + {from: 218, to: 279}, + {from: 218, to: 296}, + {from: 218, to: 366}, + {from: 218, to: 418}, + {from: 218, to: 422}, + {from: 218, to: 435}, + {from: 218, to: 493}, + {from: 218, to: 494}, + {from: 218, to: 519}, + {from: 218, to: 525}, + {from: 218, to: 526}, + {from: 218, to: 572}, + {from: 218, to: 582}, + {from: 218, to: 585}, + {from: 218, to: 591}, + {from: 218, to: 605}, + {from: 218, to: 631}, + {from: 218, to: 655}, + {from: 218, to: 693}, + {from: 218, to: 722}, + {from: 219, to: 234}, + {from: 219, to: 304}, + {from: 219, to: 309}, + {from: 219, to: 366}, + {from: 219, to: 369}, + {from: 219, to: 370}, + {from: 219, to: 457}, + {from: 219, to: 554}, + {from: 219, to: 630}, + {from: 219, to: 672}, + {from: 219, to: 701}, + {from: 220, to: 272}, + {from: 220, to: 340}, + {from: 220, to: 346}, + {from: 220, to: 347}, + {from: 220, to: 387}, + {from: 220, to: 404}, + {from: 220, to: 437}, + {from: 220, to: 503}, + {from: 220, to: 520}, + {from: 220, to: 590}, + {from: 220, to: 628}, + {from: 220, to: 664}, + {from: 220, to: 670}, + {from: 220, to: 709}, + {from: 221, to: 225}, + {from: 221, to: 260}, + {from: 221, to: 261}, + {from: 221, to: 279}, + {from: 221, to: 287}, + {from: 221, to: 319}, + {from: 221, to: 358}, + {from: 221, to: 366}, + {from: 221, to: 419}, + {from: 221, to: 422}, + {from: 221, to: 424}, + {from: 221, to: 450}, + {from: 221, to: 451}, + {from: 221, to: 462}, + {from: 221, to: 487}, + {from: 221, to: 555}, + {from: 221, to: 572}, + {from: 221, to: 591}, + {from: 221, to: 600}, + {from: 221, to: 608}, + {from: 221, to: 642}, + {from: 221, to: 645}, + {from: 221, to: 693}, + {from: 222, to: 228}, + {from: 222, to: 235}, + {from: 222, to: 236}, + {from: 222, to: 305}, + {from: 222, to: 324}, + {from: 222, to: 334}, + {from: 222, to: 353}, + {from: 222, to: 368}, + {from: 222, to: 429}, + {from: 222, to: 489}, + {from: 222, to: 499}, + {from: 222, to: 548}, + {from: 222, to: 552}, + {from: 222, to: 595}, + {from: 222, to: 710}, + {from: 223, to: 230}, + {from: 223, to: 239}, + {from: 223, to: 262}, + {from: 223, to: 320}, + {from: 223, to: 344}, + {from: 223, to: 354}, + {from: 223, to: 361}, + {from: 223, to: 362}, + {from: 223, to: 445}, + {from: 223, to: 483}, + {from: 223, to: 484}, + {from: 223, to: 512}, + {from: 224, to: 233}, + {from: 224, to: 279}, + {from: 224, to: 280}, + {from: 224, to: 289}, + {from: 224, to: 308}, + {from: 224, to: 323}, + {from: 224, to: 331}, + {from: 224, to: 335}, + {from: 224, to: 376}, + {from: 224, to: 431}, + {from: 224, to: 436}, + {from: 224, to: 443}, + {from: 224, to: 462}, + {from: 224, to: 490}, + {from: 224, to: 529}, + {from: 224, to: 547}, + {from: 224, to: 555}, + {from: 224, to: 567}, + {from: 224, to: 586}, + {from: 224, to: 642}, + {from: 224, to: 676}, + {from: 224, to: 699}, + {from: 224, to: 707}, + {from: 224, to: 717}, + {from: 224, to: 726}, + {from: 225, to: 261}, + {from: 225, to: 287}, + {from: 225, to: 319}, + {from: 225, to: 358}, + {from: 225, to: 419}, + {from: 225, to: 424}, + {from: 225, to: 450}, + {from: 225, to: 451}, + {from: 225, to: 462}, + {from: 225, to: 487}, + {from: 225, to: 555}, + {from: 225, to: 600}, + {from: 225, to: 608}, + {from: 225, to: 642}, + {from: 225, to: 645}, + {from: 226, to: 240}, + {from: 226, to: 302}, + {from: 226, to: 352}, + {from: 226, to: 359}, + {from: 226, to: 430}, + {from: 226, to: 461}, + {from: 226, to: 463}, + {from: 226, to: 486}, + {from: 226, to: 531}, + {from: 226, to: 607}, + {from: 226, to: 634}, + {from: 226, to: 711}, + {from: 226, to: 714}, + {from: 227, to: 231}, + {from: 227, to: 238}, + {from: 227, to: 245}, + {from: 227, to: 294}, + {from: 227, to: 316}, + {from: 227, to: 342}, + {from: 227, to: 349}, + {from: 227, to: 371}, + {from: 227, to: 373}, + {from: 227, to: 397}, + {from: 227, to: 547}, + {from: 227, to: 569}, + {from: 227, to: 572}, + {from: 227, to: 586}, + {from: 227, to: 591}, + {from: 227, to: 627}, + {from: 227, to: 629}, + {from: 227, to: 643}, + {from: 227, to: 644}, + {from: 227, to: 717}, + {from: 227, to: 719}, + {from: 227, to: 720}, + {from: 228, to: 235}, + {from: 228, to: 236}, + {from: 228, to: 305}, + {from: 228, to: 324}, + {from: 228, to: 334}, + {from: 228, to: 353}, + {from: 228, to: 368}, + {from: 228, to: 429}, + {from: 228, to: 489}, + {from: 228, to: 499}, + {from: 228, to: 548}, + {from: 228, to: 552}, + {from: 228, to: 595}, + {from: 228, to: 710}, + {from: 229, to: 256}, + {from: 229, to: 267}, + {from: 229, to: 275}, + {from: 229, to: 276}, + {from: 229, to: 295}, + {from: 229, to: 317}, + {from: 229, to: 318}, + {from: 229, to: 355}, + {from: 229, to: 357}, + {from: 229, to: 446}, + {from: 229, to: 509}, + {from: 229, to: 510}, + {from: 229, to: 546}, + {from: 229, to: 564}, + {from: 229, to: 581}, + {from: 229, to: 592}, + {from: 230, to: 239}, + {from: 230, to: 262}, + {from: 230, to: 320}, + {from: 230, to: 344}, + {from: 230, to: 354}, + {from: 230, to: 361}, + {from: 230, to: 362}, + {from: 230, to: 445}, + {from: 230, to: 483}, + {from: 230, to: 484}, + {from: 230, to: 512}, + {from: 231, to: 232}, + {from: 231, to: 238}, + {from: 231, to: 258}, + {from: 231, to: 303}, + {from: 231, to: 308}, + {from: 231, to: 335}, + {from: 231, to: 342}, + {from: 231, to: 348}, + {from: 231, to: 415}, + {from: 231, to: 434}, + {from: 231, to: 547}, + {from: 231, to: 575}, + {from: 231, to: 576}, + {from: 231, to: 583}, + {from: 231, to: 586}, + {from: 231, to: 603}, + {from: 231, to: 616}, + {from: 231, to: 627}, + {from: 231, to: 668}, + {from: 231, to: 713}, + {from: 231, to: 717}, + {from: 232, to: 238}, + {from: 232, to: 254}, + {from: 232, to: 258}, + {from: 232, to: 303}, + {from: 232, to: 308}, + {from: 232, to: 335}, + {from: 232, to: 348}, + {from: 232, to: 415}, + {from: 232, to: 434}, + {from: 232, to: 575}, + {from: 232, to: 576}, + {from: 232, to: 583}, + {from: 232, to: 603}, + {from: 232, to: 616}, + {from: 232, to: 668}, + {from: 232, to: 713}, + {from: 233, to: 279}, + {from: 233, to: 280}, + {from: 233, to: 289}, + {from: 233, to: 323}, + {from: 233, to: 331}, + {from: 233, to: 376}, + {from: 233, to: 431}, + {from: 233, to: 436}, + {from: 233, to: 443}, + {from: 233, to: 490}, + {from: 233, to: 529}, + {from: 233, to: 547}, + {from: 233, to: 567}, + {from: 233, to: 586}, + {from: 233, to: 676}, + {from: 233, to: 699}, + {from: 233, to: 717}, + {from: 234, to: 280}, + {from: 234, to: 287}, + {from: 234, to: 304}, + {from: 234, to: 309}, + {from: 234, to: 366}, + {from: 234, to: 369}, + {from: 234, to: 370}, + {from: 234, to: 457}, + {from: 234, to: 469}, + {from: 234, to: 554}, + {from: 234, to: 600}, + {from: 234, to: 608}, + {from: 234, to: 630}, + {from: 234, to: 631}, + {from: 234, to: 672}, + {from: 234, to: 701}, + {from: 234, to: 734}, + {from: 235, to: 236}, + {from: 235, to: 305}, + {from: 235, to: 324}, + {from: 235, to: 334}, + {from: 235, to: 353}, + {from: 235, to: 368}, + {from: 235, to: 429}, + {from: 235, to: 489}, + {from: 235, to: 499}, + {from: 235, to: 548}, + {from: 235, to: 552}, + {from: 235, to: 595}, + {from: 235, to: 710}, + {from: 236, to: 305}, + {from: 236, to: 324}, + {from: 236, to: 334}, + {from: 236, to: 353}, + {from: 236, to: 368}, + {from: 236, to: 429}, + {from: 236, to: 489}, + {from: 236, to: 499}, + {from: 236, to: 548}, + {from: 236, to: 552}, + {from: 236, to: 595}, + {from: 236, to: 710}, + {from: 237, to: 249}, + {from: 237, to: 252}, + {from: 237, to: 291}, + {from: 237, to: 416}, + {from: 237, to: 422}, + {from: 237, to: 447}, + {from: 237, to: 449}, + {from: 237, to: 452}, + {from: 237, to: 478}, + {from: 237, to: 481}, + {from: 237, to: 482}, + {from: 237, to: 622}, + {from: 237, to: 675}, + {from: 237, to: 711}, + {from: 238, to: 258}, + {from: 238, to: 303}, + {from: 238, to: 308}, + {from: 238, to: 335}, + {from: 238, to: 342}, + {from: 238, to: 348}, + {from: 238, to: 415}, + {from: 238, to: 434}, + {from: 238, to: 547}, + {from: 238, to: 575}, + {from: 238, to: 576}, + {from: 238, to: 583}, + {from: 238, to: 586}, + {from: 238, to: 603}, + {from: 238, to: 616}, + {from: 238, to: 627}, + {from: 238, to: 668}, + {from: 238, to: 713}, + {from: 238, to: 717}, + {from: 239, to: 262}, + {from: 239, to: 320}, + {from: 239, to: 344}, + {from: 239, to: 354}, + {from: 239, to: 361}, + {from: 239, to: 362}, + {from: 239, to: 391}, + {from: 239, to: 445}, + {from: 239, to: 483}, + {from: 239, to: 484}, + {from: 239, to: 512}, + {from: 240, to: 352}, + {from: 240, to: 359}, + {from: 240, to: 430}, + {from: 240, to: 434}, + {from: 240, to: 461}, + {from: 240, to: 463}, + {from: 240, to: 486}, + {from: 240, to: 491}, + {from: 240, to: 521}, + {from: 240, to: 531}, + {from: 240, to: 603}, + {from: 240, to: 607}, + {from: 240, to: 634}, + {from: 240, to: 711}, + {from: 241, to: 260}, + {from: 241, to: 266}, + {from: 241, to: 271}, + {from: 241, to: 339}, + {from: 241, to: 364}, + {from: 241, to: 453}, + {from: 241, to: 480}, + {from: 241, to: 497}, + {from: 241, to: 504}, + {from: 241, to: 578}, + {from: 241, to: 596}, + {from: 241, to: 602}, + {from: 241, to: 610}, + {from: 241, to: 661}, + {from: 241, to: 665}, + {from: 241, to: 690}, + {from: 241, to: 692}, + {from: 241, to: 693}, + {from: 241, to: 721}, + {from: 241, to: 723}, + {from: 242, to: 265}, + {from: 242, to: 326}, + {from: 242, to: 341}, + {from: 242, to: 365}, + {from: 242, to: 375}, + {from: 242, to: 406}, + {from: 242, to: 476}, + {from: 242, to: 502}, + {from: 242, to: 513}, + {from: 242, to: 530}, + {from: 242, to: 544}, + {from: 242, to: 574}, + {from: 242, to: 681}, + {from: 242, to: 683}, + {from: 242, to: 718}, + {from: 243, to: 292}, + {from: 243, to: 293}, + {from: 243, to: 439}, + {from: 243, to: 540}, + {from: 243, to: 568}, + {from: 243, to: 640}, + {from: 243, to: 641}, + {from: 243, to: 695}, + {from: 243, to: 704}, + {from: 243, to: 708}, + {from: 243, to: 732}, + {from: 243, to: 733}, + {from: 244, to: 314}, + {from: 244, to: 325}, + {from: 244, to: 338}, + {from: 244, to: 345}, + {from: 244, to: 350}, + {from: 244, to: 396}, + {from: 244, to: 417}, + {from: 244, to: 496}, + {from: 244, to: 507}, + {from: 244, to: 534}, + {from: 244, to: 566}, + {from: 244, to: 606}, + {from: 244, to: 613}, + {from: 244, to: 630}, + {from: 244, to: 659}, + {from: 244, to: 673}, + {from: 244, to: 682}, + {from: 244, to: 714}, + {from: 245, to: 294}, + {from: 245, to: 316}, + {from: 245, to: 319}, + {from: 245, to: 349}, + {from: 245, to: 368}, + {from: 245, to: 371}, + {from: 245, to: 373}, + {from: 245, to: 397}, + {from: 245, to: 419}, + {from: 245, to: 429}, + {from: 245, to: 489}, + {from: 245, to: 529}, + {from: 245, to: 569}, + {from: 245, to: 572}, + {from: 245, to: 591}, + {from: 245, to: 629}, + {from: 245, to: 643}, + {from: 245, to: 644}, + {from: 245, to: 719}, + {from: 245, to: 720}, + {from: 246, to: 257}, + {from: 246, to: 297}, + {from: 246, to: 322}, + {from: 246, to: 398}, + {from: 246, to: 436}, + {from: 246, to: 474}, + {from: 246, to: 485}, + {from: 246, to: 516}, + {from: 246, to: 553}, + {from: 246, to: 621}, + {from: 246, to: 632}, + {from: 246, to: 638}, + {from: 246, to: 639}, + {from: 246, to: 657}, + {from: 246, to: 671}, + {from: 246, to: 696}, + {from: 246, to: 715}, + {from: 246, to: 726}, + {from: 247, to: 273}, + {from: 247, to: 284}, + {from: 247, to: 306}, + {from: 247, to: 315}, + {from: 247, to: 380}, + {from: 247, to: 389}, + {from: 247, to: 467}, + {from: 247, to: 495}, + {from: 247, to: 570}, + {from: 247, to: 584}, + {from: 247, to: 598}, + {from: 247, to: 599}, + {from: 247, to: 666}, + {from: 248, to: 336}, + {from: 248, to: 343}, + {from: 248, to: 360}, + {from: 248, to: 378}, + {from: 248, to: 388}, + {from: 248, to: 448}, + {from: 248, to: 501}, + {from: 248, to: 506}, + {from: 248, to: 550}, + {from: 248, to: 563}, + {from: 248, to: 588}, + {from: 248, to: 617}, + {from: 248, to: 712}, + {from: 248, to: 727}, + {from: 249, to: 252}, + {from: 249, to: 291}, + {from: 249, to: 404}, + {from: 249, to: 416}, + {from: 249, to: 422}, + {from: 249, to: 447}, + {from: 249, to: 449}, + {from: 249, to: 452}, + {from: 249, to: 461}, + {from: 249, to: 478}, + {from: 249, to: 481}, + {from: 249, to: 482}, + {from: 249, to: 483}, + {from: 249, to: 565}, + {from: 249, to: 622}, + {from: 249, to: 661}, + {from: 249, to: 675}, + {from: 250, to: 251}, + {from: 250, to: 253}, + {from: 250, to: 254}, + {from: 250, to: 255}, + {from: 250, to: 356}, + {from: 250, to: 400}, + {from: 250, to: 401}, + {from: 250, to: 402}, + {from: 250, to: 410}, + {from: 250, to: 423}, + {from: 250, to: 482}, + {from: 250, to: 545}, + {from: 250, to: 556}, + {from: 250, to: 557}, + {from: 250, to: 558}, + {from: 250, to: 653}, + {from: 250, to: 656}, + {from: 250, to: 660}, + {from: 250, to: 674}, + {from: 250, to: 694}, + {from: 250, to: 696}, + {from: 251, to: 253}, + {from: 251, to: 254}, + {from: 251, to: 255}, + {from: 251, to: 336}, + {from: 251, to: 356}, + {from: 251, to: 400}, + {from: 251, to: 401}, + {from: 251, to: 402}, + {from: 251, to: 410}, + {from: 251, to: 423}, + {from: 251, to: 545}, + {from: 251, to: 556}, + {from: 251, to: 557}, + {from: 251, to: 558}, + {from: 251, to: 656}, + {from: 251, to: 660}, + {from: 251, to: 674}, + {from: 251, to: 694}, + {from: 251, to: 696}, + {from: 252, to: 291}, + {from: 252, to: 404}, + {from: 252, to: 416}, + {from: 252, to: 422}, + {from: 252, to: 447}, + {from: 252, to: 449}, + {from: 252, to: 452}, + {from: 252, to: 461}, + {from: 252, to: 478}, + {from: 252, to: 481}, + {from: 252, to: 482}, + {from: 252, to: 483}, + {from: 252, to: 565}, + {from: 252, to: 622}, + {from: 252, to: 661}, + {from: 252, to: 675}, + {from: 253, to: 254}, + {from: 253, to: 255}, + {from: 253, to: 356}, + {from: 253, to: 400}, + {from: 253, to: 401}, + {from: 253, to: 402}, + {from: 253, to: 410}, + {from: 253, to: 423}, + {from: 253, to: 545}, + {from: 253, to: 556}, + {from: 253, to: 557}, + {from: 253, to: 558}, + {from: 253, to: 656}, + {from: 253, to: 660}, + {from: 253, to: 674}, + {from: 253, to: 694}, + {from: 253, to: 696}, + {from: 254, to: 255}, + {from: 254, to: 356}, + {from: 254, to: 400}, + {from: 254, to: 401}, + {from: 254, to: 402}, + {from: 254, to: 410}, + {from: 254, to: 423}, + {from: 254, to: 545}, + {from: 254, to: 556}, + {from: 254, to: 557}, + {from: 254, to: 558}, + {from: 254, to: 656}, + {from: 254, to: 660}, + {from: 254, to: 674}, + {from: 254, to: 694}, + {from: 254, to: 696}, + {from: 255, to: 356}, + {from: 255, to: 400}, + {from: 255, to: 401}, + {from: 255, to: 402}, + {from: 255, to: 410}, + {from: 255, to: 423}, + {from: 255, to: 545}, + {from: 255, to: 556}, + {from: 255, to: 557}, + {from: 255, to: 558}, + {from: 255, to: 656}, + {from: 255, to: 660}, + {from: 255, to: 674}, + {from: 255, to: 694}, + {from: 255, to: 696}, + {from: 256, to: 267}, + {from: 256, to: 275}, + {from: 256, to: 276}, + {from: 256, to: 295}, + {from: 256, to: 317}, + {from: 256, to: 318}, + {from: 256, to: 355}, + {from: 256, to: 357}, + {from: 256, to: 446}, + {from: 256, to: 509}, + {from: 256, to: 510}, + {from: 256, to: 546}, + {from: 256, to: 564}, + {from: 256, to: 581}, + {from: 256, to: 592}, + {from: 257, to: 297}, + {from: 257, to: 322}, + {from: 257, to: 398}, + {from: 257, to: 445}, + {from: 257, to: 474}, + {from: 257, to: 485}, + {from: 257, to: 553}, + {from: 257, to: 621}, + {from: 257, to: 632}, + {from: 257, to: 638}, + {from: 257, to: 639}, + {from: 257, to: 657}, + {from: 257, to: 671}, + {from: 257, to: 692}, + {from: 257, to: 715}, + {from: 257, to: 723}, + {from: 257, to: 726}, + {from: 258, to: 303}, + {from: 258, to: 308}, + {from: 258, to: 335}, + {from: 258, to: 348}, + {from: 258, to: 398}, + {from: 258, to: 415}, + {from: 258, to: 430}, + {from: 258, to: 434}, + {from: 258, to: 440}, + {from: 258, to: 575}, + {from: 258, to: 576}, + {from: 258, to: 583}, + {from: 258, to: 603}, + {from: 258, to: 616}, + {from: 258, to: 654}, + {from: 258, to: 668}, + {from: 258, to: 702}, + {from: 258, to: 713}, + {from: 259, to: 342}, + {from: 259, to: 363}, + {from: 259, to: 379}, + {from: 259, to: 383}, + {from: 259, to: 403}, + {from: 259, to: 500}, + {from: 259, to: 505}, + {from: 259, to: 537}, + {from: 259, to: 553}, + {from: 259, to: 574}, + {from: 259, to: 587}, + {from: 259, to: 597}, + {from: 259, to: 649}, + {from: 259, to: 691}, + {from: 259, to: 702}, + {from: 259, to: 706}, + {from: 260, to: 261}, + {from: 260, to: 266}, + {from: 260, to: 271}, + {from: 260, to: 279}, + {from: 260, to: 339}, + {from: 260, to: 364}, + {from: 260, to: 366}, + {from: 260, to: 422}, + {from: 260, to: 453}, + {from: 260, to: 504}, + {from: 260, to: 572}, + {from: 260, to: 578}, + {from: 260, to: 591}, + {from: 260, to: 596}, + {from: 260, to: 602}, + {from: 260, to: 610}, + {from: 260, to: 661}, + {from: 260, to: 665}, + {from: 260, to: 690}, + {from: 260, to: 692}, + {from: 260, to: 693}, + {from: 260, to: 721}, + {from: 260, to: 723}, + {from: 261, to: 279}, + {from: 261, to: 287}, + {from: 261, to: 319}, + {from: 261, to: 358}, + {from: 261, to: 366}, + {from: 261, to: 419}, + {from: 261, to: 422}, + {from: 261, to: 424}, + {from: 261, to: 450}, + {from: 261, to: 451}, + {from: 261, to: 462}, + {from: 261, to: 487}, + {from: 261, to: 555}, + {from: 261, to: 572}, + {from: 261, to: 591}, + {from: 261, to: 600}, + {from: 261, to: 608}, + {from: 261, to: 642}, + {from: 261, to: 645}, + {from: 261, to: 693}, + {from: 262, to: 320}, + {from: 262, to: 344}, + {from: 262, to: 354}, + {from: 262, to: 361}, + {from: 262, to: 362}, + {from: 262, to: 445}, + {from: 262, to: 483}, + {from: 262, to: 484}, + {from: 262, to: 512}, + {from: 263, to: 299}, + {from: 263, to: 300}, + {from: 263, to: 301}, + {from: 263, to: 432}, + {from: 263, to: 444}, + {from: 263, to: 455}, + {from: 263, to: 469}, + {from: 263, to: 514}, + {from: 263, to: 535}, + {from: 263, to: 539}, + {from: 263, to: 542}, + {from: 263, to: 624}, + {from: 263, to: 653}, + {from: 263, to: 660}, + {from: 263, to: 669}, + {from: 263, to: 698}, + {from: 264, to: 281}, + {from: 264, to: 282}, + {from: 264, to: 285}, + {from: 264, to: 382}, + {from: 264, to: 438}, + {from: 264, to: 441}, + {from: 264, to: 465}, + {from: 264, to: 466}, + {from: 264, to: 491}, + {from: 264, to: 646}, + {from: 264, to: 647}, + {from: 264, to: 650}, + {from: 264, to: 651}, + {from: 264, to: 689}, + {from: 264, to: 697}, + {from: 264, to: 724}, + {from: 264, to: 725}, + {from: 264, to: 729}, + {from: 264, to: 730}, + {from: 264, to: 734}, + {from: 264, to: 735}, + {from: 265, to: 326}, + {from: 265, to: 341}, + {from: 265, to: 365}, + {from: 265, to: 375}, + {from: 265, to: 406}, + {from: 265, to: 476}, + {from: 265, to: 502}, + {from: 265, to: 513}, + {from: 265, to: 530}, + {from: 265, to: 544}, + {from: 265, to: 681}, + {from: 265, to: 683}, + {from: 266, to: 271}, + {from: 266, to: 339}, + {from: 266, to: 364}, + {from: 266, to: 453}, + {from: 266, to: 504}, + {from: 266, to: 578}, + {from: 266, to: 596}, + {from: 266, to: 602}, + {from: 266, to: 610}, + {from: 266, to: 661}, + {from: 266, to: 665}, + {from: 266, to: 690}, + {from: 266, to: 692}, + {from: 266, to: 693}, + {from: 266, to: 721}, + {from: 266, to: 723}, + {from: 267, to: 275}, + {from: 267, to: 276}, + {from: 267, to: 295}, + {from: 267, to: 317}, + {from: 267, to: 318}, + {from: 267, to: 355}, + {from: 267, to: 357}, + {from: 267, to: 446}, + {from: 267, to: 494}, + {from: 267, to: 509}, + {from: 267, to: 510}, + {from: 267, to: 546}, + {from: 267, to: 564}, + {from: 267, to: 581}, + {from: 267, to: 592}, + {from: 268, to: 269}, + {from: 268, to: 283}, + {from: 268, to: 290}, + {from: 268, to: 330}, + {from: 268, to: 377}, + {from: 268, to: 390}, + {from: 268, to: 391}, + {from: 268, to: 392}, + {from: 268, to: 393}, + {from: 268, to: 394}, + {from: 268, to: 395}, + {from: 268, to: 399}, + {from: 268, to: 405}, + {from: 268, to: 411}, + {from: 268, to: 412}, + {from: 268, to: 413}, + {from: 268, to: 414}, + {from: 268, to: 559}, + {from: 268, to: 560}, + {from: 268, to: 561}, + {from: 268, to: 658}, + {from: 268, to: 731}, + {from: 269, to: 283}, + {from: 269, to: 290}, + {from: 269, to: 330}, + {from: 269, to: 377}, + {from: 269, to: 390}, + {from: 269, to: 391}, + {from: 269, to: 392}, + {from: 269, to: 393}, + {from: 269, to: 394}, + {from: 269, to: 395}, + {from: 269, to: 399}, + {from: 269, to: 405}, + {from: 269, to: 411}, + {from: 269, to: 412}, + {from: 269, to: 413}, + {from: 269, to: 414}, + {from: 269, to: 559}, + {from: 269, to: 560}, + {from: 269, to: 561}, + {from: 269, to: 658}, + {from: 269, to: 731}, + {from: 270, to: 302}, + {from: 270, to: 312}, + {from: 270, to: 497}, + {from: 270, to: 516}, + {from: 270, to: 524}, + {from: 270, to: 538}, + {from: 270, to: 633}, + {from: 270, to: 635}, + {from: 270, to: 636}, + {from: 270, to: 637}, + {from: 270, to: 684}, + {from: 270, to: 688}, + {from: 270, to: 697}, + {from: 270, to: 736}, + {from: 271, to: 339}, + {from: 271, to: 364}, + {from: 271, to: 453}, + {from: 271, to: 504}, + {from: 271, to: 578}, + {from: 271, to: 596}, + {from: 271, to: 602}, + {from: 271, to: 610}, + {from: 271, to: 615}, + {from: 271, to: 661}, + {from: 271, to: 665}, + {from: 271, to: 690}, + {from: 271, to: 692}, + {from: 271, to: 693}, + {from: 271, to: 721}, + {from: 271, to: 723}, + {from: 272, to: 340}, + {from: 272, to: 346}, + {from: 272, to: 347}, + {from: 272, to: 387}, + {from: 272, to: 404}, + {from: 272, to: 437}, + {from: 272, to: 503}, + {from: 272, to: 520}, + {from: 272, to: 590}, + {from: 272, to: 628}, + {from: 272, to: 664}, + {from: 272, to: 670}, + {from: 272, to: 709}, + {from: 273, to: 284}, + {from: 273, to: 306}, + {from: 273, to: 315}, + {from: 273, to: 380}, + {from: 273, to: 389}, + {from: 273, to: 467}, + {from: 273, to: 495}, + {from: 273, to: 570}, + {from: 273, to: 584}, + {from: 273, to: 598}, + {from: 273, to: 599}, + {from: 273, to: 666}, + {from: 274, to: 296}, + {from: 274, to: 418}, + {from: 274, to: 435}, + {from: 274, to: 493}, + {from: 274, to: 494}, + {from: 274, to: 519}, + {from: 274, to: 525}, + {from: 274, to: 526}, + {from: 274, to: 582}, + {from: 274, to: 585}, + {from: 274, to: 605}, + {from: 274, to: 631}, + {from: 274, to: 655}, + {from: 274, to: 722}, + {from: 275, to: 276}, + {from: 275, to: 295}, + {from: 275, to: 304}, + {from: 275, to: 317}, + {from: 275, to: 318}, + {from: 275, to: 352}, + {from: 275, to: 355}, + {from: 275, to: 357}, + {from: 275, to: 369}, + {from: 275, to: 446}, + {from: 275, to: 509}, + {from: 275, to: 510}, + {from: 275, to: 526}, + {from: 275, to: 546}, + {from: 275, to: 564}, + {from: 275, to: 581}, + {from: 275, to: 592}, + {from: 275, to: 652}, + {from: 275, to: 667}, + {from: 276, to: 295}, + {from: 276, to: 317}, + {from: 276, to: 318}, + {from: 276, to: 355}, + {from: 276, to: 357}, + {from: 276, to: 446}, + {from: 276, to: 509}, + {from: 276, to: 510}, + {from: 276, to: 546}, + {from: 276, to: 564}, + {from: 276, to: 581}, + {from: 276, to: 592}, + {from: 277, to: 286}, + {from: 277, to: 332}, + {from: 277, to: 333}, + {from: 277, to: 424}, + {from: 277, to: 428}, + {from: 277, to: 511}, + {from: 277, to: 517}, + {from: 277, to: 528}, + {from: 277, to: 537}, + {from: 277, to: 571}, + {from: 277, to: 580}, + {from: 277, to: 593}, + {from: 277, to: 601}, + {from: 277, to: 618}, + {from: 277, to: 619}, + {from: 277, to: 636}, + {from: 277, to: 652}, + {from: 277, to: 703}, + {from: 277, to: 716}, + {from: 278, to: 321}, + {from: 278, to: 337}, + {from: 278, to: 407}, + {from: 278, to: 420}, + {from: 278, to: 488}, + {from: 278, to: 533}, + {from: 278, to: 579}, + {from: 278, to: 626}, + {from: 278, to: 627}, + {from: 278, to: 662}, + {from: 278, to: 705}, + {from: 279, to: 280}, + {from: 279, to: 289}, + {from: 279, to: 323}, + {from: 279, to: 331}, + {from: 279, to: 366}, + {from: 279, to: 376}, + {from: 279, to: 422}, + {from: 279, to: 431}, + {from: 279, to: 436}, + {from: 279, to: 443}, + {from: 279, to: 490}, + {from: 279, to: 529}, + {from: 279, to: 547}, + {from: 279, to: 567}, + {from: 279, to: 572}, + {from: 279, to: 586}, + {from: 279, to: 591}, + {from: 279, to: 676}, + {from: 279, to: 693}, + {from: 279, to: 699}, + {from: 279, to: 717}, + {from: 280, to: 287}, + {from: 280, to: 289}, + {from: 280, to: 323}, + {from: 280, to: 331}, + {from: 280, to: 376}, + {from: 280, to: 431}, + {from: 280, to: 436}, + {from: 280, to: 443}, + {from: 280, to: 469}, + {from: 280, to: 490}, + {from: 280, to: 529}, + {from: 280, to: 547}, + {from: 280, to: 567}, + {from: 280, to: 586}, + {from: 280, to: 600}, + {from: 280, to: 608}, + {from: 280, to: 631}, + {from: 280, to: 676}, + {from: 280, to: 699}, + {from: 280, to: 717}, + {from: 280, to: 734}, + {from: 281, to: 282}, + {from: 281, to: 285}, + {from: 281, to: 382}, + {from: 281, to: 438}, + {from: 281, to: 441}, + {from: 281, to: 465}, + {from: 281, to: 466}, + {from: 281, to: 491}, + {from: 281, to: 614}, + {from: 281, to: 646}, + {from: 281, to: 647}, + {from: 281, to: 650}, + {from: 281, to: 651}, + {from: 281, to: 689}, + {from: 281, to: 724}, + {from: 281, to: 725}, + {from: 281, to: 729}, + {from: 281, to: 730}, + {from: 281, to: 734}, + {from: 281, to: 735}, + {from: 282, to: 285}, + {from: 282, to: 364}, + {from: 282, to: 382}, + {from: 282, to: 438}, + {from: 282, to: 441}, + {from: 282, to: 465}, + {from: 282, to: 466}, + {from: 282, to: 491}, + {from: 282, to: 646}, + {from: 282, to: 647}, + {from: 282, to: 650}, + {from: 282, to: 651}, + {from: 282, to: 683}, + {from: 282, to: 689}, + {from: 282, to: 724}, + {from: 282, to: 725}, + {from: 282, to: 729}, + {from: 282, to: 730}, + {from: 282, to: 734}, + {from: 282, to: 735}, + {from: 283, to: 290}, + {from: 283, to: 330}, + {from: 283, to: 377}, + {from: 283, to: 390}, + {from: 283, to: 391}, + {from: 283, to: 392}, + {from: 283, to: 393}, + {from: 283, to: 394}, + {from: 283, to: 395}, + {from: 283, to: 399}, + {from: 283, to: 405}, + {from: 283, to: 411}, + {from: 283, to: 412}, + {from: 283, to: 413}, + {from: 283, to: 414}, + {from: 283, to: 559}, + {from: 283, to: 560}, + {from: 283, to: 561}, + {from: 283, to: 566}, + {from: 283, to: 658}, + {from: 283, to: 731}, + {from: 284, to: 306}, + {from: 284, to: 315}, + {from: 284, to: 380}, + {from: 284, to: 389}, + {from: 284, to: 467}, + {from: 284, to: 495}, + {from: 284, to: 570}, + {from: 284, to: 584}, + {from: 284, to: 598}, + {from: 284, to: 599}, + {from: 284, to: 666}, + {from: 285, to: 382}, + {from: 285, to: 438}, + {from: 285, to: 441}, + {from: 285, to: 465}, + {from: 285, to: 466}, + {from: 285, to: 491}, + {from: 285, to: 646}, + {from: 285, to: 647}, + {from: 285, to: 650}, + {from: 285, to: 651}, + {from: 285, to: 689}, + {from: 285, to: 724}, + {from: 285, to: 725}, + {from: 285, to: 729}, + {from: 285, to: 730}, + {from: 285, to: 734}, + {from: 285, to: 735}, + {from: 286, to: 326}, + {from: 286, to: 332}, + {from: 286, to: 333}, + {from: 286, to: 428}, + {from: 286, to: 511}, + {from: 286, to: 528}, + {from: 286, to: 571}, + {from: 286, to: 580}, + {from: 286, to: 593}, + {from: 286, to: 601}, + {from: 286, to: 618}, + {from: 286, to: 619}, + {from: 286, to: 652}, + {from: 286, to: 703}, + {from: 286, to: 716}, + {from: 287, to: 319}, + {from: 287, to: 358}, + {from: 287, to: 419}, + {from: 287, to: 424}, + {from: 287, to: 450}, + {from: 287, to: 451}, + {from: 287, to: 462}, + {from: 287, to: 469}, + {from: 287, to: 487}, + {from: 287, to: 555}, + {from: 287, to: 600}, + {from: 287, to: 608}, + {from: 287, to: 631}, + {from: 287, to: 642}, + {from: 287, to: 645}, + {from: 287, to: 734}, + {from: 288, to: 311}, + {from: 288, to: 381}, + {from: 288, to: 409}, + {from: 288, to: 421}, + {from: 288, to: 425}, + {from: 288, to: 440}, + {from: 288, to: 472}, + {from: 288, to: 473}, + {from: 288, to: 508}, + {from: 288, to: 521}, + {from: 288, to: 522}, + {from: 288, to: 523}, + {from: 288, to: 525}, + {from: 288, to: 527}, + {from: 288, to: 543}, + {from: 288, to: 562}, + {from: 288, to: 565}, + {from: 288, to: 567}, + {from: 288, to: 589}, + {from: 288, to: 594}, + {from: 288, to: 604}, + {from: 288, to: 663}, + {from: 288, to: 728}, + {from: 289, to: 323}, + {from: 289, to: 331}, + {from: 289, to: 376}, + {from: 289, to: 428}, + {from: 289, to: 431}, + {from: 289, to: 436}, + {from: 289, to: 443}, + {from: 289, to: 490}, + {from: 289, to: 529}, + {from: 289, to: 532}, + {from: 289, to: 540}, + {from: 289, to: 547}, + {from: 289, to: 567}, + {from: 289, to: 586}, + {from: 289, to: 676}, + {from: 289, to: 699}, + {from: 289, to: 704}, + {from: 289, to: 717}, + {from: 289, to: 732}, + {from: 290, to: 330}, + {from: 290, to: 377}, + {from: 290, to: 390}, + {from: 290, to: 391}, + {from: 290, to: 392}, + {from: 290, to: 393}, + {from: 290, to: 394}, + {from: 290, to: 395}, + {from: 290, to: 399}, + {from: 290, to: 405}, + {from: 290, to: 411}, + {from: 290, to: 412}, + {from: 290, to: 413}, + {from: 290, to: 414}, + {from: 290, to: 559}, + {from: 290, to: 560}, + {from: 290, to: 561}, + {from: 290, to: 658}, + {from: 290, to: 689}, + {from: 290, to: 731}, + {from: 291, to: 382}, + {from: 291, to: 416}, + {from: 291, to: 422}, + {from: 291, to: 447}, + {from: 291, to: 449}, + {from: 291, to: 452}, + {from: 291, to: 478}, + {from: 291, to: 481}, + {from: 291, to: 482}, + {from: 291, to: 503}, + {from: 291, to: 534}, + {from: 291, to: 622}, + {from: 291, to: 670}, + {from: 291, to: 675}, + {from: 292, to: 293}, + {from: 292, to: 439}, + {from: 292, to: 540}, + {from: 292, to: 568}, + {from: 292, to: 640}, + {from: 292, to: 641}, + {from: 292, to: 695}, + {from: 292, to: 704}, + {from: 292, to: 708}, + {from: 292, to: 732}, + {from: 292, to: 733}, + {from: 293, to: 439}, + {from: 293, to: 540}, + {from: 293, to: 568}, + {from: 293, to: 640}, + {from: 293, to: 641}, + {from: 293, to: 695}, + {from: 293, to: 704}, + {from: 293, to: 708}, + {from: 293, to: 732}, + {from: 293, to: 733}, + {from: 294, to: 316}, + {from: 294, to: 349}, + {from: 294, to: 371}, + {from: 294, to: 373}, + {from: 294, to: 381}, + {from: 294, to: 397}, + {from: 294, to: 432}, + {from: 294, to: 443}, + {from: 294, to: 569}, + {from: 294, to: 571}, + {from: 294, to: 572}, + {from: 294, to: 589}, + {from: 294, to: 591}, + {from: 294, to: 623}, + {from: 294, to: 629}, + {from: 294, to: 643}, + {from: 294, to: 644}, + {from: 294, to: 719}, + {from: 294, to: 720}, + {from: 295, to: 317}, + {from: 295, to: 318}, + {from: 295, to: 355}, + {from: 295, to: 357}, + {from: 295, to: 446}, + {from: 295, to: 509}, + {from: 295, to: 510}, + {from: 295, to: 546}, + {from: 295, to: 564}, + {from: 295, to: 581}, + {from: 295, to: 592}, + {from: 296, to: 418}, + {from: 296, to: 435}, + {from: 296, to: 450}, + {from: 296, to: 493}, + {from: 296, to: 494}, + {from: 296, to: 519}, + {from: 296, to: 525}, + {from: 296, to: 526}, + {from: 296, to: 582}, + {from: 296, to: 585}, + {from: 296, to: 605}, + {from: 296, to: 619}, + {from: 296, to: 631}, + {from: 296, to: 655}, + {from: 296, to: 716}, + {from: 296, to: 722}, + {from: 297, to: 322}, + {from: 297, to: 398}, + {from: 297, to: 474}, + {from: 297, to: 485}, + {from: 297, to: 553}, + {from: 297, to: 621}, + {from: 297, to: 632}, + {from: 297, to: 638}, + {from: 297, to: 639}, + {from: 297, to: 657}, + {from: 297, to: 663}, + {from: 297, to: 671}, + {from: 297, to: 715}, + {from: 297, to: 726}, + {from: 298, to: 307}, + {from: 298, to: 310}, + {from: 298, to: 313}, + {from: 298, to: 458}, + {from: 298, to: 459}, + {from: 298, to: 468}, + {from: 298, to: 470}, + {from: 298, to: 471}, + {from: 298, to: 477}, + {from: 298, to: 479}, + {from: 298, to: 515}, + {from: 298, to: 518}, + {from: 298, to: 541}, + {from: 298, to: 620}, + {from: 298, to: 680}, + {from: 298, to: 686}, + {from: 299, to: 300}, + {from: 299, to: 301}, + {from: 299, to: 384}, + {from: 299, to: 431}, + {from: 299, to: 432}, + {from: 299, to: 444}, + {from: 299, to: 455}, + {from: 299, to: 469}, + {from: 299, to: 514}, + {from: 299, to: 535}, + {from: 299, to: 539}, + {from: 299, to: 542}, + {from: 299, to: 602}, + {from: 299, to: 624}, + {from: 299, to: 653}, + {from: 299, to: 669}, + {from: 299, to: 698}, + {from: 299, to: 703}, + {from: 300, to: 301}, + {from: 300, to: 432}, + {from: 300, to: 444}, + {from: 300, to: 455}, + {from: 300, to: 469}, + {from: 300, to: 514}, + {from: 300, to: 535}, + {from: 300, to: 539}, + {from: 300, to: 542}, + {from: 300, to: 624}, + {from: 300, to: 653}, + {from: 300, to: 662}, + {from: 300, to: 669}, + {from: 300, to: 698}, + {from: 301, to: 384}, + {from: 301, to: 431}, + {from: 301, to: 432}, + {from: 301, to: 444}, + {from: 301, to: 455}, + {from: 301, to: 469}, + {from: 301, to: 514}, + {from: 301, to: 535}, + {from: 301, to: 539}, + {from: 301, to: 542}, + {from: 301, to: 602}, + {from: 301, to: 624}, + {from: 301, to: 653}, + {from: 301, to: 669}, + {from: 301, to: 698}, + {from: 301, to: 703}, + {from: 302, to: 312}, + {from: 302, to: 497}, + {from: 302, to: 516}, + {from: 302, to: 524}, + {from: 302, to: 538}, + {from: 302, to: 633}, + {from: 302, to: 635}, + {from: 302, to: 636}, + {from: 302, to: 637}, + {from: 302, to: 684}, + {from: 302, to: 688}, + {from: 302, to: 697}, + {from: 302, to: 714}, + {from: 302, to: 736}, + {from: 303, to: 308}, + {from: 303, to: 335}, + {from: 303, to: 348}, + {from: 303, to: 409}, + {from: 303, to: 415}, + {from: 303, to: 433}, + {from: 303, to: 434}, + {from: 303, to: 498}, + {from: 303, to: 543}, + {from: 303, to: 573}, + {from: 303, to: 575}, + {from: 303, to: 576}, + {from: 303, to: 583}, + {from: 303, to: 603}, + {from: 303, to: 616}, + {from: 303, to: 629}, + {from: 303, to: 668}, + {from: 303, to: 679}, + {from: 303, to: 713}, + {from: 304, to: 309}, + {from: 304, to: 352}, + {from: 304, to: 366}, + {from: 304, to: 369}, + {from: 304, to: 370}, + {from: 304, to: 457}, + {from: 304, to: 526}, + {from: 304, to: 554}, + {from: 304, to: 630}, + {from: 304, to: 652}, + {from: 304, to: 667}, + {from: 304, to: 672}, + {from: 304, to: 701}, + {from: 305, to: 324}, + {from: 305, to: 334}, + {from: 305, to: 353}, + {from: 305, to: 368}, + {from: 305, to: 429}, + {from: 305, to: 489}, + {from: 305, to: 499}, + {from: 305, to: 548}, + {from: 305, to: 552}, + {from: 305, to: 595}, + {from: 305, to: 710}, + {from: 306, to: 315}, + {from: 306, to: 380}, + {from: 306, to: 389}, + {from: 306, to: 467}, + {from: 306, to: 495}, + {from: 306, to: 570}, + {from: 306, to: 584}, + {from: 306, to: 598}, + {from: 306, to: 599}, + {from: 306, to: 666}, + {from: 307, to: 310}, + {from: 307, to: 313}, + {from: 307, to: 458}, + {from: 307, to: 459}, + {from: 307, to: 468}, + {from: 307, to: 470}, + {from: 307, to: 471}, + {from: 307, to: 477}, + {from: 307, to: 479}, + {from: 307, to: 515}, + {from: 307, to: 518}, + {from: 307, to: 541}, + {from: 307, to: 620}, + {from: 307, to: 680}, + {from: 307, to: 686}, + {from: 308, to: 335}, + {from: 308, to: 348}, + {from: 308, to: 415}, + {from: 308, to: 434}, + {from: 308, to: 462}, + {from: 308, to: 555}, + {from: 308, to: 575}, + {from: 308, to: 576}, + {from: 308, to: 583}, + {from: 308, to: 603}, + {from: 308, to: 616}, + {from: 308, to: 642}, + {from: 308, to: 668}, + {from: 308, to: 707}, + {from: 308, to: 713}, + {from: 308, to: 726}, + {from: 309, to: 332}, + {from: 309, to: 366}, + {from: 309, to: 369}, + {from: 309, to: 370}, + {from: 309, to: 457}, + {from: 309, to: 554}, + {from: 309, to: 630}, + {from: 309, to: 645}, + {from: 309, to: 672}, + {from: 309, to: 701}, + {from: 310, to: 313}, + {from: 310, to: 458}, + {from: 310, to: 459}, + {from: 310, to: 468}, + {from: 310, to: 470}, + {from: 310, to: 471}, + {from: 310, to: 477}, + {from: 310, to: 479}, + {from: 310, to: 515}, + {from: 310, to: 518}, + {from: 310, to: 541}, + {from: 310, to: 620}, + {from: 310, to: 680}, + {from: 310, to: 686}, + {from: 311, to: 384}, + {from: 311, to: 386}, + {from: 311, to: 408}, + {from: 311, to: 460}, + {from: 311, to: 522}, + {from: 311, to: 525}, + {from: 311, to: 527}, + {from: 311, to: 532}, + {from: 311, to: 567}, + {from: 311, to: 612}, + {from: 311, to: 625}, + {from: 311, to: 654}, + {from: 311, to: 667}, + {from: 311, to: 677}, + {from: 311, to: 679}, + {from: 311, to: 685}, + {from: 311, to: 707}, + {from: 312, to: 497}, + {from: 312, to: 516}, + {from: 312, to: 524}, + {from: 312, to: 538}, + {from: 312, to: 633}, + {from: 312, to: 635}, + {from: 312, to: 636}, + {from: 312, to: 637}, + {from: 312, to: 684}, + {from: 312, to: 688}, + {from: 312, to: 697}, + {from: 312, to: 736}, + {from: 313, to: 458}, + {from: 313, to: 459}, + {from: 313, to: 468}, + {from: 313, to: 470}, + {from: 313, to: 471}, + {from: 313, to: 477}, + {from: 313, to: 479}, + {from: 313, to: 515}, + {from: 313, to: 518}, + {from: 313, to: 541}, + {from: 313, to: 620}, + {from: 313, to: 680}, + {from: 313, to: 686}, + {from: 314, to: 325}, + {from: 314, to: 338}, + {from: 314, to: 345}, + {from: 314, to: 350}, + {from: 314, to: 396}, + {from: 314, to: 417}, + {from: 314, to: 496}, + {from: 314, to: 507}, + {from: 314, to: 534}, + {from: 314, to: 566}, + {from: 314, to: 606}, + {from: 314, to: 613}, + {from: 314, to: 659}, + {from: 314, to: 673}, + {from: 314, to: 682}, + {from: 314, to: 714}, + {from: 315, to: 380}, + {from: 315, to: 389}, + {from: 315, to: 467}, + {from: 315, to: 495}, + {from: 315, to: 570}, + {from: 315, to: 584}, + {from: 315, to: 598}, + {from: 315, to: 599}, + {from: 315, to: 666}, + {from: 316, to: 327}, + {from: 316, to: 349}, + {from: 316, to: 371}, + {from: 316, to: 373}, + {from: 316, to: 375}, + {from: 316, to: 397}, + {from: 316, to: 442}, + {from: 316, to: 454}, + {from: 316, to: 455}, + {from: 316, to: 569}, + {from: 316, to: 572}, + {from: 316, to: 577}, + {from: 316, to: 591}, + {from: 316, to: 629}, + {from: 316, to: 643}, + {from: 316, to: 644}, + {from: 316, to: 678}, + {from: 316, to: 687}, + {from: 316, to: 719}, + {from: 316, to: 720}, + {from: 316, to: 721}, + {from: 317, to: 318}, + {from: 317, to: 355}, + {from: 317, to: 357}, + {from: 317, to: 446}, + {from: 317, to: 509}, + {from: 317, to: 510}, + {from: 317, to: 546}, + {from: 317, to: 564}, + {from: 317, to: 581}, + {from: 317, to: 592}, + {from: 318, to: 355}, + {from: 318, to: 357}, + {from: 318, to: 371}, + {from: 318, to: 446}, + {from: 318, to: 460}, + {from: 318, to: 509}, + {from: 318, to: 510}, + {from: 318, to: 528}, + {from: 318, to: 546}, + {from: 318, to: 562}, + {from: 318, to: 564}, + {from: 318, to: 576}, + {from: 318, to: 581}, + {from: 318, to: 592}, + {from: 318, to: 606}, + {from: 318, to: 646}, + {from: 318, to: 713}, + {from: 319, to: 349}, + {from: 319, to: 358}, + {from: 319, to: 368}, + {from: 319, to: 419}, + {from: 319, to: 424}, + {from: 319, to: 429}, + {from: 319, to: 450}, + {from: 319, to: 451}, + {from: 319, to: 462}, + {from: 319, to: 487}, + {from: 319, to: 489}, + {from: 319, to: 529}, + {from: 319, to: 555}, + {from: 319, to: 569}, + {from: 319, to: 600}, + {from: 319, to: 608}, + {from: 319, to: 642}, + {from: 319, to: 643}, + {from: 319, to: 645}, + {from: 319, to: 720}, + {from: 320, to: 344}, + {from: 320, to: 354}, + {from: 320, to: 361}, + {from: 320, to: 362}, + {from: 320, to: 367}, + {from: 320, to: 445}, + {from: 320, to: 483}, + {from: 320, to: 484}, + {from: 320, to: 512}, + {from: 320, to: 609}, + {from: 321, to: 337}, + {from: 321, to: 340}, + {from: 321, to: 407}, + {from: 321, to: 420}, + {from: 321, to: 488}, + {from: 321, to: 533}, + {from: 321, to: 579}, + {from: 321, to: 626}, + {from: 321, to: 627}, + {from: 321, to: 662}, + {from: 321, to: 705}, + {from: 322, to: 398}, + {from: 322, to: 474}, + {from: 322, to: 485}, + {from: 322, to: 553}, + {from: 322, to: 621}, + {from: 322, to: 632}, + {from: 322, to: 638}, + {from: 322, to: 639}, + {from: 322, to: 657}, + {from: 322, to: 671}, + {from: 322, to: 715}, + {from: 322, to: 726}, + {from: 323, to: 331}, + {from: 323, to: 376}, + {from: 323, to: 431}, + {from: 323, to: 436}, + {from: 323, to: 443}, + {from: 323, to: 490}, + {from: 323, to: 529}, + {from: 323, to: 547}, + {from: 323, to: 567}, + {from: 323, to: 586}, + {from: 323, to: 676}, + {from: 323, to: 699}, + {from: 323, to: 717}, + {from: 324, to: 334}, + {from: 324, to: 353}, + {from: 324, to: 368}, + {from: 324, to: 429}, + {from: 324, to: 489}, + {from: 324, to: 499}, + {from: 324, to: 548}, + {from: 324, to: 552}, + {from: 324, to: 595}, + {from: 324, to: 710}, + {from: 325, to: 338}, + {from: 325, to: 345}, + {from: 325, to: 350}, + {from: 325, to: 396}, + {from: 325, to: 417}, + {from: 325, to: 496}, + {from: 325, to: 507}, + {from: 325, to: 511}, + {from: 325, to: 534}, + {from: 325, to: 539}, + {from: 325, to: 566}, + {from: 325, to: 606}, + {from: 325, to: 613}, + {from: 325, to: 659}, + {from: 325, to: 673}, + {from: 325, to: 682}, + {from: 325, to: 714}, + {from: 326, to: 341}, + {from: 326, to: 365}, + {from: 326, to: 375}, + {from: 326, to: 406}, + {from: 326, to: 476}, + {from: 326, to: 502}, + {from: 326, to: 513}, + {from: 326, to: 530}, + {from: 326, to: 544}, + {from: 326, to: 681}, + {from: 326, to: 683}, + {from: 327, to: 374}, + {from: 327, to: 375}, + {from: 327, to: 385}, + {from: 327, to: 433}, + {from: 327, to: 442}, + {from: 327, to: 454}, + {from: 327, to: 455}, + {from: 327, to: 475}, + {from: 327, to: 480}, + {from: 327, to: 498}, + {from: 327, to: 517}, + {from: 327, to: 573}, + {from: 327, to: 577}, + {from: 327, to: 611}, + {from: 327, to: 614}, + {from: 327, to: 623}, + {from: 327, to: 648}, + {from: 327, to: 678}, + {from: 327, to: 687}, + {from: 327, to: 721}, + {from: 328, to: 329}, + {from: 328, to: 351}, + {from: 328, to: 367}, + {from: 328, to: 372}, + {from: 328, to: 426}, + {from: 328, to: 427}, + {from: 328, to: 456}, + {from: 328, to: 464}, + {from: 328, to: 492}, + {from: 328, to: 536}, + {from: 328, to: 549}, + {from: 328, to: 551}, + {from: 328, to: 609}, + {from: 328, to: 615}, + {from: 328, to: 700}, + {from: 328, to: 718}, + {from: 329, to: 343}, + {from: 329, to: 351}, + {from: 329, to: 367}, + {from: 329, to: 372}, + {from: 329, to: 426}, + {from: 329, to: 427}, + {from: 329, to: 456}, + {from: 329, to: 464}, + {from: 329, to: 492}, + {from: 329, to: 536}, + {from: 329, to: 549}, + {from: 329, to: 551}, + {from: 329, to: 563}, + {from: 329, to: 609}, + {from: 329, to: 615}, + {from: 329, to: 700}, + {from: 329, to: 718}, + {from: 330, to: 377}, + {from: 330, to: 390}, + {from: 330, to: 391}, + {from: 330, to: 392}, + {from: 330, to: 393}, + {from: 330, to: 394}, + {from: 330, to: 395}, + {from: 330, to: 399}, + {from: 330, to: 405}, + {from: 330, to: 411}, + {from: 330, to: 412}, + {from: 330, to: 413}, + {from: 330, to: 414}, + {from: 330, to: 559}, + {from: 330, to: 560}, + {from: 330, to: 561}, + {from: 330, to: 566}, + {from: 330, to: 658}, + {from: 330, to: 731}, + {from: 331, to: 376}, + {from: 331, to: 431}, + {from: 331, to: 436}, + {from: 331, to: 443}, + {from: 331, to: 490}, + {from: 331, to: 529}, + {from: 331, to: 547}, + {from: 331, to: 567}, + {from: 331, to: 586}, + {from: 331, to: 676}, + {from: 331, to: 699}, + {from: 331, to: 717}, + {from: 332, to: 333}, + {from: 332, to: 428}, + {from: 332, to: 511}, + {from: 332, to: 528}, + {from: 332, to: 571}, + {from: 332, to: 580}, + {from: 332, to: 593}, + {from: 332, to: 601}, + {from: 332, to: 618}, + {from: 332, to: 619}, + {from: 332, to: 645}, + {from: 332, to: 652}, + {from: 332, to: 703}, + {from: 332, to: 716}, + {from: 333, to: 428}, + {from: 333, to: 511}, + {from: 333, to: 528}, + {from: 333, to: 571}, + {from: 333, to: 578}, + {from: 333, to: 580}, + {from: 333, to: 593}, + {from: 333, to: 601}, + {from: 333, to: 618}, + {from: 333, to: 619}, + {from: 333, to: 652}, + {from: 333, to: 655}, + {from: 333, to: 703}, + {from: 333, to: 716}, + {from: 334, to: 353}, + {from: 334, to: 355}, + {from: 334, to: 368}, + {from: 334, to: 429}, + {from: 334, to: 446}, + {from: 334, to: 489}, + {from: 334, to: 499}, + {from: 334, to: 548}, + {from: 334, to: 552}, + {from: 334, to: 595}, + {from: 334, to: 710}, + {from: 335, to: 348}, + {from: 335, to: 415}, + {from: 335, to: 434}, + {from: 335, to: 462}, + {from: 335, to: 555}, + {from: 335, to: 575}, + {from: 335, to: 576}, + {from: 335, to: 583}, + {from: 335, to: 603}, + {from: 335, to: 616}, + {from: 335, to: 642}, + {from: 335, to: 668}, + {from: 335, to: 707}, + {from: 335, to: 713}, + {from: 335, to: 726}, + {from: 336, to: 343}, + {from: 336, to: 356}, + {from: 336, to: 360}, + {from: 336, to: 378}, + {from: 336, to: 388}, + {from: 336, to: 401}, + {from: 336, to: 448}, + {from: 336, to: 501}, + {from: 336, to: 506}, + {from: 336, to: 550}, + {from: 336, to: 563}, + {from: 336, to: 588}, + {from: 336, to: 617}, + {from: 336, to: 712}, + {from: 336, to: 727}, + {from: 337, to: 374}, + {from: 337, to: 387}, + {from: 337, to: 396}, + {from: 337, to: 407}, + {from: 337, to: 420}, + {from: 337, to: 488}, + {from: 337, to: 533}, + {from: 337, to: 579}, + {from: 337, to: 626}, + {from: 337, to: 627}, + {from: 337, to: 633}, + {from: 337, to: 662}, + {from: 337, to: 705}, + {from: 338, to: 345}, + {from: 338, to: 350}, + {from: 338, to: 396}, + {from: 338, to: 417}, + {from: 338, to: 496}, + {from: 338, to: 507}, + {from: 338, to: 534}, + {from: 338, to: 566}, + {from: 338, to: 606}, + {from: 338, to: 613}, + {from: 338, to: 659}, + {from: 338, to: 673}, + {from: 338, to: 682}, + {from: 338, to: 714}, + {from: 339, to: 364}, + {from: 339, to: 453}, + {from: 339, to: 504}, + {from: 339, to: 514}, + {from: 339, to: 578}, + {from: 339, to: 596}, + {from: 339, to: 602}, + {from: 339, to: 610}, + {from: 339, to: 661}, + {from: 339, to: 665}, + {from: 339, to: 690}, + {from: 339, to: 692}, + {from: 339, to: 693}, + {from: 339, to: 721}, + {from: 339, to: 723}, + {from: 340, to: 346}, + {from: 340, to: 347}, + {from: 340, to: 387}, + {from: 340, to: 404}, + {from: 340, to: 437}, + {from: 340, to: 503}, + {from: 340, to: 520}, + {from: 340, to: 590}, + {from: 340, to: 628}, + {from: 340, to: 664}, + {from: 340, to: 670}, + {from: 340, to: 709}, + {from: 341, to: 365}, + {from: 341, to: 375}, + {from: 341, to: 406}, + {from: 341, to: 476}, + {from: 341, to: 502}, + {from: 341, to: 513}, + {from: 341, to: 530}, + {from: 341, to: 544}, + {from: 341, to: 681}, + {from: 341, to: 683}, + {from: 342, to: 363}, + {from: 342, to: 379}, + {from: 342, to: 383}, + {from: 342, to: 403}, + {from: 342, to: 500}, + {from: 342, to: 505}, + {from: 342, to: 537}, + {from: 342, to: 547}, + {from: 342, to: 574}, + {from: 342, to: 586}, + {from: 342, to: 587}, + {from: 342, to: 597}, + {from: 342, to: 627}, + {from: 342, to: 649}, + {from: 342, to: 691}, + {from: 342, to: 702}, + {from: 342, to: 706}, + {from: 342, to: 717}, + {from: 343, to: 360}, + {from: 343, to: 378}, + {from: 343, to: 388}, + {from: 343, to: 448}, + {from: 343, to: 501}, + {from: 343, to: 506}, + {from: 343, to: 550}, + {from: 343, to: 563}, + {from: 343, to: 588}, + {from: 343, to: 617}, + {from: 343, to: 712}, + {from: 343, to: 727}, + {from: 344, to: 354}, + {from: 344, to: 361}, + {from: 344, to: 362}, + {from: 344, to: 445}, + {from: 344, to: 483}, + {from: 344, to: 484}, + {from: 344, to: 512}, + {from: 345, to: 350}, + {from: 345, to: 396}, + {from: 345, to: 417}, + {from: 345, to: 496}, + {from: 345, to: 507}, + {from: 345, to: 534}, + {from: 345, to: 566}, + {from: 345, to: 606}, + {from: 345, to: 613}, + {from: 345, to: 659}, + {from: 345, to: 673}, + {from: 345, to: 682}, + {from: 345, to: 714}, + {from: 345, to: 715}, + {from: 346, to: 347}, + {from: 346, to: 387}, + {from: 346, to: 404}, + {from: 346, to: 437}, + {from: 346, to: 503}, + {from: 346, to: 520}, + {from: 346, to: 590}, + {from: 346, to: 628}, + {from: 346, to: 664}, + {from: 346, to: 670}, + {from: 346, to: 709}, + {from: 347, to: 387}, + {from: 347, to: 404}, + {from: 347, to: 437}, + {from: 347, to: 503}, + {from: 347, to: 520}, + {from: 347, to: 590}, + {from: 347, to: 628}, + {from: 347, to: 664}, + {from: 347, to: 670}, + {from: 347, to: 709}, + {from: 348, to: 398}, + {from: 348, to: 415}, + {from: 348, to: 430}, + {from: 348, to: 434}, + {from: 348, to: 440}, + {from: 348, to: 575}, + {from: 348, to: 576}, + {from: 348, to: 583}, + {from: 348, to: 603}, + {from: 348, to: 616}, + {from: 348, to: 654}, + {from: 348, to: 668}, + {from: 348, to: 702}, + {from: 348, to: 713}, + {from: 349, to: 368}, + {from: 349, to: 371}, + {from: 349, to: 373}, + {from: 349, to: 397}, + {from: 349, to: 419}, + {from: 349, to: 429}, + {from: 349, to: 489}, + {from: 349, to: 529}, + {from: 349, to: 569}, + {from: 349, to: 572}, + {from: 349, to: 591}, + {from: 349, to: 629}, + {from: 349, to: 643}, + {from: 349, to: 644}, + {from: 349, to: 719}, + {from: 349, to: 720}, + {from: 350, to: 396}, + {from: 350, to: 417}, + {from: 350, to: 496}, + {from: 350, to: 507}, + {from: 350, to: 534}, + {from: 350, to: 566}, + {from: 350, to: 606}, + {from: 350, to: 613}, + {from: 350, to: 659}, + {from: 350, to: 673}, + {from: 350, to: 682}, + {from: 350, to: 714}, + {from: 351, to: 367}, + {from: 351, to: 372}, + {from: 351, to: 426}, + {from: 351, to: 427}, + {from: 351, to: 456}, + {from: 351, to: 464}, + {from: 351, to: 492}, + {from: 351, to: 536}, + {from: 351, to: 549}, + {from: 351, to: 551}, + {from: 351, to: 609}, + {from: 351, to: 615}, + {from: 351, to: 700}, + {from: 351, to: 718}, + {from: 352, to: 359}, + {from: 352, to: 369}, + {from: 352, to: 430}, + {from: 352, to: 461}, + {from: 352, to: 463}, + {from: 352, to: 486}, + {from: 352, to: 526}, + {from: 352, to: 531}, + {from: 352, to: 607}, + {from: 352, to: 634}, + {from: 352, to: 652}, + {from: 352, to: 667}, + {from: 352, to: 711}, + {from: 353, to: 368}, + {from: 353, to: 429}, + {from: 353, to: 489}, + {from: 353, to: 499}, + {from: 353, to: 548}, + {from: 353, to: 552}, + {from: 353, to: 595}, + {from: 353, to: 710}, + {from: 354, to: 361}, + {from: 354, to: 362}, + {from: 354, to: 445}, + {from: 354, to: 483}, + {from: 354, to: 484}, + {from: 354, to: 512}, + {from: 355, to: 357}, + {from: 355, to: 446}, + {from: 355, to: 509}, + {from: 355, to: 510}, + {from: 355, to: 546}, + {from: 355, to: 564}, + {from: 355, to: 581}, + {from: 355, to: 592}, + {from: 356, to: 400}, + {from: 356, to: 401}, + {from: 356, to: 402}, + {from: 356, to: 410}, + {from: 356, to: 423}, + {from: 356, to: 545}, + {from: 356, to: 556}, + {from: 356, to: 557}, + {from: 356, to: 558}, + {from: 356, to: 656}, + {from: 356, to: 660}, + {from: 356, to: 674}, + {from: 356, to: 694}, + {from: 356, to: 696}, + {from: 357, to: 446}, + {from: 357, to: 509}, + {from: 357, to: 510}, + {from: 357, to: 546}, + {from: 357, to: 564}, + {from: 357, to: 581}, + {from: 357, to: 592}, + {from: 358, to: 419}, + {from: 358, to: 424}, + {from: 358, to: 450}, + {from: 358, to: 451}, + {from: 358, to: 462}, + {from: 358, to: 487}, + {from: 358, to: 555}, + {from: 358, to: 600}, + {from: 358, to: 608}, + {from: 358, to: 642}, + {from: 358, to: 645}, + {from: 359, to: 373}, + {from: 359, to: 397}, + {from: 359, to: 430}, + {from: 359, to: 461}, + {from: 359, to: 463}, + {from: 359, to: 486}, + {from: 359, to: 531}, + {from: 359, to: 607}, + {from: 359, to: 634}, + {from: 359, to: 677}, + {from: 359, to: 685}, + {from: 359, to: 711}, + {from: 360, to: 378}, + {from: 360, to: 388}, + {from: 360, to: 448}, + {from: 360, to: 501}, + {from: 360, to: 506}, + {from: 360, to: 550}, + {from: 360, to: 563}, + {from: 360, to: 588}, + {from: 360, to: 617}, + {from: 360, to: 712}, + {from: 360, to: 727}, + {from: 361, to: 362}, + {from: 361, to: 445}, + {from: 361, to: 483}, + {from: 361, to: 484}, + {from: 361, to: 512}, + {from: 362, to: 445}, + {from: 362, to: 483}, + {from: 362, to: 484}, + {from: 362, to: 512}, + {from: 363, to: 379}, + {from: 363, to: 383}, + {from: 363, to: 403}, + {from: 363, to: 417}, + {from: 363, to: 500}, + {from: 363, to: 505}, + {from: 363, to: 537}, + {from: 363, to: 574}, + {from: 363, to: 587}, + {from: 363, to: 597}, + {from: 363, to: 649}, + {from: 363, to: 691}, + {from: 363, to: 702}, + {from: 363, to: 706}, + {from: 364, to: 438}, + {from: 364, to: 453}, + {from: 364, to: 504}, + {from: 364, to: 578}, + {from: 364, to: 596}, + {from: 364, to: 602}, + {from: 364, to: 610}, + {from: 364, to: 661}, + {from: 364, to: 665}, + {from: 364, to: 683}, + {from: 364, to: 690}, + {from: 364, to: 692}, + {from: 364, to: 693}, + {from: 364, to: 721}, + {from: 364, to: 723}, + {from: 365, to: 375}, + {from: 365, to: 390}, + {from: 365, to: 406}, + {from: 365, to: 476}, + {from: 365, to: 502}, + {from: 365, to: 513}, + {from: 365, to: 530}, + {from: 365, to: 544}, + {from: 365, to: 681}, + {from: 365, to: 683}, + {from: 366, to: 369}, + {from: 366, to: 370}, + {from: 366, to: 422}, + {from: 366, to: 457}, + {from: 366, to: 554}, + {from: 366, to: 572}, + {from: 366, to: 591}, + {from: 366, to: 630}, + {from: 366, to: 672}, + {from: 366, to: 693}, + {from: 366, to: 701}, + {from: 367, to: 372}, + {from: 367, to: 426}, + {from: 367, to: 427}, + {from: 367, to: 456}, + {from: 367, to: 464}, + {from: 367, to: 492}, + {from: 367, to: 536}, + {from: 367, to: 549}, + {from: 367, to: 551}, + {from: 367, to: 609}, + {from: 367, to: 615}, + {from: 367, to: 700}, + {from: 367, to: 718}, + {from: 368, to: 419}, + {from: 368, to: 429}, + {from: 368, to: 489}, + {from: 368, to: 499}, + {from: 368, to: 529}, + {from: 368, to: 548}, + {from: 368, to: 552}, + {from: 368, to: 569}, + {from: 368, to: 595}, + {from: 368, to: 643}, + {from: 368, to: 710}, + {from: 368, to: 720}, + {from: 369, to: 370}, + {from: 369, to: 457}, + {from: 369, to: 526}, + {from: 369, to: 554}, + {from: 369, to: 630}, + {from: 369, to: 652}, + {from: 369, to: 667}, + {from: 369, to: 672}, + {from: 369, to: 701}, + {from: 370, to: 457}, + {from: 370, to: 554}, + {from: 370, to: 630}, + {from: 370, to: 672}, + {from: 370, to: 701}, + {from: 371, to: 373}, + {from: 371, to: 397}, + {from: 371, to: 460}, + {from: 371, to: 528}, + {from: 371, to: 562}, + {from: 371, to: 569}, + {from: 371, to: 572}, + {from: 371, to: 576}, + {from: 371, to: 591}, + {from: 371, to: 606}, + {from: 371, to: 629}, + {from: 371, to: 643}, + {from: 371, to: 644}, + {from: 371, to: 646}, + {from: 371, to: 713}, + {from: 371, to: 719}, + {from: 371, to: 720}, + {from: 372, to: 426}, + {from: 372, to: 427}, + {from: 372, to: 456}, + {from: 372, to: 464}, + {from: 372, to: 492}, + {from: 372, to: 536}, + {from: 372, to: 549}, + {from: 372, to: 551}, + {from: 372, to: 609}, + {from: 372, to: 615}, + {from: 372, to: 700}, + {from: 372, to: 718}, + {from: 373, to: 397}, + {from: 373, to: 569}, + {from: 373, to: 572}, + {from: 373, to: 591}, + {from: 373, to: 629}, + {from: 373, to: 643}, + {from: 373, to: 644}, + {from: 373, to: 677}, + {from: 373, to: 685}, + {from: 373, to: 719}, + {from: 373, to: 720}, + {from: 374, to: 385}, + {from: 374, to: 387}, + {from: 374, to: 396}, + {from: 374, to: 433}, + {from: 374, to: 442}, + {from: 374, to: 454}, + {from: 374, to: 475}, + {from: 374, to: 480}, + {from: 374, to: 498}, + {from: 374, to: 517}, + {from: 374, to: 573}, + {from: 374, to: 577}, + {from: 374, to: 611}, + {from: 374, to: 614}, + {from: 374, to: 623}, + {from: 374, to: 633}, + {from: 374, to: 648}, + {from: 374, to: 678}, + {from: 374, to: 687}, + {from: 375, to: 406}, + {from: 375, to: 442}, + {from: 375, to: 454}, + {from: 375, to: 455}, + {from: 375, to: 476}, + {from: 375, to: 502}, + {from: 375, to: 513}, + {from: 375, to: 530}, + {from: 375, to: 544}, + {from: 375, to: 577}, + {from: 375, to: 678}, + {from: 375, to: 681}, + {from: 375, to: 683}, + {from: 375, to: 687}, + {from: 375, to: 721}, + {from: 376, to: 431}, + {from: 376, to: 436}, + {from: 376, to: 443}, + {from: 376, to: 490}, + {from: 376, to: 502}, + {from: 376, to: 529}, + {from: 376, to: 547}, + {from: 376, to: 567}, + {from: 376, to: 586}, + {from: 376, to: 676}, + {from: 376, to: 699}, + {from: 376, to: 717}, + {from: 377, to: 390}, + {from: 377, to: 391}, + {from: 377, to: 392}, + {from: 377, to: 393}, + {from: 377, to: 394}, + {from: 377, to: 395}, + {from: 377, to: 399}, + {from: 377, to: 405}, + {from: 377, to: 411}, + {from: 377, to: 412}, + {from: 377, to: 413}, + {from: 377, to: 414}, + {from: 377, to: 559}, + {from: 377, to: 560}, + {from: 377, to: 561}, + {from: 377, to: 658}, + {from: 377, to: 731}, + {from: 378, to: 388}, + {from: 378, to: 399}, + {from: 378, to: 448}, + {from: 378, to: 488}, + {from: 378, to: 501}, + {from: 378, to: 506}, + {from: 378, to: 550}, + {from: 378, to: 561}, + {from: 378, to: 563}, + {from: 378, to: 588}, + {from: 378, to: 617}, + {from: 378, to: 647}, + {from: 378, to: 712}, + {from: 378, to: 727}, + {from: 379, to: 383}, + {from: 379, to: 403}, + {from: 379, to: 500}, + {from: 379, to: 505}, + {from: 379, to: 537}, + {from: 379, to: 574}, + {from: 379, to: 587}, + {from: 379, to: 597}, + {from: 379, to: 649}, + {from: 379, to: 691}, + {from: 379, to: 702}, + {from: 379, to: 706}, + {from: 380, to: 389}, + {from: 380, to: 467}, + {from: 380, to: 495}, + {from: 380, to: 570}, + {from: 380, to: 584}, + {from: 380, to: 598}, + {from: 380, to: 599}, + {from: 380, to: 666}, + {from: 381, to: 409}, + {from: 381, to: 421}, + {from: 381, to: 425}, + {from: 381, to: 432}, + {from: 381, to: 440}, + {from: 381, to: 443}, + {from: 381, to: 472}, + {from: 381, to: 473}, + {from: 381, to: 508}, + {from: 381, to: 521}, + {from: 381, to: 523}, + {from: 381, to: 543}, + {from: 381, to: 562}, + {from: 381, to: 565}, + {from: 381, to: 571}, + {from: 381, to: 589}, + {from: 381, to: 594}, + {from: 381, to: 604}, + {from: 381, to: 623}, + {from: 381, to: 644}, + {from: 381, to: 663}, + {from: 381, to: 719}, + {from: 381, to: 728}, + {from: 382, to: 438}, + {from: 382, to: 441}, + {from: 382, to: 452}, + {from: 382, to: 465}, + {from: 382, to: 466}, + {from: 382, to: 481}, + {from: 382, to: 491}, + {from: 382, to: 503}, + {from: 382, to: 534}, + {from: 382, to: 646}, + {from: 382, to: 647}, + {from: 382, to: 650}, + {from: 382, to: 651}, + {from: 382, to: 670}, + {from: 382, to: 689}, + {from: 382, to: 724}, + {from: 382, to: 725}, + {from: 382, to: 729}, + {from: 382, to: 730}, + {from: 382, to: 734}, + {from: 382, to: 735}, + {from: 383, to: 403}, + {from: 383, to: 500}, + {from: 383, to: 505}, + {from: 383, to: 537}, + {from: 383, to: 574}, + {from: 383, to: 587}, + {from: 383, to: 597}, + {from: 383, to: 649}, + {from: 383, to: 691}, + {from: 383, to: 702}, + {from: 383, to: 706}, + {from: 384, to: 386}, + {from: 384, to: 408}, + {from: 384, to: 431}, + {from: 384, to: 460}, + {from: 384, to: 522}, + {from: 384, to: 527}, + {from: 384, to: 532}, + {from: 384, to: 602}, + {from: 384, to: 612}, + {from: 384, to: 625}, + {from: 384, to: 654}, + {from: 384, to: 667}, + {from: 384, to: 677}, + {from: 384, to: 679}, + {from: 384, to: 685}, + {from: 384, to: 703}, + {from: 384, to: 707}, + {from: 385, to: 433}, + {from: 385, to: 442}, + {from: 385, to: 454}, + {from: 385, to: 475}, + {from: 385, to: 480}, + {from: 385, to: 498}, + {from: 385, to: 517}, + {from: 385, to: 518}, + {from: 385, to: 573}, + {from: 385, to: 577}, + {from: 385, to: 611}, + {from: 385, to: 614}, + {from: 385, to: 623}, + {from: 385, to: 648}, + {from: 385, to: 656}, + {from: 385, to: 678}, + {from: 385, to: 687}, + {from: 386, to: 408}, + {from: 386, to: 415}, + {from: 386, to: 460}, + {from: 386, to: 522}, + {from: 386, to: 527}, + {from: 386, to: 532}, + {from: 386, to: 575}, + {from: 386, to: 612}, + {from: 386, to: 616}, + {from: 386, to: 625}, + {from: 386, to: 654}, + {from: 386, to: 667}, + {from: 386, to: 677}, + {from: 386, to: 679}, + {from: 386, to: 681}, + {from: 386, to: 685}, + {from: 386, to: 707}, + {from: 387, to: 396}, + {from: 387, to: 404}, + {from: 387, to: 437}, + {from: 387, to: 503}, + {from: 387, to: 520}, + {from: 387, to: 590}, + {from: 387, to: 628}, + {from: 387, to: 633}, + {from: 387, to: 664}, + {from: 387, to: 670}, + {from: 387, to: 709}, + {from: 388, to: 423}, + {from: 388, to: 448}, + {from: 388, to: 501}, + {from: 388, to: 506}, + {from: 388, to: 550}, + {from: 388, to: 563}, + {from: 388, to: 588}, + {from: 388, to: 617}, + {from: 388, to: 712}, + {from: 388, to: 727}, + {from: 389, to: 467}, + {from: 389, to: 495}, + {from: 389, to: 570}, + {from: 389, to: 584}, + {from: 389, to: 598}, + {from: 389, to: 599}, + {from: 389, to: 666}, + {from: 390, to: 391}, + {from: 390, to: 392}, + {from: 390, to: 393}, + {from: 390, to: 394}, + {from: 390, to: 395}, + {from: 390, to: 399}, + {from: 390, to: 405}, + {from: 390, to: 411}, + {from: 390, to: 412}, + {from: 390, to: 413}, + {from: 390, to: 414}, + {from: 390, to: 559}, + {from: 390, to: 560}, + {from: 390, to: 561}, + {from: 390, to: 658}, + {from: 390, to: 731}, + {from: 391, to: 392}, + {from: 391, to: 393}, + {from: 391, to: 394}, + {from: 391, to: 395}, + {from: 391, to: 399}, + {from: 391, to: 405}, + {from: 391, to: 411}, + {from: 391, to: 412}, + {from: 391, to: 413}, + {from: 391, to: 414}, + {from: 391, to: 559}, + {from: 391, to: 560}, + {from: 391, to: 561}, + {from: 391, to: 658}, + {from: 391, to: 731}, + {from: 392, to: 393}, + {from: 392, to: 394}, + {from: 392, to: 395}, + {from: 392, to: 399}, + {from: 392, to: 405}, + {from: 392, to: 411}, + {from: 392, to: 412}, + {from: 392, to: 413}, + {from: 392, to: 414}, + {from: 392, to: 559}, + {from: 392, to: 560}, + {from: 392, to: 561}, + {from: 392, to: 658}, + {from: 392, to: 731}, + {from: 393, to: 394}, + {from: 393, to: 395}, + {from: 393, to: 399}, + {from: 393, to: 405}, + {from: 393, to: 411}, + {from: 393, to: 412}, + {from: 393, to: 413}, + {from: 393, to: 414}, + {from: 393, to: 559}, + {from: 393, to: 560}, + {from: 393, to: 561}, + {from: 393, to: 658}, + {from: 393, to: 731}, + {from: 394, to: 395}, + {from: 394, to: 399}, + {from: 394, to: 405}, + {from: 394, to: 411}, + {from: 394, to: 412}, + {from: 394, to: 413}, + {from: 394, to: 414}, + {from: 394, to: 559}, + {from: 394, to: 560}, + {from: 394, to: 561}, + {from: 394, to: 658}, + {from: 394, to: 731}, + {from: 395, to: 399}, + {from: 395, to: 405}, + {from: 395, to: 411}, + {from: 395, to: 412}, + {from: 395, to: 413}, + {from: 395, to: 414}, + {from: 395, to: 559}, + {from: 395, to: 560}, + {from: 395, to: 561}, + {from: 395, to: 658}, + {from: 395, to: 731}, + {from: 396, to: 417}, + {from: 396, to: 496}, + {from: 396, to: 507}, + {from: 396, to: 534}, + {from: 396, to: 566}, + {from: 396, to: 606}, + {from: 396, to: 613}, + {from: 396, to: 633}, + {from: 396, to: 659}, + {from: 396, to: 673}, + {from: 396, to: 682}, + {from: 396, to: 714}, + {from: 397, to: 569}, + {from: 397, to: 572}, + {from: 397, to: 591}, + {from: 397, to: 629}, + {from: 397, to: 643}, + {from: 397, to: 644}, + {from: 397, to: 677}, + {from: 397, to: 685}, + {from: 397, to: 719}, + {from: 397, to: 720}, + {from: 398, to: 430}, + {from: 398, to: 440}, + {from: 398, to: 474}, + {from: 398, to: 485}, + {from: 398, to: 553}, + {from: 398, to: 583}, + {from: 398, to: 621}, + {from: 398, to: 632}, + {from: 398, to: 638}, + {from: 398, to: 639}, + {from: 398, to: 654}, + {from: 398, to: 657}, + {from: 398, to: 668}, + {from: 398, to: 671}, + {from: 398, to: 702}, + {from: 398, to: 715}, + {from: 398, to: 726}, + {from: 399, to: 405}, + {from: 399, to: 411}, + {from: 399, to: 412}, + {from: 399, to: 413}, + {from: 399, to: 414}, + {from: 399, to: 488}, + {from: 399, to: 559}, + {from: 399, to: 560}, + {from: 399, to: 561}, + {from: 399, to: 647}, + {from: 399, to: 658}, + {from: 399, to: 731}, + {from: 400, to: 401}, + {from: 400, to: 402}, + {from: 400, to: 410}, + {from: 400, to: 423}, + {from: 400, to: 545}, + {from: 400, to: 556}, + {from: 400, to: 557}, + {from: 400, to: 558}, + {from: 400, to: 656}, + {from: 400, to: 660}, + {from: 400, to: 674}, + {from: 400, to: 694}, + {from: 400, to: 696}, + {from: 401, to: 402}, + {from: 401, to: 410}, + {from: 401, to: 423}, + {from: 401, to: 545}, + {from: 401, to: 556}, + {from: 401, to: 557}, + {from: 401, to: 558}, + {from: 401, to: 656}, + {from: 401, to: 660}, + {from: 401, to: 674}, + {from: 401, to: 694}, + {from: 401, to: 696}, + {from: 402, to: 410}, + {from: 402, to: 423}, + {from: 402, to: 545}, + {from: 402, to: 556}, + {from: 402, to: 557}, + {from: 402, to: 558}, + {from: 402, to: 656}, + {from: 402, to: 660}, + {from: 402, to: 674}, + {from: 402, to: 694}, + {from: 402, to: 696}, + {from: 403, to: 500}, + {from: 403, to: 505}, + {from: 403, to: 537}, + {from: 403, to: 574}, + {from: 403, to: 587}, + {from: 403, to: 597}, + {from: 403, to: 649}, + {from: 403, to: 691}, + {from: 403, to: 702}, + {from: 403, to: 706}, + {from: 404, to: 416}, + {from: 404, to: 437}, + {from: 404, to: 461}, + {from: 404, to: 483}, + {from: 404, to: 503}, + {from: 404, to: 520}, + {from: 404, to: 565}, + {from: 404, to: 590}, + {from: 404, to: 628}, + {from: 404, to: 661}, + {from: 404, to: 664}, + {from: 404, to: 670}, + {from: 404, to: 709}, + {from: 405, to: 411}, + {from: 405, to: 412}, + {from: 405, to: 413}, + {from: 405, to: 414}, + {from: 405, to: 559}, + {from: 405, to: 560}, + {from: 405, to: 561}, + {from: 405, to: 658}, + {from: 405, to: 731}, + {from: 406, to: 476}, + {from: 406, to: 502}, + {from: 406, to: 513}, + {from: 406, to: 530}, + {from: 406, to: 544}, + {from: 406, to: 681}, + {from: 406, to: 683}, + {from: 407, to: 420}, + {from: 407, to: 488}, + {from: 407, to: 533}, + {from: 407, to: 579}, + {from: 407, to: 626}, + {from: 407, to: 627}, + {from: 407, to: 662}, + {from: 407, to: 705}, + {from: 408, to: 460}, + {from: 408, to: 522}, + {from: 408, to: 527}, + {from: 408, to: 532}, + {from: 408, to: 612}, + {from: 408, to: 625}, + {from: 408, to: 654}, + {from: 408, to: 667}, + {from: 408, to: 677}, + {from: 408, to: 679}, + {from: 408, to: 685}, + {from: 408, to: 707}, + {from: 409, to: 421}, + {from: 409, to: 425}, + {from: 409, to: 433}, + {from: 409, to: 440}, + {from: 409, to: 472}, + {from: 409, to: 473}, + {from: 409, to: 498}, + {from: 409, to: 508}, + {from: 409, to: 521}, + {from: 409, to: 523}, + {from: 409, to: 543}, + {from: 409, to: 562}, + {from: 409, to: 565}, + {from: 409, to: 573}, + {from: 409, to: 589}, + {from: 409, to: 594}, + {from: 409, to: 604}, + {from: 409, to: 629}, + {from: 409, to: 663}, + {from: 409, to: 679}, + {from: 409, to: 728}, + {from: 410, to: 423}, + {from: 410, to: 545}, + {from: 410, to: 556}, + {from: 410, to: 557}, + {from: 410, to: 558}, + {from: 410, to: 656}, + {from: 410, to: 660}, + {from: 410, to: 674}, + {from: 410, to: 694}, + {from: 410, to: 696}, + {from: 411, to: 412}, + {from: 411, to: 413}, + {from: 411, to: 414}, + {from: 411, to: 559}, + {from: 411, to: 560}, + {from: 411, to: 561}, + {from: 411, to: 658}, + {from: 411, to: 731}, + {from: 412, to: 413}, + {from: 412, to: 414}, + {from: 412, to: 559}, + {from: 412, to: 560}, + {from: 412, to: 561}, + {from: 412, to: 658}, + {from: 412, to: 731}, + {from: 413, to: 414}, + {from: 413, to: 559}, + {from: 413, to: 560}, + {from: 413, to: 561}, + {from: 413, to: 658}, + {from: 413, to: 731}, + {from: 414, to: 559}, + {from: 414, to: 560}, + {from: 414, to: 561}, + {from: 414, to: 658}, + {from: 414, to: 731}, + {from: 415, to: 434}, + {from: 415, to: 575}, + {from: 415, to: 576}, + {from: 415, to: 583}, + {from: 415, to: 603}, + {from: 415, to: 612}, + {from: 415, to: 616}, + {from: 415, to: 668}, + {from: 415, to: 681}, + {from: 415, to: 713}, + {from: 416, to: 422}, + {from: 416, to: 447}, + {from: 416, to: 449}, + {from: 416, to: 452}, + {from: 416, to: 461}, + {from: 416, to: 478}, + {from: 416, to: 481}, + {from: 416, to: 482}, + {from: 416, to: 483}, + {from: 416, to: 565}, + {from: 416, to: 622}, + {from: 416, to: 661}, + {from: 416, to: 675}, + {from: 417, to: 496}, + {from: 417, to: 507}, + {from: 417, to: 534}, + {from: 417, to: 566}, + {from: 417, to: 606}, + {from: 417, to: 613}, + {from: 417, to: 659}, + {from: 417, to: 673}, + {from: 417, to: 682}, + {from: 417, to: 714}, + {from: 418, to: 435}, + {from: 418, to: 493}, + {from: 418, to: 494}, + {from: 418, to: 519}, + {from: 418, to: 525}, + {from: 418, to: 526}, + {from: 418, to: 582}, + {from: 418, to: 585}, + {from: 418, to: 605}, + {from: 418, to: 631}, + {from: 418, to: 655}, + {from: 418, to: 722}, + {from: 419, to: 424}, + {from: 419, to: 429}, + {from: 419, to: 450}, + {from: 419, to: 451}, + {from: 419, to: 462}, + {from: 419, to: 487}, + {from: 419, to: 489}, + {from: 419, to: 529}, + {from: 419, to: 555}, + {from: 419, to: 569}, + {from: 419, to: 600}, + {from: 419, to: 608}, + {from: 419, to: 642}, + {from: 419, to: 643}, + {from: 419, to: 645}, + {from: 419, to: 720}, + {from: 420, to: 488}, + {from: 420, to: 533}, + {from: 420, to: 579}, + {from: 420, to: 626}, + {from: 420, to: 627}, + {from: 420, to: 662}, + {from: 420, to: 705}, + {from: 421, to: 425}, + {from: 421, to: 440}, + {from: 421, to: 472}, + {from: 421, to: 473}, + {from: 421, to: 508}, + {from: 421, to: 521}, + {from: 421, to: 523}, + {from: 421, to: 543}, + {from: 421, to: 562}, + {from: 421, to: 565}, + {from: 421, to: 589}, + {from: 421, to: 594}, + {from: 421, to: 604}, + {from: 421, to: 649}, + {from: 421, to: 663}, + {from: 421, to: 682}, + {from: 421, to: 728}, + {from: 422, to: 447}, + {from: 422, to: 449}, + {from: 422, to: 452}, + {from: 422, to: 478}, + {from: 422, to: 481}, + {from: 422, to: 482}, + {from: 422, to: 572}, + {from: 422, to: 591}, + {from: 422, to: 622}, + {from: 422, to: 675}, + {from: 422, to: 693}, + {from: 423, to: 545}, + {from: 423, to: 556}, + {from: 423, to: 557}, + {from: 423, to: 558}, + {from: 423, to: 656}, + {from: 423, to: 660}, + {from: 423, to: 674}, + {from: 423, to: 694}, + {from: 423, to: 696}, + {from: 424, to: 450}, + {from: 424, to: 451}, + {from: 424, to: 462}, + {from: 424, to: 487}, + {from: 424, to: 517}, + {from: 424, to: 537}, + {from: 424, to: 555}, + {from: 424, to: 600}, + {from: 424, to: 608}, + {from: 424, to: 636}, + {from: 424, to: 642}, + {from: 424, to: 645}, + {from: 425, to: 440}, + {from: 425, to: 449}, + {from: 425, to: 472}, + {from: 425, to: 473}, + {from: 425, to: 490}, + {from: 425, to: 508}, + {from: 425, to: 521}, + {from: 425, to: 523}, + {from: 425, to: 543}, + {from: 425, to: 562}, + {from: 425, to: 565}, + {from: 425, to: 589}, + {from: 425, to: 594}, + {from: 425, to: 604}, + {from: 425, to: 622}, + {from: 425, to: 663}, + {from: 425, to: 675}, + {from: 425, to: 676}, + {from: 425, to: 728}, + {from: 426, to: 427}, + {from: 426, to: 456}, + {from: 426, to: 464}, + {from: 426, to: 492}, + {from: 426, to: 536}, + {from: 426, to: 549}, + {from: 426, to: 551}, + {from: 426, to: 609}, + {from: 426, to: 615}, + {from: 426, to: 700}, + {from: 426, to: 718}, + {from: 427, to: 456}, + {from: 427, to: 464}, + {from: 427, to: 492}, + {from: 427, to: 536}, + {from: 427, to: 549}, + {from: 427, to: 551}, + {from: 427, to: 609}, + {from: 427, to: 615}, + {from: 427, to: 700}, + {from: 427, to: 718}, + {from: 428, to: 511}, + {from: 428, to: 528}, + {from: 428, to: 532}, + {from: 428, to: 540}, + {from: 428, to: 571}, + {from: 428, to: 580}, + {from: 428, to: 593}, + {from: 428, to: 601}, + {from: 428, to: 618}, + {from: 428, to: 619}, + {from: 428, to: 652}, + {from: 428, to: 703}, + {from: 428, to: 704}, + {from: 428, to: 716}, + {from: 428, to: 732}, + {from: 429, to: 489}, + {from: 429, to: 499}, + {from: 429, to: 529}, + {from: 429, to: 548}, + {from: 429, to: 552}, + {from: 429, to: 569}, + {from: 429, to: 595}, + {from: 429, to: 643}, + {from: 429, to: 710}, + {from: 429, to: 720}, + {from: 430, to: 440}, + {from: 430, to: 461}, + {from: 430, to: 463}, + {from: 430, to: 486}, + {from: 430, to: 531}, + {from: 430, to: 583}, + {from: 430, to: 607}, + {from: 430, to: 634}, + {from: 430, to: 654}, + {from: 430, to: 668}, + {from: 430, to: 702}, + {from: 430, to: 711}, + {from: 431, to: 436}, + {from: 431, to: 443}, + {from: 431, to: 490}, + {from: 431, to: 529}, + {from: 431, to: 547}, + {from: 431, to: 567}, + {from: 431, to: 586}, + {from: 431, to: 602}, + {from: 431, to: 676}, + {from: 431, to: 699}, + {from: 431, to: 703}, + {from: 431, to: 717}, + {from: 432, to: 443}, + {from: 432, to: 444}, + {from: 432, to: 455}, + {from: 432, to: 469}, + {from: 432, to: 514}, + {from: 432, to: 535}, + {from: 432, to: 539}, + {from: 432, to: 542}, + {from: 432, to: 571}, + {from: 432, to: 589}, + {from: 432, to: 623}, + {from: 432, to: 624}, + {from: 432, to: 644}, + {from: 432, to: 653}, + {from: 432, to: 669}, + {from: 432, to: 698}, + {from: 432, to: 719}, + {from: 433, to: 442}, + {from: 433, to: 454}, + {from: 433, to: 475}, + {from: 433, to: 480}, + {from: 433, to: 498}, + {from: 433, to: 517}, + {from: 433, to: 543}, + {from: 433, to: 573}, + {from: 433, to: 577}, + {from: 433, to: 611}, + {from: 433, to: 614}, + {from: 433, to: 623}, + {from: 433, to: 629}, + {from: 433, to: 648}, + {from: 433, to: 678}, + {from: 433, to: 679}, + {from: 433, to: 687}, + {from: 434, to: 491}, + {from: 434, to: 521}, + {from: 434, to: 575}, + {from: 434, to: 576}, + {from: 434, to: 583}, + {from: 434, to: 603}, + {from: 434, to: 616}, + {from: 434, to: 668}, + {from: 434, to: 713}, + {from: 435, to: 493}, + {from: 435, to: 494}, + {from: 435, to: 519}, + {from: 435, to: 525}, + {from: 435, to: 526}, + {from: 435, to: 582}, + {from: 435, to: 585}, + {from: 435, to: 605}, + {from: 435, to: 631}, + {from: 435, to: 655}, + {from: 435, to: 722}, + {from: 436, to: 443}, + {from: 436, to: 490}, + {from: 436, to: 516}, + {from: 436, to: 529}, + {from: 436, to: 547}, + {from: 436, to: 567}, + {from: 436, to: 586}, + {from: 436, to: 676}, + {from: 436, to: 696}, + {from: 436, to: 699}, + {from: 436, to: 717}, + {from: 437, to: 503}, + {from: 437, to: 520}, + {from: 437, to: 590}, + {from: 437, to: 628}, + {from: 437, to: 664}, + {from: 437, to: 670}, + {from: 437, to: 709}, + {from: 438, to: 441}, + {from: 438, to: 465}, + {from: 438, to: 466}, + {from: 438, to: 491}, + {from: 438, to: 646}, + {from: 438, to: 647}, + {from: 438, to: 650}, + {from: 438, to: 651}, + {from: 438, to: 683}, + {from: 438, to: 689}, + {from: 438, to: 724}, + {from: 438, to: 725}, + {from: 438, to: 729}, + {from: 438, to: 730}, + {from: 438, to: 734}, + {from: 438, to: 735}, + {from: 439, to: 540}, + {from: 439, to: 568}, + {from: 439, to: 640}, + {from: 439, to: 641}, + {from: 439, to: 695}, + {from: 439, to: 704}, + {from: 439, to: 708}, + {from: 439, to: 709}, + {from: 439, to: 732}, + {from: 439, to: 733}, + {from: 440, to: 472}, + {from: 440, to: 473}, + {from: 440, to: 508}, + {from: 440, to: 521}, + {from: 440, to: 523}, + {from: 440, to: 543}, + {from: 440, to: 562}, + {from: 440, to: 565}, + {from: 440, to: 583}, + {from: 440, to: 589}, + {from: 440, to: 594}, + {from: 440, to: 604}, + {from: 440, to: 654}, + {from: 440, to: 663}, + {from: 440, to: 668}, + {from: 440, to: 702}, + {from: 440, to: 728}, + {from: 441, to: 465}, + {from: 441, to: 466}, + {from: 441, to: 491}, + {from: 441, to: 646}, + {from: 441, to: 647}, + {from: 441, to: 650}, + {from: 441, to: 651}, + {from: 441, to: 689}, + {from: 441, to: 724}, + {from: 441, to: 725}, + {from: 441, to: 729}, + {from: 441, to: 730}, + {from: 441, to: 734}, + {from: 441, to: 735}, + {from: 442, to: 454}, + {from: 442, to: 455}, + {from: 442, to: 475}, + {from: 442, to: 480}, + {from: 442, to: 498}, + {from: 442, to: 517}, + {from: 442, to: 573}, + {from: 442, to: 577}, + {from: 442, to: 611}, + {from: 442, to: 614}, + {from: 442, to: 623}, + {from: 442, to: 648}, + {from: 442, to: 678}, + {from: 442, to: 687}, + {from: 442, to: 721}, + {from: 443, to: 490}, + {from: 443, to: 529}, + {from: 443, to: 547}, + {from: 443, to: 567}, + {from: 443, to: 571}, + {from: 443, to: 586}, + {from: 443, to: 589}, + {from: 443, to: 623}, + {from: 443, to: 644}, + {from: 443, to: 676}, + {from: 443, to: 699}, + {from: 443, to: 717}, + {from: 443, to: 719}, + {from: 444, to: 455}, + {from: 444, to: 469}, + {from: 444, to: 514}, + {from: 444, to: 535}, + {from: 444, to: 539}, + {from: 444, to: 542}, + {from: 444, to: 624}, + {from: 444, to: 653}, + {from: 444, to: 669}, + {from: 444, to: 698}, + {from: 445, to: 483}, + {from: 445, to: 484}, + {from: 445, to: 512}, + {from: 445, to: 638}, + {from: 445, to: 692}, + {from: 445, to: 723}, + {from: 446, to: 509}, + {from: 446, to: 510}, + {from: 446, to: 546}, + {from: 446, to: 564}, + {from: 446, to: 581}, + {from: 446, to: 592}, + {from: 447, to: 449}, + {from: 447, to: 452}, + {from: 447, to: 478}, + {from: 447, to: 481}, + {from: 447, to: 482}, + {from: 447, to: 622}, + {from: 447, to: 675}, + {from: 447, to: 711}, + {from: 448, to: 501}, + {from: 448, to: 506}, + {from: 448, to: 520}, + {from: 448, to: 550}, + {from: 448, to: 563}, + {from: 448, to: 588}, + {from: 448, to: 617}, + {from: 448, to: 712}, + {from: 448, to: 727}, + {from: 449, to: 452}, + {from: 449, to: 478}, + {from: 449, to: 481}, + {from: 449, to: 482}, + {from: 449, to: 490}, + {from: 449, to: 622}, + {from: 449, to: 675}, + {from: 449, to: 676}, + {from: 449, to: 728}, + {from: 450, to: 451}, + {from: 450, to: 462}, + {from: 450, to: 487}, + {from: 450, to: 555}, + {from: 450, to: 600}, + {from: 450, to: 608}, + {from: 450, to: 619}, + {from: 450, to: 642}, + {from: 450, to: 645}, + {from: 450, to: 716}, + {from: 451, to: 462}, + {from: 451, to: 487}, + {from: 451, to: 555}, + {from: 451, to: 600}, + {from: 451, to: 608}, + {from: 451, to: 642}, + {from: 451, to: 645}, + {from: 452, to: 478}, + {from: 452, to: 481}, + {from: 452, to: 482}, + {from: 452, to: 503}, + {from: 452, to: 534}, + {from: 452, to: 622}, + {from: 452, to: 670}, + {from: 452, to: 675}, + {from: 453, to: 504}, + {from: 453, to: 578}, + {from: 453, to: 596}, + {from: 453, to: 602}, + {from: 453, to: 610}, + {from: 453, to: 661}, + {from: 453, to: 665}, + {from: 453, to: 690}, + {from: 453, to: 692}, + {from: 453, to: 693}, + {from: 453, to: 721}, + {from: 453, to: 723}, + {from: 454, to: 455}, + {from: 454, to: 475}, + {from: 454, to: 480}, + {from: 454, to: 498}, + {from: 454, to: 517}, + {from: 454, to: 573}, + {from: 454, to: 577}, + {from: 454, to: 611}, + {from: 454, to: 614}, + {from: 454, to: 623}, + {from: 454, to: 648}, + {from: 454, to: 678}, + {from: 454, to: 687}, + {from: 454, to: 721}, + {from: 455, to: 469}, + {from: 455, to: 514}, + {from: 455, to: 535}, + {from: 455, to: 539}, + {from: 455, to: 542}, + {from: 455, to: 577}, + {from: 455, to: 624}, + {from: 455, to: 653}, + {from: 455, to: 669}, + {from: 455, to: 678}, + {from: 455, to: 687}, + {from: 455, to: 698}, + {from: 455, to: 721}, + {from: 456, to: 464}, + {from: 456, to: 492}, + {from: 456, to: 536}, + {from: 456, to: 549}, + {from: 456, to: 551}, + {from: 456, to: 609}, + {from: 456, to: 615}, + {from: 456, to: 700}, + {from: 456, to: 718}, + {from: 457, to: 554}, + {from: 457, to: 630}, + {from: 457, to: 672}, + {from: 457, to: 701}, + {from: 458, to: 459}, + {from: 458, to: 468}, + {from: 458, to: 470}, + {from: 458, to: 471}, + {from: 458, to: 477}, + {from: 458, to: 479}, + {from: 458, to: 515}, + {from: 458, to: 518}, + {from: 458, to: 541}, + {from: 458, to: 620}, + {from: 458, to: 680}, + {from: 458, to: 686}, + {from: 459, to: 468}, + {from: 459, to: 470}, + {from: 459, to: 471}, + {from: 459, to: 477}, + {from: 459, to: 479}, + {from: 459, to: 515}, + {from: 459, to: 518}, + {from: 459, to: 541}, + {from: 459, to: 620}, + {from: 459, to: 680}, + {from: 459, to: 686}, + {from: 460, to: 522}, + {from: 460, to: 527}, + {from: 460, to: 528}, + {from: 460, to: 532}, + {from: 460, to: 562}, + {from: 460, to: 576}, + {from: 460, to: 606}, + {from: 460, to: 612}, + {from: 460, to: 625}, + {from: 460, to: 646}, + {from: 460, to: 654}, + {from: 460, to: 667}, + {from: 460, to: 677}, + {from: 460, to: 679}, + {from: 460, to: 685}, + {from: 460, to: 707}, + {from: 460, to: 713}, + {from: 461, to: 463}, + {from: 461, to: 483}, + {from: 461, to: 486}, + {from: 461, to: 531}, + {from: 461, to: 565}, + {from: 461, to: 607}, + {from: 461, to: 634}, + {from: 461, to: 661}, + {from: 461, to: 711}, + {from: 462, to: 487}, + {from: 462, to: 555}, + {from: 462, to: 600}, + {from: 462, to: 608}, + {from: 462, to: 642}, + {from: 462, to: 645}, + {from: 462, to: 707}, + {from: 462, to: 726}, + {from: 463, to: 486}, + {from: 463, to: 531}, + {from: 463, to: 607}, + {from: 463, to: 634}, + {from: 463, to: 711}, + {from: 464, to: 492}, + {from: 464, to: 536}, + {from: 464, to: 549}, + {from: 464, to: 551}, + {from: 464, to: 552}, + {from: 464, to: 609}, + {from: 464, to: 615}, + {from: 464, to: 700}, + {from: 464, to: 718}, + {from: 465, to: 466}, + {from: 465, to: 491}, + {from: 465, to: 646}, + {from: 465, to: 647}, + {from: 465, to: 650}, + {from: 465, to: 651}, + {from: 465, to: 689}, + {from: 465, to: 724}, + {from: 465, to: 725}, + {from: 465, to: 729}, + {from: 465, to: 730}, + {from: 465, to: 734}, + {from: 465, to: 735}, + {from: 466, to: 491}, + {from: 466, to: 646}, + {from: 466, to: 647}, + {from: 466, to: 650}, + {from: 466, to: 651}, + {from: 466, to: 689}, + {from: 466, to: 724}, + {from: 466, to: 725}, + {from: 466, to: 729}, + {from: 466, to: 730}, + {from: 466, to: 734}, + {from: 466, to: 735}, + {from: 467, to: 495}, + {from: 467, to: 570}, + {from: 467, to: 584}, + {from: 467, to: 598}, + {from: 467, to: 599}, + {from: 467, to: 666}, + {from: 468, to: 470}, + {from: 468, to: 471}, + {from: 468, to: 477}, + {from: 468, to: 479}, + {from: 468, to: 515}, + {from: 468, to: 518}, + {from: 468, to: 541}, + {from: 468, to: 620}, + {from: 468, to: 680}, + {from: 468, to: 686}, + {from: 469, to: 514}, + {from: 469, to: 535}, + {from: 469, to: 539}, + {from: 469, to: 542}, + {from: 469, to: 600}, + {from: 469, to: 608}, + {from: 469, to: 624}, + {from: 469, to: 631}, + {from: 469, to: 653}, + {from: 469, to: 669}, + {from: 469, to: 698}, + {from: 469, to: 734}, + {from: 470, to: 471}, + {from: 470, to: 477}, + {from: 470, to: 479}, + {from: 470, to: 515}, + {from: 470, to: 518}, + {from: 470, to: 541}, + {from: 470, to: 620}, + {from: 470, to: 680}, + {from: 470, to: 686}, + {from: 471, to: 477}, + {from: 471, to: 479}, + {from: 471, to: 515}, + {from: 471, to: 518}, + {from: 471, to: 541}, + {from: 471, to: 550}, + {from: 471, to: 620}, + {from: 471, to: 680}, + {from: 471, to: 686}, + {from: 472, to: 473}, + {from: 472, to: 508}, + {from: 472, to: 521}, + {from: 472, to: 523}, + {from: 472, to: 543}, + {from: 472, to: 562}, + {from: 472, to: 565}, + {from: 472, to: 589}, + {from: 472, to: 594}, + {from: 472, to: 604}, + {from: 472, to: 649}, + {from: 472, to: 663}, + {from: 472, to: 682}, + {from: 472, to: 728}, + {from: 473, to: 508}, + {from: 473, to: 521}, + {from: 473, to: 523}, + {from: 473, to: 533}, + {from: 473, to: 543}, + {from: 473, to: 562}, + {from: 473, to: 565}, + {from: 473, to: 589}, + {from: 473, to: 594}, + {from: 473, to: 604}, + {from: 473, to: 663}, + {from: 473, to: 728}, + {from: 474, to: 485}, + {from: 474, to: 553}, + {from: 474, to: 621}, + {from: 474, to: 632}, + {from: 474, to: 638}, + {from: 474, to: 639}, + {from: 474, to: 657}, + {from: 474, to: 671}, + {from: 474, to: 715}, + {from: 474, to: 726}, + {from: 475, to: 480}, + {from: 475, to: 498}, + {from: 475, to: 517}, + {from: 475, to: 518}, + {from: 475, to: 573}, + {from: 475, to: 577}, + {from: 475, to: 611}, + {from: 475, to: 614}, + {from: 475, to: 623}, + {from: 475, to: 648}, + {from: 475, to: 656}, + {from: 475, to: 678}, + {from: 475, to: 687}, + {from: 476, to: 502}, + {from: 476, to: 513}, + {from: 476, to: 530}, + {from: 476, to: 544}, + {from: 476, to: 681}, + {from: 476, to: 683}, + {from: 477, to: 479}, + {from: 477, to: 515}, + {from: 477, to: 518}, + {from: 477, to: 541}, + {from: 477, to: 620}, + {from: 477, to: 680}, + {from: 477, to: 686}, + {from: 478, to: 481}, + {from: 478, to: 482}, + {from: 478, to: 558}, + {from: 478, to: 622}, + {from: 478, to: 675}, + {from: 479, to: 515}, + {from: 479, to: 518}, + {from: 479, to: 541}, + {from: 479, to: 620}, + {from: 479, to: 680}, + {from: 479, to: 686}, + {from: 480, to: 497}, + {from: 480, to: 498}, + {from: 480, to: 517}, + {from: 480, to: 573}, + {from: 480, to: 577}, + {from: 480, to: 611}, + {from: 480, to: 614}, + {from: 480, to: 623}, + {from: 480, to: 648}, + {from: 480, to: 678}, + {from: 480, to: 687}, + {from: 481, to: 482}, + {from: 481, to: 503}, + {from: 481, to: 534}, + {from: 481, to: 622}, + {from: 481, to: 670}, + {from: 481, to: 675}, + {from: 482, to: 622}, + {from: 482, to: 653}, + {from: 482, to: 675}, + {from: 483, to: 484}, + {from: 483, to: 512}, + {from: 483, to: 565}, + {from: 483, to: 661}, + {from: 484, to: 512}, + {from: 484, to: 701}, + {from: 485, to: 553}, + {from: 485, to: 621}, + {from: 485, to: 632}, + {from: 485, to: 638}, + {from: 485, to: 639}, + {from: 485, to: 657}, + {from: 485, to: 663}, + {from: 485, to: 671}, + {from: 485, to: 715}, + {from: 485, to: 726}, + {from: 486, to: 531}, + {from: 486, to: 607}, + {from: 486, to: 618}, + {from: 486, to: 634}, + {from: 486, to: 711}, + {from: 487, to: 555}, + {from: 487, to: 600}, + {from: 487, to: 608}, + {from: 487, to: 642}, + {from: 487, to: 645}, + {from: 488, to: 533}, + {from: 488, to: 561}, + {from: 488, to: 579}, + {from: 488, to: 626}, + {from: 488, to: 627}, + {from: 488, to: 647}, + {from: 488, to: 662}, + {from: 488, to: 705}, + {from: 489, to: 499}, + {from: 489, to: 529}, + {from: 489, to: 548}, + {from: 489, to: 552}, + {from: 489, to: 569}, + {from: 489, to: 595}, + {from: 489, to: 643}, + {from: 489, to: 710}, + {from: 489, to: 720}, + {from: 490, to: 529}, + {from: 490, to: 547}, + {from: 490, to: 567}, + {from: 490, to: 586}, + {from: 490, to: 622}, + {from: 490, to: 675}, + {from: 490, to: 676}, + {from: 490, to: 699}, + {from: 490, to: 717}, + {from: 490, to: 728}, + {from: 491, to: 521}, + {from: 491, to: 603}, + {from: 491, to: 646}, + {from: 491, to: 647}, + {from: 491, to: 650}, + {from: 491, to: 651}, + {from: 491, to: 689}, + {from: 491, to: 724}, + {from: 491, to: 725}, + {from: 491, to: 729}, + {from: 491, to: 730}, + {from: 491, to: 734}, + {from: 491, to: 735}, + {from: 492, to: 535}, + {from: 492, to: 536}, + {from: 492, to: 549}, + {from: 492, to: 551}, + {from: 492, to: 609}, + {from: 492, to: 615}, + {from: 492, to: 700}, + {from: 492, to: 718}, + {from: 493, to: 494}, + {from: 493, to: 519}, + {from: 493, to: 525}, + {from: 493, to: 526}, + {from: 493, to: 582}, + {from: 493, to: 585}, + {from: 493, to: 605}, + {from: 493, to: 624}, + {from: 493, to: 631}, + {from: 493, to: 655}, + {from: 493, to: 722}, + {from: 494, to: 519}, + {from: 494, to: 525}, + {from: 494, to: 526}, + {from: 494, to: 582}, + {from: 494, to: 585}, + {from: 494, to: 605}, + {from: 494, to: 631}, + {from: 494, to: 655}, + {from: 494, to: 722}, + {from: 495, to: 570}, + {from: 495, to: 584}, + {from: 495, to: 598}, + {from: 495, to: 599}, + {from: 495, to: 666}, + {from: 496, to: 507}, + {from: 496, to: 534}, + {from: 496, to: 566}, + {from: 496, to: 606}, + {from: 496, to: 613}, + {from: 496, to: 630}, + {from: 496, to: 659}, + {from: 496, to: 673}, + {from: 496, to: 682}, + {from: 496, to: 714}, + {from: 497, to: 516}, + {from: 497, to: 524}, + {from: 497, to: 538}, + {from: 497, to: 633}, + {from: 497, to: 635}, + {from: 497, to: 636}, + {from: 497, to: 637}, + {from: 497, to: 684}, + {from: 497, to: 688}, + {from: 497, to: 697}, + {from: 497, to: 736}, + {from: 498, to: 517}, + {from: 498, to: 543}, + {from: 498, to: 573}, + {from: 498, to: 577}, + {from: 498, to: 611}, + {from: 498, to: 614}, + {from: 498, to: 623}, + {from: 498, to: 629}, + {from: 498, to: 648}, + {from: 498, to: 678}, + {from: 498, to: 679}, + {from: 498, to: 687}, + {from: 499, to: 548}, + {from: 499, to: 552}, + {from: 499, to: 595}, + {from: 499, to: 710}, + {from: 500, to: 505}, + {from: 500, to: 537}, + {from: 500, to: 574}, + {from: 500, to: 587}, + {from: 500, to: 597}, + {from: 500, to: 649}, + {from: 500, to: 691}, + {from: 500, to: 702}, + {from: 500, to: 706}, + {from: 501, to: 506}, + {from: 501, to: 550}, + {from: 501, to: 563}, + {from: 501, to: 588}, + {from: 501, to: 617}, + {from: 501, to: 712}, + {from: 501, to: 727}, + {from: 502, to: 513}, + {from: 502, to: 530}, + {from: 502, to: 544}, + {from: 502, to: 681}, + {from: 502, to: 683}, + {from: 503, to: 520}, + {from: 503, to: 534}, + {from: 503, to: 590}, + {from: 503, to: 628}, + {from: 503, to: 664}, + {from: 503, to: 670}, + {from: 503, to: 709}, + {from: 504, to: 578}, + {from: 504, to: 596}, + {from: 504, to: 602}, + {from: 504, to: 610}, + {from: 504, to: 661}, + {from: 504, to: 665}, + {from: 504, to: 690}, + {from: 504, to: 692}, + {from: 504, to: 693}, + {from: 504, to: 721}, + {from: 504, to: 723}, + {from: 505, to: 537}, + {from: 505, to: 574}, + {from: 505, to: 587}, + {from: 505, to: 597}, + {from: 505, to: 649}, + {from: 505, to: 691}, + {from: 505, to: 702}, + {from: 505, to: 706}, + {from: 506, to: 550}, + {from: 506, to: 563}, + {from: 506, to: 588}, + {from: 506, to: 617}, + {from: 506, to: 712}, + {from: 506, to: 727}, + {from: 507, to: 534}, + {from: 507, to: 566}, + {from: 507, to: 606}, + {from: 507, to: 613}, + {from: 507, to: 659}, + {from: 507, to: 673}, + {from: 507, to: 682}, + {from: 507, to: 714}, + {from: 507, to: 715}, + {from: 508, to: 521}, + {from: 508, to: 523}, + {from: 508, to: 543}, + {from: 508, to: 562}, + {from: 508, to: 565}, + {from: 508, to: 589}, + {from: 508, to: 594}, + {from: 508, to: 604}, + {from: 508, to: 663}, + {from: 508, to: 728}, + {from: 509, to: 510}, + {from: 509, to: 546}, + {from: 509, to: 564}, + {from: 509, to: 581}, + {from: 509, to: 592}, + {from: 510, to: 546}, + {from: 510, to: 564}, + {from: 510, to: 581}, + {from: 510, to: 592}, + {from: 511, to: 528}, + {from: 511, to: 539}, + {from: 511, to: 571}, + {from: 511, to: 580}, + {from: 511, to: 593}, + {from: 511, to: 601}, + {from: 511, to: 618}, + {from: 511, to: 619}, + {from: 511, to: 652}, + {from: 511, to: 703}, + {from: 511, to: 716}, + {from: 513, to: 530}, + {from: 513, to: 544}, + {from: 513, to: 681}, + {from: 513, to: 683}, + {from: 514, to: 535}, + {from: 514, to: 539}, + {from: 514, to: 542}, + {from: 514, to: 624}, + {from: 514, to: 653}, + {from: 514, to: 669}, + {from: 514, to: 698}, + {from: 515, to: 518}, + {from: 515, to: 541}, + {from: 515, to: 620}, + {from: 515, to: 680}, + {from: 515, to: 686}, + {from: 516, to: 524}, + {from: 516, to: 538}, + {from: 516, to: 633}, + {from: 516, to: 635}, + {from: 516, to: 636}, + {from: 516, to: 637}, + {from: 516, to: 684}, + {from: 516, to: 688}, + {from: 516, to: 696}, + {from: 516, to: 697}, + {from: 516, to: 736}, + {from: 517, to: 537}, + {from: 517, to: 573}, + {from: 517, to: 577}, + {from: 517, to: 611}, + {from: 517, to: 614}, + {from: 517, to: 623}, + {from: 517, to: 636}, + {from: 517, to: 648}, + {from: 517, to: 678}, + {from: 517, to: 687}, + {from: 518, to: 541}, + {from: 518, to: 611}, + {from: 518, to: 620}, + {from: 518, to: 656}, + {from: 518, to: 680}, + {from: 518, to: 686}, + {from: 519, to: 525}, + {from: 519, to: 526}, + {from: 519, to: 582}, + {from: 519, to: 585}, + {from: 519, to: 605}, + {from: 519, to: 631}, + {from: 519, to: 655}, + {from: 519, to: 722}, + {from: 520, to: 590}, + {from: 520, to: 628}, + {from: 520, to: 664}, + {from: 520, to: 670}, + {from: 520, to: 709}, + {from: 521, to: 523}, + {from: 521, to: 543}, + {from: 521, to: 562}, + {from: 521, to: 565}, + {from: 521, to: 589}, + {from: 521, to: 594}, + {from: 521, to: 603}, + {from: 521, to: 604}, + {from: 521, to: 663}, + {from: 521, to: 728}, + {from: 522, to: 525}, + {from: 522, to: 527}, + {from: 522, to: 532}, + {from: 522, to: 567}, + {from: 522, to: 612}, + {from: 522, to: 625}, + {from: 522, to: 654}, + {from: 522, to: 667}, + {from: 522, to: 677}, + {from: 522, to: 679}, + {from: 522, to: 685}, + {from: 522, to: 707}, + {from: 523, to: 543}, + {from: 523, to: 562}, + {from: 523, to: 565}, + {from: 523, to: 589}, + {from: 523, to: 594}, + {from: 523, to: 604}, + {from: 523, to: 649}, + {from: 523, to: 663}, + {from: 523, to: 682}, + {from: 523, to: 728}, + {from: 524, to: 538}, + {from: 524, to: 633}, + {from: 524, to: 635}, + {from: 524, to: 636}, + {from: 524, to: 637}, + {from: 524, to: 684}, + {from: 524, to: 688}, + {from: 524, to: 697}, + {from: 524, to: 736}, + {from: 525, to: 526}, + {from: 525, to: 527}, + {from: 525, to: 567}, + {from: 525, to: 582}, + {from: 525, to: 585}, + {from: 525, to: 605}, + {from: 525, to: 631}, + {from: 525, to: 655}, + {from: 525, to: 722}, + {from: 526, to: 582}, + {from: 526, to: 585}, + {from: 526, to: 605}, + {from: 526, to: 631}, + {from: 526, to: 652}, + {from: 526, to: 655}, + {from: 526, to: 667}, + {from: 526, to: 722}, + {from: 527, to: 532}, + {from: 527, to: 567}, + {from: 527, to: 612}, + {from: 527, to: 625}, + {from: 527, to: 654}, + {from: 527, to: 667}, + {from: 527, to: 677}, + {from: 527, to: 679}, + {from: 527, to: 685}, + {from: 527, to: 707}, + {from: 528, to: 562}, + {from: 528, to: 571}, + {from: 528, to: 576}, + {from: 528, to: 580}, + {from: 528, to: 593}, + {from: 528, to: 601}, + {from: 528, to: 606}, + {from: 528, to: 618}, + {from: 528, to: 619}, + {from: 528, to: 646}, + {from: 528, to: 652}, + {from: 528, to: 703}, + {from: 528, to: 713}, + {from: 528, to: 716}, + {from: 529, to: 547}, + {from: 529, to: 567}, + {from: 529, to: 569}, + {from: 529, to: 586}, + {from: 529, to: 643}, + {from: 529, to: 676}, + {from: 529, to: 699}, + {from: 529, to: 717}, + {from: 529, to: 720}, + {from: 530, to: 544}, + {from: 530, to: 681}, + {from: 530, to: 683}, + {from: 531, to: 607}, + {from: 531, to: 634}, + {from: 531, to: 711}, + {from: 532, to: 540}, + {from: 532, to: 612}, + {from: 532, to: 625}, + {from: 532, to: 654}, + {from: 532, to: 667}, + {from: 532, to: 677}, + {from: 532, to: 679}, + {from: 532, to: 685}, + {from: 532, to: 704}, + {from: 532, to: 707}, + {from: 532, to: 732}, + {from: 533, to: 579}, + {from: 533, to: 626}, + {from: 533, to: 627}, + {from: 533, to: 662}, + {from: 533, to: 705}, + {from: 534, to: 566}, + {from: 534, to: 606}, + {from: 534, to: 613}, + {from: 534, to: 659}, + {from: 534, to: 670}, + {from: 534, to: 673}, + {from: 534, to: 682}, + {from: 534, to: 714}, + {from: 535, to: 539}, + {from: 535, to: 542}, + {from: 535, to: 624}, + {from: 535, to: 653}, + {from: 535, to: 669}, + {from: 535, to: 698}, + {from: 536, to: 549}, + {from: 536, to: 551}, + {from: 536, to: 609}, + {from: 536, to: 615}, + {from: 536, to: 700}, + {from: 536, to: 718}, + {from: 537, to: 574}, + {from: 537, to: 587}, + {from: 537, to: 597}, + {from: 537, to: 636}, + {from: 537, to: 649}, + {from: 537, to: 691}, + {from: 537, to: 702}, + {from: 537, to: 706}, + {from: 538, to: 633}, + {from: 538, to: 635}, + {from: 538, to: 636}, + {from: 538, to: 637}, + {from: 538, to: 684}, + {from: 538, to: 688}, + {from: 538, to: 697}, + {from: 538, to: 736}, + {from: 539, to: 542}, + {from: 539, to: 624}, + {from: 539, to: 653}, + {from: 539, to: 669}, + {from: 539, to: 698}, + {from: 540, to: 568}, + {from: 540, to: 640}, + {from: 540, to: 641}, + {from: 540, to: 695}, + {from: 540, to: 704}, + {from: 540, to: 708}, + {from: 540, to: 732}, + {from: 540, to: 733}, + {from: 541, to: 620}, + {from: 541, to: 680}, + {from: 541, to: 686}, + {from: 542, to: 624}, + {from: 542, to: 653}, + {from: 542, to: 669}, + {from: 542, to: 698}, + {from: 543, to: 562}, + {from: 543, to: 565}, + {from: 543, to: 573}, + {from: 543, to: 589}, + {from: 543, to: 594}, + {from: 543, to: 604}, + {from: 543, to: 629}, + {from: 543, to: 663}, + {from: 543, to: 679}, + {from: 543, to: 728}, + {from: 544, to: 681}, + {from: 544, to: 683}, + {from: 545, to: 556}, + {from: 545, to: 557}, + {from: 545, to: 558}, + {from: 545, to: 656}, + {from: 545, to: 660}, + {from: 545, to: 674}, + {from: 545, to: 694}, + {from: 545, to: 696}, + {from: 545, to: 722}, + {from: 546, to: 564}, + {from: 546, to: 581}, + {from: 546, to: 592}, + {from: 547, to: 567}, + {from: 547, to: 586}, + {from: 547, to: 627}, + {from: 547, to: 676}, + {from: 547, to: 699}, + {from: 547, to: 717}, + {from: 548, to: 552}, + {from: 548, to: 595}, + {from: 548, to: 710}, + {from: 549, to: 551}, + {from: 549, to: 609}, + {from: 549, to: 615}, + {from: 549, to: 700}, + {from: 549, to: 718}, + {from: 550, to: 563}, + {from: 550, to: 588}, + {from: 550, to: 617}, + {from: 550, to: 712}, + {from: 550, to: 727}, + {from: 551, to: 609}, + {from: 551, to: 615}, + {from: 551, to: 700}, + {from: 551, to: 718}, + {from: 552, to: 595}, + {from: 552, to: 710}, + {from: 553, to: 621}, + {from: 553, to: 632}, + {from: 553, to: 638}, + {from: 553, to: 639}, + {from: 553, to: 657}, + {from: 553, to: 671}, + {from: 553, to: 715}, + {from: 553, to: 726}, + {from: 554, to: 630}, + {from: 554, to: 672}, + {from: 554, to: 701}, + {from: 555, to: 600}, + {from: 555, to: 608}, + {from: 555, to: 642}, + {from: 555, to: 645}, + {from: 555, to: 707}, + {from: 555, to: 726}, + {from: 556, to: 557}, + {from: 556, to: 558}, + {from: 556, to: 656}, + {from: 556, to: 660}, + {from: 556, to: 674}, + {from: 556, to: 694}, + {from: 556, to: 696}, + {from: 557, to: 558}, + {from: 557, to: 656}, + {from: 557, to: 660}, + {from: 557, to: 674}, + {from: 557, to: 694}, + {from: 557, to: 696}, + {from: 558, to: 656}, + {from: 558, to: 660}, + {from: 558, to: 674}, + {from: 558, to: 694}, + {from: 558, to: 696}, + {from: 559, to: 560}, + {from: 559, to: 561}, + {from: 559, to: 658}, + {from: 559, to: 731}, + {from: 560, to: 561}, + {from: 560, to: 658}, + {from: 560, to: 731}, + {from: 561, to: 647}, + {from: 561, to: 658}, + {from: 561, to: 731}, + {from: 562, to: 565}, + {from: 562, to: 576}, + {from: 562, to: 589}, + {from: 562, to: 594}, + {from: 562, to: 604}, + {from: 562, to: 606}, + {from: 562, to: 646}, + {from: 562, to: 663}, + {from: 562, to: 713}, + {from: 562, to: 728}, + {from: 563, to: 588}, + {from: 563, to: 617}, + {from: 563, to: 712}, + {from: 563, to: 727}, + {from: 564, to: 581}, + {from: 564, to: 592}, + {from: 565, to: 589}, + {from: 565, to: 594}, + {from: 565, to: 604}, + {from: 565, to: 661}, + {from: 565, to: 663}, + {from: 565, to: 728}, + {from: 566, to: 606}, + {from: 566, to: 613}, + {from: 566, to: 659}, + {from: 566, to: 673}, + {from: 566, to: 682}, + {from: 566, to: 714}, + {from: 567, to: 586}, + {from: 567, to: 676}, + {from: 567, to: 699}, + {from: 567, to: 717}, + {from: 568, to: 640}, + {from: 568, to: 641}, + {from: 568, to: 695}, + {from: 568, to: 704}, + {from: 568, to: 708}, + {from: 568, to: 709}, + {from: 568, to: 732}, + {from: 568, to: 733}, + {from: 569, to: 572}, + {from: 569, to: 591}, + {from: 569, to: 629}, + {from: 569, to: 643}, + {from: 569, to: 644}, + {from: 569, to: 719}, + {from: 569, to: 720}, + {from: 570, to: 584}, + {from: 570, to: 598}, + {from: 570, to: 599}, + {from: 570, to: 666}, + {from: 571, to: 580}, + {from: 571, to: 589}, + {from: 571, to: 593}, + {from: 571, to: 601}, + {from: 571, to: 618}, + {from: 571, to: 619}, + {from: 571, to: 623}, + {from: 571, to: 644}, + {from: 571, to: 652}, + {from: 571, to: 703}, + {from: 571, to: 716}, + {from: 571, to: 719}, + {from: 572, to: 591}, + {from: 572, to: 629}, + {from: 572, to: 643}, + {from: 572, to: 644}, + {from: 572, to: 693}, + {from: 572, to: 719}, + {from: 572, to: 720}, + {from: 573, to: 577}, + {from: 573, to: 611}, + {from: 573, to: 614}, + {from: 573, to: 623}, + {from: 573, to: 629}, + {from: 573, to: 648}, + {from: 573, to: 678}, + {from: 573, to: 679}, + {from: 573, to: 687}, + {from: 574, to: 587}, + {from: 574, to: 597}, + {from: 574, to: 649}, + {from: 574, to: 691}, + {from: 574, to: 702}, + {from: 574, to: 706}, + {from: 574, to: 718}, + {from: 575, to: 576}, + {from: 575, to: 583}, + {from: 575, to: 603}, + {from: 575, to: 612}, + {from: 575, to: 616}, + {from: 575, to: 668}, + {from: 575, to: 681}, + {from: 575, to: 713}, + {from: 576, to: 583}, + {from: 576, to: 603}, + {from: 576, to: 606}, + {from: 576, to: 616}, + {from: 576, to: 646}, + {from: 576, to: 668}, + {from: 576, to: 713}, + {from: 577, to: 611}, + {from: 577, to: 614}, + {from: 577, to: 623}, + {from: 577, to: 648}, + {from: 577, to: 678}, + {from: 577, to: 687}, + {from: 577, to: 721}, + {from: 578, to: 596}, + {from: 578, to: 601}, + {from: 578, to: 602}, + {from: 578, to: 610}, + {from: 578, to: 655}, + {from: 578, to: 661}, + {from: 578, to: 665}, + {from: 578, to: 690}, + {from: 578, to: 692}, + {from: 578, to: 693}, + {from: 578, to: 721}, + {from: 578, to: 723}, + {from: 579, to: 593}, + {from: 579, to: 626}, + {from: 579, to: 627}, + {from: 579, to: 662}, + {from: 579, to: 705}, + {from: 580, to: 593}, + {from: 580, to: 601}, + {from: 580, to: 618}, + {from: 580, to: 619}, + {from: 580, to: 652}, + {from: 580, to: 703}, + {from: 580, to: 716}, + {from: 581, to: 592}, + {from: 582, to: 585}, + {from: 582, to: 605}, + {from: 582, to: 631}, + {from: 582, to: 655}, + {from: 582, to: 722}, + {from: 583, to: 603}, + {from: 583, to: 616}, + {from: 583, to: 654}, + {from: 583, to: 668}, + {from: 583, to: 702}, + {from: 583, to: 713}, + {from: 584, to: 598}, + {from: 584, to: 599}, + {from: 584, to: 666}, + {from: 585, to: 605}, + {from: 585, to: 631}, + {from: 585, to: 655}, + {from: 585, to: 722}, + {from: 586, to: 627}, + {from: 586, to: 676}, + {from: 586, to: 699}, + {from: 586, to: 717}, + {from: 587, to: 597}, + {from: 587, to: 649}, + {from: 587, to: 691}, + {from: 587, to: 702}, + {from: 587, to: 706}, + {from: 588, to: 617}, + {from: 588, to: 712}, + {from: 588, to: 727}, + {from: 589, to: 594}, + {from: 589, to: 604}, + {from: 589, to: 623}, + {from: 589, to: 644}, + {from: 589, to: 663}, + {from: 589, to: 719}, + {from: 589, to: 728}, + {from: 590, to: 628}, + {from: 590, to: 664}, + {from: 590, to: 670}, + {from: 590, to: 709}, + {from: 591, to: 629}, + {from: 591, to: 643}, + {from: 591, to: 644}, + {from: 591, to: 693}, + {from: 591, to: 719}, + {from: 591, to: 720}, + {from: 593, to: 601}, + {from: 593, to: 618}, + {from: 593, to: 619}, + {from: 593, to: 652}, + {from: 593, to: 703}, + {from: 593, to: 716}, + {from: 594, to: 604}, + {from: 594, to: 663}, + {from: 594, to: 728}, + {from: 595, to: 710}, + {from: 596, to: 602}, + {from: 596, to: 610}, + {from: 596, to: 661}, + {from: 596, to: 665}, + {from: 596, to: 690}, + {from: 596, to: 692}, + {from: 596, to: 693}, + {from: 596, to: 721}, + {from: 596, to: 723}, + {from: 597, to: 649}, + {from: 597, to: 691}, + {from: 597, to: 702}, + {from: 597, to: 706}, + {from: 598, to: 599}, + {from: 598, to: 666}, + {from: 599, to: 666}, + {from: 600, to: 608}, + {from: 600, to: 631}, + {from: 600, to: 642}, + {from: 600, to: 645}, + {from: 600, to: 734}, + {from: 601, to: 618}, + {from: 601, to: 619}, + {from: 601, to: 652}, + {from: 601, to: 655}, + {from: 601, to: 703}, + {from: 601, to: 716}, + {from: 602, to: 610}, + {from: 602, to: 661}, + {from: 602, to: 665}, + {from: 602, to: 690}, + {from: 602, to: 692}, + {from: 602, to: 693}, + {from: 602, to: 703}, + {from: 602, to: 721}, + {from: 602, to: 723}, + {from: 603, to: 616}, + {from: 603, to: 668}, + {from: 603, to: 713}, + {from: 604, to: 621}, + {from: 604, to: 663}, + {from: 604, to: 706}, + {from: 604, to: 728}, + {from: 605, to: 631}, + {from: 605, to: 655}, + {from: 605, to: 722}, + {from: 606, to: 613}, + {from: 606, to: 646}, + {from: 606, to: 659}, + {from: 606, to: 673}, + {from: 606, to: 682}, + {from: 606, to: 713}, + {from: 606, to: 714}, + {from: 607, to: 634}, + {from: 607, to: 711}, + {from: 608, to: 631}, + {from: 608, to: 642}, + {from: 608, to: 645}, + {from: 608, to: 734}, + {from: 609, to: 615}, + {from: 609, to: 700}, + {from: 609, to: 718}, + {from: 610, to: 661}, + {from: 610, to: 665}, + {from: 610, to: 690}, + {from: 610, to: 692}, + {from: 610, to: 693}, + {from: 610, to: 721}, + {from: 610, to: 723}, + {from: 611, to: 614}, + {from: 611, to: 623}, + {from: 611, to: 648}, + {from: 611, to: 656}, + {from: 611, to: 678}, + {from: 611, to: 687}, + {from: 612, to: 616}, + {from: 612, to: 625}, + {from: 612, to: 654}, + {from: 612, to: 667}, + {from: 612, to: 677}, + {from: 612, to: 679}, + {from: 612, to: 681}, + {from: 612, to: 685}, + {from: 612, to: 707}, + {from: 613, to: 659}, + {from: 613, to: 673}, + {from: 613, to: 682}, + {from: 613, to: 714}, + {from: 614, to: 623}, + {from: 614, to: 648}, + {from: 614, to: 678}, + {from: 614, to: 687}, + {from: 615, to: 700}, + {from: 615, to: 718}, + {from: 616, to: 668}, + {from: 616, to: 681}, + {from: 616, to: 713}, + {from: 617, to: 680}, + {from: 617, to: 712}, + {from: 617, to: 727}, + {from: 618, to: 619}, + {from: 618, to: 652}, + {from: 618, to: 703}, + {from: 618, to: 716}, + {from: 619, to: 652}, + {from: 619, to: 703}, + {from: 619, to: 716}, + {from: 620, to: 680}, + {from: 620, to: 686}, + {from: 621, to: 632}, + {from: 621, to: 638}, + {from: 621, to: 639}, + {from: 621, to: 657}, + {from: 621, to: 671}, + {from: 621, to: 706}, + {from: 621, to: 715}, + {from: 621, to: 726}, + {from: 622, to: 675}, + {from: 622, to: 676}, + {from: 622, to: 728}, + {from: 623, to: 644}, + {from: 623, to: 648}, + {from: 623, to: 678}, + {from: 623, to: 687}, + {from: 623, to: 719}, + {from: 624, to: 653}, + {from: 624, to: 669}, + {from: 624, to: 698}, + {from: 625, to: 654}, + {from: 625, to: 667}, + {from: 625, to: 677}, + {from: 625, to: 679}, + {from: 625, to: 685}, + {from: 625, to: 707}, + {from: 626, to: 627}, + {from: 626, to: 662}, + {from: 626, to: 705}, + {from: 627, to: 662}, + {from: 627, to: 705}, + {from: 627, to: 717}, + {from: 628, to: 664}, + {from: 628, to: 670}, + {from: 628, to: 709}, + {from: 629, to: 643}, + {from: 629, to: 644}, + {from: 629, to: 679}, + {from: 629, to: 719}, + {from: 629, to: 720}, + {from: 630, to: 672}, + {from: 630, to: 701}, + {from: 631, to: 655}, + {from: 631, to: 722}, + {from: 631, to: 734}, + {from: 632, to: 638}, + {from: 632, to: 639}, + {from: 632, to: 657}, + {from: 632, to: 671}, + {from: 632, to: 715}, + {from: 632, to: 726}, + {from: 633, to: 635}, + {from: 633, to: 636}, + {from: 633, to: 637}, + {from: 633, to: 684}, + {from: 633, to: 688}, + {from: 633, to: 697}, + {from: 633, to: 736}, + {from: 634, to: 711}, + {from: 635, to: 636}, + {from: 635, to: 637}, + {from: 635, to: 684}, + {from: 635, to: 688}, + {from: 635, to: 697}, + {from: 635, to: 736}, + {from: 636, to: 637}, + {from: 636, to: 684}, + {from: 636, to: 688}, + {from: 636, to: 697}, + {from: 636, to: 736}, + {from: 637, to: 684}, + {from: 637, to: 688}, + {from: 637, to: 697}, + {from: 637, to: 736}, + {from: 638, to: 639}, + {from: 638, to: 657}, + {from: 638, to: 671}, + {from: 638, to: 692}, + {from: 638, to: 715}, + {from: 638, to: 723}, + {from: 638, to: 726}, + {from: 639, to: 657}, + {from: 639, to: 671}, + {from: 639, to: 715}, + {from: 639, to: 726}, + {from: 640, to: 641}, + {from: 640, to: 695}, + {from: 640, to: 704}, + {from: 640, to: 708}, + {from: 640, to: 732}, + {from: 640, to: 733}, + {from: 641, to: 695}, + {from: 641, to: 704}, + {from: 641, to: 708}, + {from: 641, to: 709}, + {from: 641, to: 732}, + {from: 641, to: 733}, + {from: 642, to: 645}, + {from: 642, to: 707}, + {from: 642, to: 726}, + {from: 643, to: 644}, + {from: 643, to: 719}, + {from: 643, to: 720}, + {from: 644, to: 719}, + {from: 644, to: 720}, + {from: 646, to: 647}, + {from: 646, to: 650}, + {from: 646, to: 651}, + {from: 646, to: 689}, + {from: 646, to: 713}, + {from: 646, to: 724}, + {from: 646, to: 725}, + {from: 646, to: 729}, + {from: 646, to: 730}, + {from: 646, to: 734}, + {from: 646, to: 735}, + {from: 647, to: 650}, + {from: 647, to: 651}, + {from: 647, to: 689}, + {from: 647, to: 724}, + {from: 647, to: 725}, + {from: 647, to: 729}, + {from: 647, to: 730}, + {from: 647, to: 734}, + {from: 647, to: 735}, + {from: 648, to: 678}, + {from: 648, to: 687}, + {from: 649, to: 682}, + {from: 649, to: 691}, + {from: 649, to: 702}, + {from: 649, to: 706}, + {from: 650, to: 651}, + {from: 650, to: 689}, + {from: 650, to: 724}, + {from: 650, to: 725}, + {from: 650, to: 729}, + {from: 650, to: 730}, + {from: 650, to: 734}, + {from: 650, to: 735}, + {from: 651, to: 689}, + {from: 651, to: 724}, + {from: 651, to: 725}, + {from: 651, to: 729}, + {from: 651, to: 730}, + {from: 651, to: 734}, + {from: 651, to: 735}, + {from: 652, to: 667}, + {from: 652, to: 703}, + {from: 652, to: 716}, + {from: 653, to: 669}, + {from: 653, to: 698}, + {from: 654, to: 667}, + {from: 654, to: 668}, + {from: 654, to: 677}, + {from: 654, to: 679}, + {from: 654, to: 685}, + {from: 654, to: 702}, + {from: 654, to: 707}, + {from: 655, to: 722}, + {from: 656, to: 660}, + {from: 656, to: 674}, + {from: 656, to: 694}, + {from: 656, to: 696}, + {from: 657, to: 671}, + {from: 657, to: 715}, + {from: 657, to: 726}, + {from: 658, to: 731}, + {from: 659, to: 673}, + {from: 659, to: 682}, + {from: 659, to: 714}, + {from: 660, to: 674}, + {from: 660, to: 694}, + {from: 660, to: 696}, + {from: 661, to: 665}, + {from: 661, to: 690}, + {from: 661, to: 692}, + {from: 661, to: 693}, + {from: 661, to: 721}, + {from: 661, to: 723}, + {from: 662, to: 705}, + {from: 663, to: 728}, + {from: 664, to: 670}, + {from: 664, to: 709}, + {from: 665, to: 690}, + {from: 665, to: 692}, + {from: 665, to: 693}, + {from: 665, to: 721}, + {from: 665, to: 723}, + {from: 667, to: 677}, + {from: 667, to: 679}, + {from: 667, to: 685}, + {from: 667, to: 707}, + {from: 668, to: 702}, + {from: 668, to: 713}, + {from: 669, to: 698}, + {from: 670, to: 709}, + {from: 671, to: 715}, + {from: 671, to: 726}, + {from: 672, to: 701}, + {from: 673, to: 682}, + {from: 673, to: 714}, + {from: 674, to: 694}, + {from: 674, to: 696}, + {from: 675, to: 676}, + {from: 675, to: 728}, + {from: 676, to: 699}, + {from: 676, to: 717}, + {from: 676, to: 728}, + {from: 677, to: 679}, + {from: 677, to: 685}, + {from: 677, to: 707}, + {from: 678, to: 687}, + {from: 678, to: 721}, + {from: 679, to: 685}, + {from: 679, to: 707}, + {from: 680, to: 686}, + {from: 681, to: 683}, + {from: 682, to: 714}, + {from: 684, to: 688}, + {from: 684, to: 697}, + {from: 684, to: 736}, + {from: 685, to: 707}, + {from: 687, to: 721}, + {from: 688, to: 697}, + {from: 688, to: 736}, + {from: 689, to: 724}, + {from: 689, to: 725}, + {from: 689, to: 729}, + {from: 689, to: 730}, + {from: 689, to: 734}, + {from: 689, to: 735}, + {from: 690, to: 692}, + {from: 690, to: 693}, + {from: 690, to: 721}, + {from: 690, to: 723}, + {from: 691, to: 702}, + {from: 691, to: 706}, + {from: 692, to: 693}, + {from: 692, to: 721}, + {from: 692, to: 723}, + {from: 693, to: 721}, + {from: 693, to: 723}, + {from: 694, to: 696}, + {from: 695, to: 704}, + {from: 695, to: 708}, + {from: 695, to: 732}, + {from: 695, to: 733}, + {from: 697, to: 736}, + {from: 699, to: 717}, + {from: 700, to: 718}, + {from: 702, to: 706}, + {from: 703, to: 716}, + {from: 704, to: 708}, + {from: 704, to: 732}, + {from: 704, to: 733}, + {from: 707, to: 726}, + {from: 708, to: 732}, + {from: 708, to: 733}, + {from: 712, to: 727}, + {from: 715, to: 726}, + {from: 719, to: 720}, + {from: 721, to: 723}, + {from: 724, to: 725}, + {from: 724, to: 729}, + {from: 724, to: 730}, + {from: 724, to: 734}, + {from: 724, to: 735}, + {from: 725, to: 729}, + {from: 725, to: 730}, + {from: 725, to: 734}, + {from: 725, to: 735}, + {from: 729, to: 730}, + {from: 729, to: 734}, + {from: 729, to: 735}, + {from: 730, to: 734}, + {from: 730, to: 735}, + {from: 732, to: 733}, + {from: 734, to: 735} - // create an array with edges - var edges = [ -{from: 1, to: 15}, -{from: 1, to: 97}, -{from: 1, to: 108}, -{from: 1, to: 173}, -{from: 1, to: 195}, -{from: 1, to: 205}, -{from: 1, to: 218}, -{from: 1, to: 274}, -{from: 1, to: 296}, -{from: 1, to: 418}, -{from: 1, to: 435}, -{from: 1, to: 493}, -{from: 1, to: 494}, -{from: 1, to: 519}, -{from: 1, to: 525}, -{from: 1, to: 526}, -{from: 1, to: 582}, -{from: 1, to: 585}, -{from: 1, to: 605}, -{from: 1, to: 631}, -{from: 1, to: 655}, -{from: 1, to: 722}, -{from: 2, to: 10}, -{from: 2, to: 31}, -{from: 2, to: 96}, -{from: 2, to: 99}, -{from: 2, to: 100}, -{from: 2, to: 105}, -{from: 2, to: 106}, -{from: 2, to: 130}, -{from: 2, to: 153}, -{from: 2, to: 181}, -{from: 2, to: 219}, -{from: 2, to: 234}, -{from: 2, to: 304}, -{from: 2, to: 309}, -{from: 2, to: 322}, -{from: 2, to: 366}, -{from: 2, to: 369}, -{from: 2, to: 370}, -{from: 2, to: 457}, -{from: 2, to: 554}, -{from: 2, to: 630}, -{from: 2, to: 639}, -{from: 2, to: 672}, -{from: 2, to: 701}, -{from: 3, to: 38}, -{from: 3, to: 39}, -{from: 3, to: 121}, -{from: 3, to: 129}, -{from: 3, to: 165}, -{from: 3, to: 166}, -{from: 3, to: 167}, -{from: 3, to: 168}, -{from: 3, to: 185}, -{from: 3, to: 191}, -{from: 3, to: 226}, -{from: 3, to: 240}, -{from: 3, to: 352}, -{from: 3, to: 359}, -{from: 3, to: 430}, -{from: 3, to: 461}, -{from: 3, to: 463}, -{from: 3, to: 486}, -{from: 3, to: 531}, -{from: 3, to: 607}, -{from: 3, to: 634}, -{from: 3, to: 711}, -{from: 4, to: 11}, -{from: 4, to: 18}, -{from: 4, to: 43}, -{from: 4, to: 65}, -{from: 4, to: 118}, -{from: 4, to: 138}, -{from: 4, to: 199}, -{from: 4, to: 220}, -{from: 4, to: 272}, -{from: 4, to: 340}, -{from: 4, to: 346}, -{from: 4, to: 347}, -{from: 4, to: 387}, -{from: 4, to: 404}, -{from: 4, to: 437}, -{from: 4, to: 503}, -{from: 4, to: 520}, -{from: 4, to: 590}, -{from: 4, to: 628}, -{from: 4, to: 664}, -{from: 4, to: 670}, -{from: 4, to: 709}, -{from: 5, to: 27}, -{from: 5, to: 80}, -{from: 5, to: 116}, -{from: 5, to: 139}, -{from: 5, to: 144}, -{from: 5, to: 157}, -{from: 5, to: 231}, -{from: 5, to: 232}, -{from: 5, to: 238}, -{from: 5, to: 240}, -{from: 5, to: 258}, -{from: 5, to: 303}, -{from: 5, to: 308}, -{from: 5, to: 335}, -{from: 5, to: 348}, -{from: 5, to: 415}, -{from: 5, to: 434}, -{from: 5, to: 491}, -{from: 5, to: 521}, -{from: 5, to: 575}, -{from: 5, to: 576}, -{from: 5, to: 583}, -{from: 5, to: 603}, -{from: 5, to: 616}, -{from: 5, to: 668}, -{from: 5, to: 713}, -{from: 6, to: 29}, -{from: 6, to: 77}, -{from: 6, to: 81}, -{from: 6, to: 148}, -{from: 6, to: 208}, -{from: 6, to: 298}, -{from: 6, to: 307}, -{from: 6, to: 310}, -{from: 6, to: 313}, -{from: 6, to: 458}, -{from: 6, to: 459}, -{from: 6, to: 468}, -{from: 6, to: 470}, -{from: 6, to: 471}, -{from: 6, to: 477}, -{from: 6, to: 479}, -{from: 6, to: 515}, -{from: 6, to: 518}, -{from: 6, to: 541}, -{from: 6, to: 620}, -{from: 6, to: 680}, -{from: 6, to: 686}, -{from: 7, to: 88}, -{from: 7, to: 162}, -{from: 7, to: 215}, -{from: 7, to: 241}, -{from: 7, to: 260}, -{from: 7, to: 266}, -{from: 7, to: 271}, -{from: 7, to: 339}, -{from: 7, to: 364}, -{from: 7, to: 453}, -{from: 7, to: 480}, -{from: 7, to: 497}, -{from: 7, to: 504}, -{from: 7, to: 578}, -{from: 7, to: 596}, -{from: 7, to: 602}, -{from: 7, to: 610}, -{from: 7, to: 661}, -{from: 7, to: 665}, -{from: 7, to: 690}, -{from: 7, to: 692}, -{from: 7, to: 693}, -{from: 7, to: 721}, -{from: 7, to: 723}, -{from: 8, to: 56}, -{from: 8, to: 60}, -{from: 8, to: 74}, -{from: 8, to: 116}, -{from: 8, to: 140}, -{from: 8, to: 144}, -{from: 8, to: 150}, -{from: 8, to: 172}, -{from: 8, to: 177}, -{from: 8, to: 179}, -{from: 8, to: 311}, -{from: 8, to: 318}, -{from: 8, to: 371}, -{from: 8, to: 384}, -{from: 8, to: 386}, -{from: 8, to: 408}, -{from: 8, to: 460}, -{from: 8, to: 522}, -{from: 8, to: 527}, -{from: 8, to: 528}, -{from: 8, to: 532}, -{from: 8, to: 562}, -{from: 8, to: 576}, -{from: 8, to: 606}, -{from: 8, to: 612}, -{from: 8, to: 625}, -{from: 8, to: 646}, -{from: 8, to: 654}, -{from: 8, to: 667}, -{from: 8, to: 677}, -{from: 8, to: 679}, -{from: 8, to: 685}, -{from: 8, to: 707}, -{from: 8, to: 713}, -{from: 9, to: 30}, -{from: 9, to: 60}, -{from: 9, to: 102}, -{from: 9, to: 120}, -{from: 9, to: 186}, -{from: 9, to: 201}, -{from: 9, to: 222}, -{from: 9, to: 228}, -{from: 9, to: 235}, -{from: 9, to: 236}, -{from: 9, to: 305}, -{from: 9, to: 324}, -{from: 9, to: 334}, -{from: 9, to: 353}, -{from: 9, to: 368}, -{from: 9, to: 429}, -{from: 9, to: 489}, -{from: 9, to: 499}, -{from: 9, to: 548}, -{from: 9, to: 552}, -{from: 9, to: 595}, -{from: 9, to: 710}, -{from: 10, to: 31}, -{from: 10, to: 96}, -{from: 10, to: 99}, -{from: 10, to: 100}, -{from: 10, to: 105}, -{from: 10, to: 106}, -{from: 10, to: 130}, -{from: 10, to: 153}, -{from: 10, to: 181}, -{from: 10, to: 219}, -{from: 10, to: 234}, -{from: 10, to: 304}, -{from: 10, to: 309}, -{from: 10, to: 341}, -{from: 10, to: 366}, -{from: 10, to: 369}, -{from: 10, to: 370}, -{from: 10, to: 457}, -{from: 10, to: 554}, -{from: 10, to: 630}, -{from: 10, to: 672}, -{from: 10, to: 701}, -{from: 11, to: 18}, -{from: 11, to: 43}, -{from: 11, to: 59}, -{from: 11, to: 65}, -{from: 11, to: 118}, -{from: 11, to: 138}, -{from: 11, to: 199}, -{from: 11, to: 220}, -{from: 11, to: 237}, -{from: 11, to: 272}, -{from: 11, to: 340}, -{from: 11, to: 346}, -{from: 11, to: 347}, -{from: 11, to: 387}, -{from: 11, to: 404}, -{from: 11, to: 437}, -{from: 11, to: 447}, -{from: 11, to: 503}, -{from: 11, to: 520}, -{from: 11, to: 590}, -{from: 11, to: 628}, -{from: 11, to: 664}, -{from: 11, to: 670}, -{from: 11, to: 709}, -{from: 11, to: 711}, -{from: 12, to: 54}, -{from: 12, to: 70}, -{from: 12, to: 202}, -{from: 12, to: 211}, -{from: 12, to: 212}, -{from: 12, to: 221}, -{from: 12, to: 225}, -{from: 12, to: 261}, -{from: 12, to: 287}, -{from: 12, to: 319}, -{from: 12, to: 358}, -{from: 12, to: 419}, -{from: 12, to: 424}, -{from: 12, to: 450}, -{from: 12, to: 451}, -{from: 12, to: 462}, -{from: 12, to: 487}, -{from: 12, to: 555}, -{from: 12, to: 600}, -{from: 12, to: 608}, -{from: 12, to: 642}, -{from: 12, to: 645}, -{from: 13, to: 35}, -{from: 13, to: 36}, -{from: 13, to: 40}, -{from: 13, to: 41}, -{from: 13, to: 66}, -{from: 13, to: 78}, -{from: 13, to: 137}, -{from: 13, to: 192}, -{from: 13, to: 247}, -{from: 13, to: 273}, -{from: 13, to: 284}, -{from: 13, to: 306}, -{from: 13, to: 315}, -{from: 13, to: 380}, -{from: 13, to: 389}, -{from: 13, to: 467}, -{from: 13, to: 495}, -{from: 13, to: 570}, -{from: 13, to: 584}, -{from: 13, to: 598}, -{from: 13, to: 599}, -{from: 13, to: 666}, -{from: 14, to: 16}, -{from: 14, to: 72}, -{from: 14, to: 75}, -{from: 14, to: 115}, -{from: 14, to: 190}, -{from: 14, to: 194}, -{from: 14, to: 200}, -{from: 14, to: 243}, -{from: 14, to: 259}, -{from: 14, to: 292}, -{from: 14, to: 342}, -{from: 14, to: 363}, -{from: 14, to: 379}, -{from: 14, to: 383}, -{from: 14, to: 403}, -{from: 14, to: 500}, -{from: 14, to: 505}, -{from: 14, to: 537}, -{from: 14, to: 574}, -{from: 14, to: 587}, -{from: 14, to: 597}, -{from: 14, to: 640}, -{from: 14, to: 649}, -{from: 14, to: 691}, -{from: 14, to: 695}, -{from: 14, to: 702}, -{from: 14, to: 706}, -{from: 15, to: 97}, -{from: 15, to: 108}, -{from: 15, to: 173}, -{from: 15, to: 195}, -{from: 15, to: 205}, -{from: 15, to: 218}, -{from: 15, to: 274}, -{from: 15, to: 296}, -{from: 15, to: 418}, -{from: 15, to: 435}, -{from: 15, to: 493}, -{from: 15, to: 494}, -{from: 15, to: 519}, -{from: 15, to: 525}, -{from: 15, to: 526}, -{from: 15, to: 582}, -{from: 15, to: 585}, -{from: 15, to: 605}, -{from: 15, to: 631}, -{from: 15, to: 655}, -{from: 15, to: 722}, -{from: 16, to: 21}, -{from: 16, to: 22}, -{from: 16, to: 23}, -{from: 16, to: 24}, -{from: 16, to: 25}, -{from: 16, to: 48}, -{from: 16, to: 51}, -{from: 16, to: 158}, -{from: 16, to: 174}, -{from: 16, to: 243}, -{from: 16, to: 292}, -{from: 16, to: 293}, -{from: 16, to: 439}, -{from: 16, to: 540}, -{from: 16, to: 568}, -{from: 16, to: 640}, -{from: 16, to: 641}, -{from: 16, to: 695}, -{from: 16, to: 704}, -{from: 16, to: 708}, -{from: 16, to: 732}, -{from: 16, to: 733}, -{from: 17, to: 34}, -{from: 17, to: 49}, -{from: 17, to: 103}, -{from: 17, to: 104}, -{from: 17, to: 169}, -{from: 17, to: 229}, -{from: 17, to: 256}, -{from: 17, to: 267}, -{from: 17, to: 275}, -{from: 17, to: 276}, -{from: 17, to: 295}, -{from: 17, to: 317}, -{from: 17, to: 318}, -{from: 17, to: 355}, -{from: 17, to: 357}, -{from: 17, to: 446}, -{from: 17, to: 509}, -{from: 17, to: 510}, -{from: 17, to: 546}, -{from: 17, to: 564}, -{from: 17, to: 581}, -{from: 17, to: 592}, -{from: 18, to: 43}, -{from: 18, to: 65}, -{from: 18, to: 118}, -{from: 18, to: 138}, -{from: 18, to: 199}, -{from: 18, to: 220}, -{from: 18, to: 272}, -{from: 18, to: 340}, -{from: 18, to: 346}, -{from: 18, to: 347}, -{from: 18, to: 383}, -{from: 18, to: 387}, -{from: 18, to: 404}, -{from: 18, to: 437}, -{from: 18, to: 503}, -{from: 18, to: 520}, -{from: 18, to: 590}, -{from: 18, to: 628}, -{from: 18, to: 664}, -{from: 18, to: 670}, -{from: 18, to: 709}, -{from: 19, to: 26}, -{from: 19, to: 45}, -{from: 19, to: 46}, -{from: 19, to: 55}, -{from: 19, to: 58}, -{from: 19, to: 59}, -{from: 19, to: 123}, -{from: 19, to: 125}, -{from: 19, to: 141}, -{from: 19, to: 237}, -{from: 19, to: 249}, -{from: 19, to: 252}, -{from: 19, to: 291}, -{from: 19, to: 370}, -{from: 19, to: 416}, -{from: 19, to: 422}, -{from: 19, to: 447}, -{from: 19, to: 449}, -{from: 19, to: 452}, -{from: 19, to: 478}, -{from: 19, to: 481}, -{from: 19, to: 482}, -{from: 19, to: 622}, -{from: 19, to: 675}, -{from: 20, to: 62}, -{from: 20, to: 90}, -{from: 20, to: 91}, -{from: 20, to: 117}, -{from: 20, to: 126}, -{from: 20, to: 134}, -{from: 20, to: 156}, -{from: 20, to: 213}, -{from: 20, to: 242}, -{from: 20, to: 265}, -{from: 20, to: 326}, -{from: 20, to: 341}, -{from: 20, to: 365}, -{from: 20, to: 375}, -{from: 20, to: 406}, -{from: 20, to: 476}, -{from: 20, to: 502}, -{from: 20, to: 513}, -{from: 20, to: 530}, -{from: 20, to: 544}, -{from: 20, to: 681}, -{from: 20, to: 683}, -{from: 21, to: 22}, -{from: 21, to: 23}, -{from: 21, to: 24}, -{from: 21, to: 25}, -{from: 21, to: 48}, -{from: 21, to: 51}, -{from: 21, to: 74}, -{from: 21, to: 158}, -{from: 21, to: 174}, -{from: 21, to: 243}, -{from: 21, to: 289}, -{from: 21, to: 292}, -{from: 21, to: 293}, -{from: 21, to: 428}, -{from: 21, to: 439}, -{from: 21, to: 532}, -{from: 21, to: 540}, -{from: 21, to: 568}, -{from: 21, to: 640}, -{from: 21, to: 641}, -{from: 21, to: 695}, -{from: 21, to: 704}, -{from: 21, to: 708}, -{from: 21, to: 732}, -{from: 21, to: 733}, -{from: 22, to: 23}, -{from: 22, to: 24}, -{from: 22, to: 25}, -{from: 22, to: 48}, -{from: 22, to: 51}, -{from: 22, to: 120}, -{from: 22, to: 158}, -{from: 22, to: 174}, -{from: 22, to: 243}, -{from: 22, to: 292}, -{from: 22, to: 293}, -{from: 22, to: 439}, -{from: 22, to: 540}, -{from: 22, to: 568}, -{from: 22, to: 640}, -{from: 22, to: 641}, -{from: 22, to: 695}, -{from: 22, to: 704}, -{from: 22, to: 708}, -{from: 22, to: 732}, -{from: 22, to: 733}, -{from: 23, to: 24}, -{from: 23, to: 25}, -{from: 23, to: 48}, -{from: 23, to: 51}, -{from: 23, to: 158}, -{from: 23, to: 174}, -{from: 23, to: 243}, -{from: 23, to: 292}, -{from: 23, to: 293}, -{from: 23, to: 439}, -{from: 23, to: 540}, -{from: 23, to: 568}, -{from: 23, to: 640}, -{from: 23, to: 641}, -{from: 23, to: 695}, -{from: 23, to: 698}, -{from: 23, to: 704}, -{from: 23, to: 708}, -{from: 23, to: 732}, -{from: 23, to: 733}, -{from: 24, to: 25}, -{from: 24, to: 48}, -{from: 24, to: 51}, -{from: 24, to: 120}, -{from: 24, to: 158}, -{from: 24, to: 174}, -{from: 24, to: 243}, -{from: 24, to: 292}, -{from: 24, to: 293}, -{from: 24, to: 439}, -{from: 24, to: 540}, -{from: 24, to: 568}, -{from: 24, to: 640}, -{from: 24, to: 641}, -{from: 24, to: 695}, -{from: 24, to: 704}, -{from: 24, to: 708}, -{from: 24, to: 732}, -{from: 24, to: 733}, -{from: 25, to: 48}, -{from: 25, to: 51}, -{from: 25, to: 120}, -{from: 25, to: 158}, -{from: 25, to: 174}, -{from: 25, to: 243}, -{from: 25, to: 292}, -{from: 25, to: 293}, -{from: 25, to: 439}, -{from: 25, to: 540}, -{from: 25, to: 568}, -{from: 25, to: 640}, -{from: 25, to: 641}, -{from: 25, to: 695}, -{from: 25, to: 704}, -{from: 25, to: 708}, -{from: 25, to: 732}, -{from: 25, to: 733}, -{from: 26, to: 45}, -{from: 26, to: 46}, -{from: 26, to: 58}, -{from: 26, to: 59}, -{from: 26, to: 123}, -{from: 26, to: 125}, -{from: 26, to: 141}, -{from: 26, to: 237}, -{from: 26, to: 249}, -{from: 26, to: 252}, -{from: 26, to: 291}, -{from: 26, to: 416}, -{from: 26, to: 422}, -{from: 26, to: 447}, -{from: 26, to: 449}, -{from: 26, to: 452}, -{from: 26, to: 478}, -{from: 26, to: 481}, -{from: 26, to: 482}, -{from: 26, to: 558}, -{from: 26, to: 622}, -{from: 26, to: 675}, -{from: 27, to: 76}, -{from: 27, to: 80}, -{from: 27, to: 116}, -{from: 27, to: 139}, -{from: 27, to: 144}, -{from: 27, to: 231}, -{from: 27, to: 232}, -{from: 27, to: 238}, -{from: 27, to: 258}, -{from: 27, to: 303}, -{from: 27, to: 308}, -{from: 27, to: 335}, -{from: 27, to: 348}, -{from: 27, to: 409}, -{from: 27, to: 415}, -{from: 27, to: 433}, -{from: 27, to: 434}, -{from: 27, to: 498}, -{from: 27, to: 543}, -{from: 27, to: 573}, -{from: 27, to: 575}, -{from: 27, to: 576}, -{from: 27, to: 583}, -{from: 27, to: 603}, -{from: 27, to: 616}, -{from: 27, to: 629}, -{from: 27, to: 668}, -{from: 27, to: 679}, -{from: 27, to: 713}, -{from: 28, to: 33}, -{from: 28, to: 37}, -{from: 28, to: 50}, -{from: 28, to: 71}, -{from: 28, to: 83}, -{from: 28, to: 84}, -{from: 28, to: 107}, -{from: 28, to: 111}, -{from: 28, to: 113}, -{from: 28, to: 135}, -{from: 28, to: 146}, -{from: 28, to: 182}, -{from: 28, to: 210}, -{from: 28, to: 217}, -{from: 28, to: 245}, -{from: 28, to: 278}, -{from: 28, to: 319}, -{from: 28, to: 321}, -{from: 28, to: 337}, -{from: 28, to: 349}, -{from: 28, to: 368}, -{from: 28, to: 407}, -{from: 28, to: 419}, -{from: 28, to: 420}, -{from: 28, to: 429}, -{from: 28, to: 488}, -{from: 28, to: 489}, -{from: 28, to: 529}, -{from: 28, to: 533}, -{from: 28, to: 569}, -{from: 28, to: 579}, -{from: 28, to: 626}, -{from: 28, to: 627}, -{from: 28, to: 643}, -{from: 28, to: 662}, -{from: 28, to: 705}, -{from: 28, to: 720}, -{from: 29, to: 77}, -{from: 29, to: 81}, -{from: 29, to: 148}, -{from: 29, to: 208}, -{from: 29, to: 298}, -{from: 29, to: 307}, -{from: 29, to: 310}, -{from: 29, to: 313}, -{from: 29, to: 458}, -{from: 29, to: 459}, -{from: 29, to: 468}, -{from: 29, to: 470}, -{from: 29, to: 471}, -{from: 29, to: 477}, -{from: 29, to: 479}, -{from: 29, to: 515}, -{from: 29, to: 518}, -{from: 29, to: 541}, -{from: 29, to: 620}, -{from: 29, to: 680}, -{from: 29, to: 686}, -{from: 30, to: 60}, -{from: 30, to: 102}, -{from: 30, to: 120}, -{from: 30, to: 186}, -{from: 30, to: 201}, -{from: 30, to: 222}, -{from: 30, to: 228}, -{from: 30, to: 235}, -{from: 30, to: 236}, -{from: 30, to: 305}, -{from: 30, to: 324}, -{from: 30, to: 334}, -{from: 30, to: 353}, -{from: 30, to: 368}, -{from: 30, to: 429}, -{from: 30, to: 489}, -{from: 30, to: 499}, -{from: 30, to: 548}, -{from: 30, to: 552}, -{from: 30, to: 595}, -{from: 30, to: 710}, -{from: 31, to: 96}, -{from: 31, to: 99}, -{from: 31, to: 100}, -{from: 31, to: 105}, -{from: 31, to: 106}, -{from: 31, to: 130}, -{from: 31, to: 153}, -{from: 31, to: 181}, -{from: 31, to: 219}, -{from: 31, to: 234}, -{from: 31, to: 304}, -{from: 31, to: 309}, -{from: 31, to: 366}, -{from: 31, to: 369}, -{from: 31, to: 370}, -{from: 31, to: 457}, -{from: 31, to: 554}, -{from: 31, to: 630}, -{from: 31, to: 672}, -{from: 31, to: 701}, -{from: 32, to: 47}, -{from: 32, to: 170}, -{from: 32, to: 250}, -{from: 32, to: 251}, -{from: 32, to: 253}, -{from: 32, to: 254}, -{from: 32, to: 255}, -{from: 32, to: 356}, -{from: 32, to: 400}, -{from: 32, to: 401}, -{from: 32, to: 402}, -{from: 32, to: 410}, -{from: 32, to: 423}, -{from: 32, to: 545}, -{from: 32, to: 556}, -{from: 32, to: 557}, -{from: 32, to: 558}, -{from: 32, to: 656}, -{from: 32, to: 660}, -{from: 32, to: 674}, -{from: 32, to: 694}, -{from: 32, to: 696}, -{from: 33, to: 50}, -{from: 33, to: 64}, -{from: 33, to: 101}, -{from: 33, to: 111}, -{from: 33, to: 112}, -{from: 33, to: 124}, -{from: 33, to: 132}, -{from: 33, to: 135}, -{from: 33, to: 189}, -{from: 33, to: 207}, -{from: 33, to: 209}, -{from: 33, to: 214}, -{from: 33, to: 223}, -{from: 33, to: 230}, -{from: 33, to: 239}, -{from: 33, to: 245}, -{from: 33, to: 262}, -{from: 33, to: 319}, -{from: 33, to: 320}, -{from: 33, to: 344}, -{from: 33, to: 349}, -{from: 33, to: 354}, -{from: 33, to: 361}, -{from: 33, to: 362}, -{from: 33, to: 368}, -{from: 33, to: 419}, -{from: 33, to: 429}, -{from: 33, to: 445}, -{from: 33, to: 483}, -{from: 33, to: 484}, -{from: 33, to: 489}, -{from: 33, to: 512}, -{from: 33, to: 529}, -{from: 33, to: 569}, -{from: 33, to: 643}, -{from: 33, to: 720}, -{from: 34, to: 49}, -{from: 34, to: 103}, -{from: 34, to: 104}, -{from: 34, to: 169}, -{from: 34, to: 229}, -{from: 34, to: 256}, -{from: 34, to: 267}, -{from: 34, to: 275}, -{from: 34, to: 276}, -{from: 34, to: 295}, -{from: 34, to: 317}, -{from: 34, to: 318}, -{from: 34, to: 355}, -{from: 34, to: 357}, -{from: 34, to: 446}, -{from: 34, to: 509}, -{from: 34, to: 510}, -{from: 34, to: 546}, -{from: 34, to: 564}, -{from: 34, to: 581}, -{from: 34, to: 592}, -{from: 35, to: 36}, -{from: 35, to: 40}, -{from: 35, to: 41}, -{from: 35, to: 66}, -{from: 35, to: 78}, -{from: 35, to: 137}, -{from: 35, to: 192}, -{from: 35, to: 247}, -{from: 35, to: 273}, -{from: 35, to: 284}, -{from: 35, to: 306}, -{from: 35, to: 315}, -{from: 35, to: 380}, -{from: 35, to: 389}, -{from: 35, to: 467}, -{from: 35, to: 495}, -{from: 35, to: 570}, -{from: 35, to: 584}, -{from: 35, to: 598}, -{from: 35, to: 599}, -{from: 35, to: 666}, -{from: 36, to: 40}, -{from: 36, to: 41}, -{from: 36, to: 66}, -{from: 36, to: 78}, -{from: 36, to: 137}, -{from: 36, to: 192}, -{from: 36, to: 247}, -{from: 36, to: 273}, -{from: 36, to: 284}, -{from: 36, to: 306}, -{from: 36, to: 315}, -{from: 36, to: 380}, -{from: 36, to: 389}, -{from: 36, to: 467}, -{from: 36, to: 495}, -{from: 36, to: 570}, -{from: 36, to: 584}, -{from: 36, to: 598}, -{from: 36, to: 599}, -{from: 36, to: 666}, -{from: 37, to: 71}, -{from: 37, to: 83}, -{from: 37, to: 84}, -{from: 37, to: 107}, -{from: 37, to: 113}, -{from: 37, to: 146}, -{from: 37, to: 182}, -{from: 37, to: 210}, -{from: 37, to: 217}, -{from: 37, to: 278}, -{from: 37, to: 321}, -{from: 37, to: 337}, -{from: 37, to: 407}, -{from: 37, to: 420}, -{from: 37, to: 488}, -{from: 37, to: 533}, -{from: 37, to: 545}, -{from: 37, to: 579}, -{from: 37, to: 626}, -{from: 37, to: 627}, -{from: 37, to: 662}, -{from: 37, to: 705}, -{from: 37, to: 722}, -{from: 38, to: 39}, -{from: 38, to: 58}, -{from: 38, to: 121}, -{from: 38, to: 129}, -{from: 38, to: 165}, -{from: 38, to: 166}, -{from: 38, to: 167}, -{from: 38, to: 168}, -{from: 38, to: 185}, -{from: 38, to: 191}, -{from: 38, to: 226}, -{from: 38, to: 240}, -{from: 38, to: 277}, -{from: 38, to: 352}, -{from: 38, to: 359}, -{from: 38, to: 424}, -{from: 38, to: 430}, -{from: 38, to: 461}, -{from: 38, to: 463}, -{from: 38, to: 486}, -{from: 38, to: 517}, -{from: 38, to: 531}, -{from: 38, to: 537}, -{from: 38, to: 607}, -{from: 38, to: 634}, -{from: 38, to: 636}, -{from: 38, to: 711}, -{from: 39, to: 121}, -{from: 39, to: 129}, -{from: 39, to: 165}, -{from: 39, to: 166}, -{from: 39, to: 167}, -{from: 39, to: 168}, -{from: 39, to: 185}, -{from: 39, to: 191}, -{from: 39, to: 226}, -{from: 39, to: 240}, -{from: 39, to: 352}, -{from: 39, to: 359}, -{from: 39, to: 430}, -{from: 39, to: 461}, -{from: 39, to: 463}, -{from: 39, to: 486}, -{from: 39, to: 531}, -{from: 39, to: 607}, -{from: 39, to: 634}, -{from: 39, to: 711}, -{from: 40, to: 41}, -{from: 40, to: 66}, -{from: 40, to: 78}, -{from: 40, to: 137}, -{from: 40, to: 192}, -{from: 40, to: 247}, -{from: 40, to: 273}, -{from: 40, to: 284}, -{from: 40, to: 306}, -{from: 40, to: 315}, -{from: 40, to: 380}, -{from: 40, to: 389}, -{from: 40, to: 467}, -{from: 40, to: 495}, -{from: 40, to: 570}, -{from: 40, to: 584}, -{from: 40, to: 598}, -{from: 40, to: 599}, -{from: 40, to: 666}, -{from: 41, to: 66}, -{from: 41, to: 78}, -{from: 41, to: 137}, -{from: 41, to: 192}, -{from: 41, to: 247}, -{from: 41, to: 273}, -{from: 41, to: 284}, -{from: 41, to: 306}, -{from: 41, to: 315}, -{from: 41, to: 380}, -{from: 41, to: 389}, -{from: 41, to: 467}, -{from: 41, to: 495}, -{from: 41, to: 570}, -{from: 41, to: 584}, -{from: 41, to: 598}, -{from: 41, to: 599}, -{from: 41, to: 666}, -{from: 42, to: 86}, -{from: 42, to: 93}, -{from: 42, to: 131}, -{from: 42, to: 180}, -{from: 42, to: 188}, -{from: 42, to: 202}, -{from: 42, to: 211}, -{from: 42, to: 216}, -{from: 42, to: 277}, -{from: 42, to: 286}, -{from: 42, to: 332}, -{from: 42, to: 333}, -{from: 42, to: 428}, -{from: 42, to: 486}, -{from: 42, to: 511}, -{from: 42, to: 528}, -{from: 42, to: 571}, -{from: 42, to: 580}, -{from: 42, to: 593}, -{from: 42, to: 601}, -{from: 42, to: 618}, -{from: 42, to: 619}, -{from: 42, to: 652}, -{from: 42, to: 703}, -{from: 42, to: 716}, -{from: 43, to: 65}, -{from: 43, to: 118}, -{from: 43, to: 138}, -{from: 43, to: 199}, -{from: 43, to: 220}, -{from: 43, to: 272}, -{from: 43, to: 340}, -{from: 43, to: 346}, -{from: 43, to: 347}, -{from: 43, to: 387}, -{from: 43, to: 404}, -{from: 43, to: 437}, -{from: 43, to: 473}, -{from: 43, to: 503}, -{from: 43, to: 520}, -{from: 43, to: 533}, -{from: 43, to: 590}, -{from: 43, to: 628}, -{from: 43, to: 664}, -{from: 43, to: 670}, -{from: 43, to: 709}, -{from: 44, to: 79}, -{from: 44, to: 82}, -{from: 44, to: 110}, -{from: 44, to: 122}, -{from: 44, to: 151}, -{from: 44, to: 179}, -{from: 44, to: 203}, -{from: 44, to: 227}, -{from: 44, to: 231}, -{from: 44, to: 238}, -{from: 44, to: 327}, -{from: 44, to: 342}, -{from: 44, to: 374}, -{from: 44, to: 385}, -{from: 44, to: 433}, -{from: 44, to: 442}, -{from: 44, to: 454}, -{from: 44, to: 475}, -{from: 44, to: 480}, -{from: 44, to: 498}, -{from: 44, to: 517}, -{from: 44, to: 547}, -{from: 44, to: 573}, -{from: 44, to: 577}, -{from: 44, to: 586}, -{from: 44, to: 611}, -{from: 44, to: 614}, -{from: 44, to: 623}, -{from: 44, to: 627}, -{from: 44, to: 648}, -{from: 44, to: 678}, -{from: 44, to: 687}, -{from: 44, to: 717}, -{from: 45, to: 46}, -{from: 45, to: 58}, -{from: 45, to: 59}, -{from: 45, to: 64}, -{from: 45, to: 123}, -{from: 45, to: 125}, -{from: 45, to: 141}, -{from: 45, to: 237}, -{from: 45, to: 249}, -{from: 45, to: 252}, -{from: 45, to: 291}, -{from: 45, to: 404}, -{from: 45, to: 416}, -{from: 45, to: 422}, -{from: 45, to: 447}, -{from: 45, to: 449}, -{from: 45, to: 452}, -{from: 45, to: 461}, -{from: 45, to: 478}, -{from: 45, to: 481}, -{from: 45, to: 482}, -{from: 45, to: 483}, -{from: 45, to: 565}, -{from: 45, to: 622}, -{from: 45, to: 661}, -{from: 45, to: 675}, -{from: 46, to: 58}, -{from: 46, to: 59}, -{from: 46, to: 64}, -{from: 46, to: 123}, -{from: 46, to: 125}, -{from: 46, to: 141}, -{from: 46, to: 237}, -{from: 46, to: 249}, -{from: 46, to: 252}, -{from: 46, to: 291}, -{from: 46, to: 404}, -{from: 46, to: 416}, -{from: 46, to: 422}, -{from: 46, to: 447}, -{from: 46, to: 449}, -{from: 46, to: 452}, -{from: 46, to: 461}, -{from: 46, to: 478}, -{from: 46, to: 481}, -{from: 46, to: 482}, -{from: 46, to: 483}, -{from: 46, to: 565}, -{from: 46, to: 622}, -{from: 46, to: 661}, -{from: 46, to: 675}, -{from: 47, to: 170}, -{from: 47, to: 250}, -{from: 47, to: 251}, -{from: 47, to: 253}, -{from: 47, to: 254}, -{from: 47, to: 255}, -{from: 47, to: 336}, -{from: 47, to: 356}, -{from: 47, to: 400}, -{from: 47, to: 401}, -{from: 47, to: 402}, -{from: 47, to: 410}, -{from: 47, to: 423}, -{from: 47, to: 545}, -{from: 47, to: 556}, -{from: 47, to: 557}, -{from: 47, to: 558}, -{from: 47, to: 656}, -{from: 47, to: 660}, -{from: 47, to: 674}, -{from: 47, to: 694}, -{from: 47, to: 696}, -{from: 48, to: 51}, -{from: 48, to: 158}, -{from: 48, to: 174}, -{from: 48, to: 243}, -{from: 48, to: 292}, -{from: 48, to: 293}, -{from: 48, to: 439}, -{from: 48, to: 540}, -{from: 48, to: 568}, -{from: 48, to: 640}, -{from: 48, to: 641}, -{from: 48, to: 695}, -{from: 48, to: 704}, -{from: 48, to: 708}, -{from: 48, to: 732}, -{from: 48, to: 733}, -{from: 49, to: 103}, -{from: 49, to: 104}, -{from: 49, to: 169}, -{from: 49, to: 198}, -{from: 49, to: 229}, -{from: 49, to: 256}, -{from: 49, to: 267}, -{from: 49, to: 275}, -{from: 49, to: 276}, -{from: 49, to: 295}, -{from: 49, to: 317}, -{from: 49, to: 318}, -{from: 49, to: 355}, -{from: 49, to: 357}, -{from: 49, to: 446}, -{from: 49, to: 509}, -{from: 49, to: 510}, -{from: 49, to: 546}, -{from: 49, to: 564}, -{from: 49, to: 581}, -{from: 49, to: 592}, -{from: 49, to: 658}, -{from: 50, to: 110}, -{from: 50, to: 111}, -{from: 50, to: 135}, -{from: 50, to: 150}, -{from: 50, to: 154}, -{from: 50, to: 155}, -{from: 50, to: 164}, -{from: 50, to: 227}, -{from: 50, to: 245}, -{from: 50, to: 294}, -{from: 50, to: 316}, -{from: 50, to: 319}, -{from: 50, to: 349}, -{from: 50, to: 368}, -{from: 50, to: 371}, -{from: 50, to: 373}, -{from: 50, to: 397}, -{from: 50, to: 419}, -{from: 50, to: 429}, -{from: 50, to: 489}, -{from: 50, to: 529}, -{from: 50, to: 569}, -{from: 50, to: 572}, -{from: 50, to: 591}, -{from: 50, to: 629}, -{from: 50, to: 643}, -{from: 50, to: 644}, -{from: 50, to: 719}, -{from: 50, to: 720}, -{from: 51, to: 158}, -{from: 51, to: 174}, -{from: 51, to: 243}, -{from: 51, to: 292}, -{from: 51, to: 293}, -{from: 51, to: 439}, -{from: 51, to: 540}, -{from: 51, to: 568}, -{from: 51, to: 640}, -{from: 51, to: 641}, -{from: 51, to: 695}, -{from: 51, to: 704}, -{from: 51, to: 708}, -{from: 51, to: 732}, -{from: 51, to: 733}, -{from: 52, to: 56}, -{from: 52, to: 92}, -{from: 52, to: 98}, -{from: 52, to: 176}, -{from: 52, to: 178}, -{from: 52, to: 197}, -{from: 52, to: 328}, -{from: 52, to: 329}, -{from: 52, to: 351}, -{from: 52, to: 367}, -{from: 52, to: 372}, -{from: 52, to: 426}, -{from: 52, to: 427}, -{from: 52, to: 456}, -{from: 52, to: 464}, -{from: 52, to: 492}, -{from: 52, to: 536}, -{from: 52, to: 549}, -{from: 52, to: 551}, -{from: 52, to: 609}, -{from: 52, to: 615}, -{from: 52, to: 700}, -{from: 52, to: 718}, -{from: 53, to: 67}, -{from: 53, to: 68}, -{from: 53, to: 73}, -{from: 53, to: 183}, -{from: 53, to: 184}, -{from: 53, to: 198}, -{from: 53, to: 204}, -{from: 53, to: 270}, -{from: 53, to: 302}, -{from: 53, to: 312}, -{from: 53, to: 497}, -{from: 53, to: 516}, -{from: 53, to: 524}, -{from: 53, to: 538}, -{from: 53, to: 633}, -{from: 53, to: 635}, -{from: 53, to: 636}, -{from: 53, to: 637}, -{from: 53, to: 684}, -{from: 53, to: 688}, -{from: 53, to: 697}, -{from: 53, to: 736}, -{from: 54, to: 70}, -{from: 54, to: 131}, -{from: 54, to: 202}, -{from: 54, to: 211}, -{from: 54, to: 212}, -{from: 54, to: 216}, -{from: 54, to: 221}, -{from: 54, to: 225}, -{from: 54, to: 261}, -{from: 54, to: 287}, -{from: 54, to: 294}, -{from: 54, to: 319}, -{from: 54, to: 358}, -{from: 54, to: 381}, -{from: 54, to: 419}, -{from: 54, to: 424}, -{from: 54, to: 432}, -{from: 54, to: 443}, -{from: 54, to: 450}, -{from: 54, to: 451}, -{from: 54, to: 462}, -{from: 54, to: 487}, -{from: 54, to: 555}, -{from: 54, to: 571}, -{from: 54, to: 589}, -{from: 54, to: 600}, -{from: 54, to: 608}, -{from: 54, to: 623}, -{from: 54, to: 642}, -{from: 54, to: 644}, -{from: 54, to: 645}, -{from: 54, to: 719}, -{from: 55, to: 142}, -{from: 55, to: 143}, -{from: 55, to: 147}, -{from: 55, to: 157}, -{from: 55, to: 175}, -{from: 55, to: 187}, -{from: 55, to: 263}, -{from: 55, to: 299}, -{from: 55, to: 300}, -{from: 55, to: 301}, -{from: 55, to: 370}, -{from: 55, to: 432}, -{from: 55, to: 444}, -{from: 55, to: 455}, -{from: 55, to: 469}, -{from: 55, to: 514}, -{from: 55, to: 535}, -{from: 55, to: 539}, -{from: 55, to: 542}, -{from: 55, to: 624}, -{from: 55, to: 653}, -{from: 55, to: 669}, -{from: 55, to: 698}, -{from: 56, to: 74}, -{from: 56, to: 140}, -{from: 56, to: 172}, -{from: 56, to: 177}, -{from: 56, to: 179}, -{from: 56, to: 311}, -{from: 56, to: 384}, -{from: 56, to: 386}, -{from: 56, to: 408}, -{from: 56, to: 460}, -{from: 56, to: 522}, -{from: 56, to: 527}, -{from: 56, to: 532}, -{from: 56, to: 612}, -{from: 56, to: 625}, -{from: 56, to: 654}, -{from: 56, to: 667}, -{from: 56, to: 677}, -{from: 56, to: 679}, -{from: 56, to: 685}, -{from: 56, to: 707}, -{from: 57, to: 76}, -{from: 57, to: 87}, -{from: 57, to: 124}, -{from: 57, to: 196}, -{from: 57, to: 271}, -{from: 57, to: 288}, -{from: 57, to: 381}, -{from: 57, to: 409}, -{from: 57, to: 421}, -{from: 57, to: 425}, -{from: 57, to: 440}, -{from: 57, to: 472}, -{from: 57, to: 473}, -{from: 57, to: 508}, -{from: 57, to: 521}, -{from: 57, to: 523}, -{from: 57, to: 543}, -{from: 57, to: 562}, -{from: 57, to: 565}, -{from: 57, to: 589}, -{from: 57, to: 594}, -{from: 57, to: 604}, -{from: 57, to: 615}, -{from: 57, to: 663}, -{from: 57, to: 728}, -{from: 58, to: 59}, -{from: 58, to: 123}, -{from: 58, to: 125}, -{from: 58, to: 141}, -{from: 58, to: 237}, -{from: 58, to: 249}, -{from: 58, to: 252}, -{from: 58, to: 277}, -{from: 58, to: 291}, -{from: 58, to: 416}, -{from: 58, to: 422}, -{from: 58, to: 424}, -{from: 58, to: 447}, -{from: 58, to: 449}, -{from: 58, to: 452}, -{from: 58, to: 478}, -{from: 58, to: 481}, -{from: 58, to: 482}, -{from: 58, to: 517}, -{from: 58, to: 537}, -{from: 58, to: 622}, -{from: 58, to: 636}, -{from: 58, to: 675}, -{from: 59, to: 123}, -{from: 59, to: 125}, -{from: 59, to: 141}, -{from: 59, to: 237}, -{from: 59, to: 249}, -{from: 59, to: 252}, -{from: 59, to: 291}, -{from: 59, to: 416}, -{from: 59, to: 422}, -{from: 59, to: 447}, -{from: 59, to: 449}, -{from: 59, to: 452}, -{from: 59, to: 478}, -{from: 59, to: 481}, -{from: 59, to: 482}, -{from: 59, to: 622}, -{from: 59, to: 675}, -{from: 59, to: 711}, -{from: 60, to: 102}, -{from: 60, to: 116}, -{from: 60, to: 120}, -{from: 60, to: 144}, -{from: 60, to: 150}, -{from: 60, to: 186}, -{from: 60, to: 201}, -{from: 60, to: 222}, -{from: 60, to: 228}, -{from: 60, to: 235}, -{from: 60, to: 236}, -{from: 60, to: 305}, -{from: 60, to: 318}, -{from: 60, to: 324}, -{from: 60, to: 334}, -{from: 60, to: 353}, -{from: 60, to: 368}, -{from: 60, to: 371}, -{from: 60, to: 429}, -{from: 60, to: 460}, -{from: 60, to: 489}, -{from: 60, to: 499}, -{from: 60, to: 528}, -{from: 60, to: 548}, -{from: 60, to: 552}, -{from: 60, to: 562}, -{from: 60, to: 576}, -{from: 60, to: 595}, -{from: 60, to: 606}, -{from: 60, to: 646}, -{from: 60, to: 710}, -{from: 60, to: 713}, -{from: 61, to: 79}, -{from: 61, to: 94}, -{from: 61, to: 133}, -{from: 61, to: 140}, -{from: 61, to: 145}, -{from: 61, to: 149}, -{from: 61, to: 171}, -{from: 61, to: 244}, -{from: 61, to: 314}, -{from: 61, to: 316}, -{from: 61, to: 325}, -{from: 61, to: 327}, -{from: 61, to: 338}, -{from: 61, to: 345}, -{from: 61, to: 350}, -{from: 61, to: 375}, -{from: 61, to: 396}, -{from: 61, to: 417}, -{from: 61, to: 442}, -{from: 61, to: 454}, -{from: 61, to: 455}, -{from: 61, to: 496}, -{from: 61, to: 507}, -{from: 61, to: 534}, -{from: 61, to: 566}, -{from: 61, to: 577}, -{from: 61, to: 606}, -{from: 61, to: 613}, -{from: 61, to: 659}, -{from: 61, to: 673}, -{from: 61, to: 678}, -{from: 61, to: 682}, -{from: 61, to: 687}, -{from: 61, to: 714}, -{from: 61, to: 721}, -{from: 62, to: 90}, -{from: 62, to: 91}, -{from: 62, to: 117}, -{from: 62, to: 126}, -{from: 62, to: 134}, -{from: 62, to: 156}, -{from: 62, to: 213}, -{from: 62, to: 242}, -{from: 62, to: 265}, -{from: 62, to: 326}, -{from: 62, to: 341}, -{from: 62, to: 365}, -{from: 62, to: 375}, -{from: 62, to: 406}, -{from: 62, to: 476}, -{from: 62, to: 502}, -{from: 62, to: 513}, -{from: 62, to: 530}, -{from: 62, to: 544}, -{from: 62, to: 681}, -{from: 62, to: 683}, -{from: 63, to: 89}, -{from: 63, to: 102}, -{from: 63, to: 114}, -{from: 63, to: 127}, -{from: 63, to: 159}, -{from: 63, to: 160}, -{from: 63, to: 161}, -{from: 63, to: 246}, -{from: 63, to: 257}, -{from: 63, to: 264}, -{from: 63, to: 297}, -{from: 63, to: 322}, -{from: 63, to: 398}, -{from: 63, to: 474}, -{from: 63, to: 485}, -{from: 63, to: 553}, -{from: 63, to: 621}, -{from: 63, to: 632}, -{from: 63, to: 638}, -{from: 63, to: 639}, -{from: 63, to: 657}, -{from: 63, to: 671}, -{from: 63, to: 697}, -{from: 63, to: 715}, -{from: 63, to: 726}, -{from: 64, to: 101}, -{from: 64, to: 112}, -{from: 64, to: 124}, -{from: 64, to: 125}, -{from: 64, to: 132}, -{from: 64, to: 189}, -{from: 64, to: 207}, -{from: 64, to: 209}, -{from: 64, to: 214}, -{from: 64, to: 223}, -{from: 64, to: 230}, -{from: 64, to: 239}, -{from: 64, to: 249}, -{from: 64, to: 252}, -{from: 64, to: 262}, -{from: 64, to: 320}, -{from: 64, to: 344}, -{from: 64, to: 354}, -{from: 64, to: 361}, -{from: 64, to: 362}, -{from: 64, to: 404}, -{from: 64, to: 416}, -{from: 64, to: 445}, -{from: 64, to: 461}, -{from: 64, to: 483}, -{from: 64, to: 484}, -{from: 64, to: 512}, -{from: 64, to: 565}, -{from: 64, to: 661}, -{from: 65, to: 118}, -{from: 65, to: 138}, -{from: 65, to: 199}, -{from: 65, to: 220}, -{from: 65, to: 272}, -{from: 65, to: 340}, -{from: 65, to: 346}, -{from: 65, to: 347}, -{from: 65, to: 387}, -{from: 65, to: 404}, -{from: 65, to: 437}, -{from: 65, to: 503}, -{from: 65, to: 520}, -{from: 65, to: 590}, -{from: 65, to: 628}, -{from: 65, to: 664}, -{from: 65, to: 670}, -{from: 65, to: 709}, -{from: 66, to: 78}, -{from: 66, to: 137}, -{from: 66, to: 192}, -{from: 66, to: 247}, -{from: 66, to: 253}, -{from: 66, to: 273}, -{from: 66, to: 284}, -{from: 66, to: 306}, -{from: 66, to: 315}, -{from: 66, to: 380}, -{from: 66, to: 389}, -{from: 66, to: 402}, -{from: 66, to: 467}, -{from: 66, to: 495}, -{from: 66, to: 570}, -{from: 66, to: 584}, -{from: 66, to: 598}, -{from: 66, to: 599}, -{from: 66, to: 666}, -{from: 67, to: 68}, -{from: 67, to: 73}, -{from: 67, to: 183}, -{from: 67, to: 184}, -{from: 67, to: 198}, -{from: 67, to: 204}, -{from: 67, to: 270}, -{from: 67, to: 302}, -{from: 67, to: 312}, -{from: 67, to: 497}, -{from: 67, to: 516}, -{from: 67, to: 524}, -{from: 67, to: 538}, -{from: 67, to: 633}, -{from: 67, to: 635}, -{from: 67, to: 636}, -{from: 67, to: 637}, -{from: 67, to: 684}, -{from: 67, to: 688}, -{from: 67, to: 697}, -{from: 67, to: 736}, -{from: 68, to: 73}, -{from: 68, to: 183}, -{from: 68, to: 184}, -{from: 68, to: 198}, -{from: 68, to: 204}, -{from: 68, to: 242}, -{from: 68, to: 270}, -{from: 68, to: 302}, -{from: 68, to: 312}, -{from: 68, to: 497}, -{from: 68, to: 516}, -{from: 68, to: 524}, -{from: 68, to: 538}, -{from: 68, to: 574}, -{from: 68, to: 633}, -{from: 68, to: 635}, -{from: 68, to: 636}, -{from: 68, to: 637}, -{from: 68, to: 684}, -{from: 68, to: 688}, -{from: 68, to: 697}, -{from: 68, to: 718}, -{from: 68, to: 736}, -{from: 69, to: 82}, -{from: 69, to: 193}, -{from: 69, to: 264}, -{from: 69, to: 281}, -{from: 69, to: 282}, -{from: 69, to: 285}, -{from: 69, to: 337}, -{from: 69, to: 374}, -{from: 69, to: 382}, -{from: 69, to: 387}, -{from: 69, to: 396}, -{from: 69, to: 438}, -{from: 69, to: 441}, -{from: 69, to: 465}, -{from: 69, to: 466}, -{from: 69, to: 491}, -{from: 69, to: 633}, -{from: 69, to: 646}, -{from: 69, to: 647}, -{from: 69, to: 650}, -{from: 69, to: 651}, -{from: 69, to: 689}, -{from: 69, to: 724}, -{from: 69, to: 725}, -{from: 69, to: 729}, -{from: 69, to: 730}, -{from: 69, to: 734}, -{from: 69, to: 735}, -{from: 70, to: 202}, -{from: 70, to: 211}, -{from: 70, to: 212}, -{from: 70, to: 214}, -{from: 70, to: 221}, -{from: 70, to: 225}, -{from: 70, to: 261}, -{from: 70, to: 287}, -{from: 70, to: 319}, -{from: 70, to: 358}, -{from: 70, to: 419}, -{from: 70, to: 424}, -{from: 70, to: 450}, -{from: 70, to: 451}, -{from: 70, to: 462}, -{from: 70, to: 487}, -{from: 70, to: 555}, -{from: 70, to: 600}, -{from: 70, to: 608}, -{from: 70, to: 642}, -{from: 70, to: 645}, -{from: 71, to: 83}, -{from: 71, to: 84}, -{from: 71, to: 107}, -{from: 71, to: 113}, -{from: 71, to: 146}, -{from: 71, to: 159}, -{from: 71, to: 182}, -{from: 71, to: 210}, -{from: 71, to: 217}, -{from: 71, to: 226}, -{from: 71, to: 278}, -{from: 71, to: 302}, -{from: 71, to: 321}, -{from: 71, to: 337}, -{from: 71, to: 407}, -{from: 71, to: 420}, -{from: 71, to: 488}, -{from: 71, to: 533}, -{from: 71, to: 579}, -{from: 71, to: 626}, -{from: 71, to: 627}, -{from: 71, to: 662}, -{from: 71, to: 705}, -{from: 71, to: 714}, -{from: 72, to: 75}, -{from: 72, to: 115}, -{from: 72, to: 190}, -{from: 72, to: 194}, -{from: 72, to: 200}, -{from: 72, to: 259}, -{from: 72, to: 342}, -{from: 72, to: 363}, -{from: 72, to: 379}, -{from: 72, to: 383}, -{from: 72, to: 403}, -{from: 72, to: 500}, -{from: 72, to: 505}, -{from: 72, to: 537}, -{from: 72, to: 574}, -{from: 72, to: 587}, -{from: 72, to: 597}, -{from: 72, to: 649}, -{from: 72, to: 691}, -{from: 72, to: 702}, -{from: 72, to: 706}, -{from: 73, to: 183}, -{from: 73, to: 184}, -{from: 73, to: 198}, -{from: 73, to: 204}, -{from: 73, to: 270}, -{from: 73, to: 302}, -{from: 73, to: 312}, -{from: 73, to: 497}, -{from: 73, to: 516}, -{from: 73, to: 524}, -{from: 73, to: 538}, -{from: 73, to: 633}, -{from: 73, to: 635}, -{from: 73, to: 636}, -{from: 73, to: 637}, -{from: 73, to: 684}, -{from: 73, to: 688}, -{from: 73, to: 697}, -{from: 73, to: 736}, -{from: 74, to: 140}, -{from: 74, to: 172}, -{from: 74, to: 177}, -{from: 74, to: 179}, -{from: 74, to: 289}, -{from: 74, to: 311}, -{from: 74, to: 384}, -{from: 74, to: 386}, -{from: 74, to: 408}, -{from: 74, to: 428}, -{from: 74, to: 460}, -{from: 74, to: 522}, -{from: 74, to: 527}, -{from: 74, to: 532}, -{from: 74, to: 540}, -{from: 74, to: 612}, -{from: 74, to: 625}, -{from: 74, to: 654}, -{from: 74, to: 667}, -{from: 74, to: 677}, -{from: 74, to: 679}, -{from: 74, to: 685}, -{from: 74, to: 704}, -{from: 74, to: 707}, -{from: 74, to: 732}, -{from: 75, to: 115}, -{from: 75, to: 190}, -{from: 75, to: 194}, -{from: 75, to: 200}, -{from: 75, to: 259}, -{from: 75, to: 342}, -{from: 75, to: 363}, -{from: 75, to: 379}, -{from: 75, to: 383}, -{from: 75, to: 403}, -{from: 75, to: 500}, -{from: 75, to: 505}, -{from: 75, to: 537}, -{from: 75, to: 574}, -{from: 75, to: 587}, -{from: 75, to: 597}, -{from: 75, to: 649}, -{from: 75, to: 691}, -{from: 75, to: 702}, -{from: 75, to: 706}, -{from: 76, to: 87}, -{from: 76, to: 196}, -{from: 76, to: 288}, -{from: 76, to: 303}, -{from: 76, to: 381}, -{from: 76, to: 409}, -{from: 76, to: 421}, -{from: 76, to: 425}, -{from: 76, to: 433}, -{from: 76, to: 440}, -{from: 76, to: 472}, -{from: 76, to: 473}, -{from: 76, to: 498}, -{from: 76, to: 508}, -{from: 76, to: 521}, -{from: 76, to: 523}, -{from: 76, to: 543}, -{from: 76, to: 562}, -{from: 76, to: 565}, -{from: 76, to: 573}, -{from: 76, to: 589}, -{from: 76, to: 594}, -{from: 76, to: 604}, -{from: 76, to: 629}, -{from: 76, to: 663}, -{from: 76, to: 679}, -{from: 76, to: 728}, -{from: 77, to: 81}, -{from: 77, to: 148}, -{from: 77, to: 208}, -{from: 77, to: 298}, -{from: 77, to: 307}, -{from: 77, to: 310}, -{from: 77, to: 313}, -{from: 77, to: 458}, -{from: 77, to: 459}, -{from: 77, to: 468}, -{from: 77, to: 470}, -{from: 77, to: 471}, -{from: 77, to: 477}, -{from: 77, to: 479}, -{from: 77, to: 515}, -{from: 77, to: 518}, -{from: 77, to: 541}, -{from: 77, to: 620}, -{from: 77, to: 680}, -{from: 77, to: 686}, -{from: 78, to: 137}, -{from: 78, to: 192}, -{from: 78, to: 247}, -{from: 78, to: 273}, -{from: 78, to: 284}, -{from: 78, to: 306}, -{from: 78, to: 315}, -{from: 78, to: 380}, -{from: 78, to: 389}, -{from: 78, to: 467}, -{from: 78, to: 495}, -{from: 78, to: 570}, -{from: 78, to: 584}, -{from: 78, to: 598}, -{from: 78, to: 599}, -{from: 78, to: 666}, -{from: 79, to: 82}, -{from: 79, to: 122}, -{from: 79, to: 140}, -{from: 79, to: 145}, -{from: 79, to: 203}, -{from: 79, to: 316}, -{from: 79, to: 327}, -{from: 79, to: 374}, -{from: 79, to: 375}, -{from: 79, to: 385}, -{from: 79, to: 433}, -{from: 79, to: 442}, -{from: 79, to: 454}, -{from: 79, to: 455}, -{from: 79, to: 475}, -{from: 79, to: 480}, -{from: 79, to: 498}, -{from: 79, to: 517}, -{from: 79, to: 573}, -{from: 79, to: 577}, -{from: 79, to: 611}, -{from: 79, to: 614}, -{from: 79, to: 623}, -{from: 79, to: 648}, -{from: 79, to: 678}, -{from: 79, to: 687}, -{from: 79, to: 721}, -{from: 80, to: 116}, -{from: 80, to: 139}, -{from: 80, to: 144}, -{from: 80, to: 167}, -{from: 80, to: 231}, -{from: 80, to: 232}, -{from: 80, to: 238}, -{from: 80, to: 258}, -{from: 80, to: 303}, -{from: 80, to: 308}, -{from: 80, to: 335}, -{from: 80, to: 348}, -{from: 80, to: 415}, -{from: 80, to: 434}, -{from: 80, to: 575}, -{from: 80, to: 576}, -{from: 80, to: 583}, -{from: 80, to: 603}, -{from: 80, to: 616}, -{from: 80, to: 668}, -{from: 80, to: 713}, -{from: 81, to: 148}, -{from: 81, to: 208}, -{from: 81, to: 298}, -{from: 81, to: 307}, -{from: 81, to: 310}, -{from: 81, to: 313}, -{from: 81, to: 458}, -{from: 81, to: 459}, -{from: 81, to: 468}, -{from: 81, to: 470}, -{from: 81, to: 471}, -{from: 81, to: 474}, -{from: 81, to: 477}, -{from: 81, to: 479}, -{from: 81, to: 515}, -{from: 81, to: 518}, -{from: 81, to: 541}, -{from: 81, to: 620}, -{from: 81, to: 680}, -{from: 81, to: 686}, -{from: 82, to: 122}, -{from: 82, to: 203}, -{from: 82, to: 327}, -{from: 82, to: 337}, -{from: 82, to: 374}, -{from: 82, to: 385}, -{from: 82, to: 387}, -{from: 82, to: 396}, -{from: 82, to: 433}, -{from: 82, to: 442}, -{from: 82, to: 454}, -{from: 82, to: 475}, -{from: 82, to: 480}, -{from: 82, to: 498}, -{from: 82, to: 517}, -{from: 82, to: 573}, -{from: 82, to: 577}, -{from: 82, to: 611}, -{from: 82, to: 614}, -{from: 82, to: 623}, -{from: 82, to: 633}, -{from: 82, to: 648}, -{from: 82, to: 678}, -{from: 82, to: 687}, -{from: 83, to: 84}, -{from: 83, to: 107}, -{from: 83, to: 113}, -{from: 83, to: 146}, -{from: 83, to: 182}, -{from: 83, to: 210}, -{from: 83, to: 217}, -{from: 83, to: 278}, -{from: 83, to: 321}, -{from: 83, to: 337}, -{from: 83, to: 407}, -{from: 83, to: 420}, -{from: 83, to: 488}, -{from: 83, to: 533}, -{from: 83, to: 579}, -{from: 83, to: 626}, -{from: 83, to: 627}, -{from: 83, to: 662}, -{from: 83, to: 705}, -{from: 84, to: 107}, -{from: 84, to: 113}, -{from: 84, to: 146}, -{from: 84, to: 182}, -{from: 84, to: 210}, -{from: 84, to: 217}, -{from: 84, to: 278}, -{from: 84, to: 321}, -{from: 84, to: 337}, -{from: 84, to: 407}, -{from: 84, to: 420}, -{from: 84, to: 488}, -{from: 84, to: 533}, -{from: 84, to: 579}, -{from: 84, to: 626}, -{from: 84, to: 627}, -{from: 84, to: 662}, -{from: 84, to: 705}, -{from: 84, to: 731}, -{from: 85, to: 135}, -{from: 85, to: 145}, -{from: 85, to: 147}, -{from: 85, to: 151}, -{from: 85, to: 187}, -{from: 85, to: 224}, -{from: 85, to: 233}, -{from: 85, to: 279}, -{from: 85, to: 280}, -{from: 85, to: 289}, -{from: 85, to: 323}, -{from: 85, to: 331}, -{from: 85, to: 376}, -{from: 85, to: 431}, -{from: 85, to: 436}, -{from: 85, to: 443}, -{from: 85, to: 490}, -{from: 85, to: 529}, -{from: 85, to: 547}, -{from: 85, to: 567}, -{from: 85, to: 586}, -{from: 85, to: 676}, -{from: 85, to: 699}, -{from: 85, to: 717}, -{from: 86, to: 93}, -{from: 86, to: 99}, -{from: 86, to: 131}, -{from: 86, to: 180}, -{from: 86, to: 188}, -{from: 86, to: 216}, -{from: 86, to: 277}, -{from: 86, to: 286}, -{from: 86, to: 300}, -{from: 86, to: 332}, -{from: 86, to: 333}, -{from: 86, to: 428}, -{from: 86, to: 511}, -{from: 86, to: 528}, -{from: 86, to: 571}, -{from: 86, to: 580}, -{from: 86, to: 593}, -{from: 86, to: 601}, -{from: 86, to: 618}, -{from: 86, to: 619}, -{from: 86, to: 652}, -{from: 86, to: 662}, -{from: 86, to: 703}, -{from: 86, to: 716}, -{from: 87, to: 185}, -{from: 87, to: 196}, -{from: 87, to: 212}, -{from: 87, to: 288}, -{from: 87, to: 381}, -{from: 87, to: 409}, -{from: 87, to: 421}, -{from: 87, to: 425}, -{from: 87, to: 440}, -{from: 87, to: 449}, -{from: 87, to: 472}, -{from: 87, to: 473}, -{from: 87, to: 490}, -{from: 87, to: 508}, -{from: 87, to: 521}, -{from: 87, to: 523}, -{from: 87, to: 543}, -{from: 87, to: 562}, -{from: 87, to: 565}, -{from: 87, to: 589}, -{from: 87, to: 594}, -{from: 87, to: 604}, -{from: 87, to: 622}, -{from: 87, to: 663}, -{from: 87, to: 675}, -{from: 87, to: 676}, -{from: 87, to: 728}, -{from: 88, to: 162}, -{from: 88, to: 177}, -{from: 88, to: 215}, -{from: 88, to: 218}, -{from: 88, to: 221}, -{from: 88, to: 241}, -{from: 88, to: 260}, -{from: 88, to: 261}, -{from: 88, to: 266}, -{from: 88, to: 271}, -{from: 88, to: 279}, -{from: 88, to: 339}, -{from: 88, to: 364}, -{from: 88, to: 366}, -{from: 88, to: 422}, -{from: 88, to: 453}, -{from: 88, to: 504}, -{from: 88, to: 572}, -{from: 88, to: 578}, -{from: 88, to: 591}, -{from: 88, to: 596}, -{from: 88, to: 602}, -{from: 88, to: 610}, -{from: 88, to: 661}, -{from: 88, to: 665}, -{from: 88, to: 690}, -{from: 88, to: 692}, -{from: 88, to: 693}, -{from: 88, to: 721}, -{from: 88, to: 723}, -{from: 89, to: 114}, -{from: 89, to: 127}, -{from: 89, to: 159}, -{from: 89, to: 160}, -{from: 89, to: 161}, -{from: 89, to: 246}, -{from: 89, to: 257}, -{from: 89, to: 297}, -{from: 89, to: 322}, -{from: 89, to: 398}, -{from: 89, to: 474}, -{from: 89, to: 485}, -{from: 89, to: 553}, -{from: 89, to: 621}, -{from: 89, to: 632}, -{from: 89, to: 638}, -{from: 89, to: 639}, -{from: 89, to: 657}, -{from: 89, to: 671}, -{from: 89, to: 715}, -{from: 89, to: 726}, -{from: 90, to: 91}, -{from: 90, to: 117}, -{from: 90, to: 126}, -{from: 90, to: 134}, -{from: 90, to: 156}, -{from: 90, to: 213}, -{from: 90, to: 242}, -{from: 90, to: 265}, -{from: 90, to: 326}, -{from: 90, to: 341}, -{from: 90, to: 365}, -{from: 90, to: 375}, -{from: 90, to: 406}, -{from: 90, to: 476}, -{from: 90, to: 502}, -{from: 90, to: 513}, -{from: 90, to: 530}, -{from: 90, to: 544}, -{from: 90, to: 549}, -{from: 90, to: 681}, -{from: 90, to: 683}, -{from: 91, to: 117}, -{from: 91, to: 126}, -{from: 91, to: 134}, -{from: 91, to: 156}, -{from: 91, to: 213}, -{from: 91, to: 242}, -{from: 91, to: 265}, -{from: 91, to: 326}, -{from: 91, to: 341}, -{from: 91, to: 365}, -{from: 91, to: 375}, -{from: 91, to: 406}, -{from: 91, to: 476}, -{from: 91, to: 502}, -{from: 91, to: 513}, -{from: 91, to: 530}, -{from: 91, to: 544}, -{from: 91, to: 613}, -{from: 91, to: 681}, -{from: 91, to: 683}, -{from: 92, to: 98}, -{from: 92, to: 176}, -{from: 92, to: 178}, -{from: 92, to: 197}, -{from: 92, to: 328}, -{from: 92, to: 329}, -{from: 92, to: 351}, -{from: 92, to: 367}, -{from: 92, to: 372}, -{from: 92, to: 426}, -{from: 92, to: 427}, -{from: 92, to: 456}, -{from: 92, to: 464}, -{from: 92, to: 492}, -{from: 92, to: 536}, -{from: 92, to: 549}, -{from: 92, to: 551}, -{from: 92, to: 609}, -{from: 92, to: 615}, -{from: 92, to: 700}, -{from: 92, to: 718}, -{from: 93, to: 131}, -{from: 93, to: 171}, -{from: 93, to: 180}, -{from: 93, to: 188}, -{from: 93, to: 200}, -{from: 93, to: 216}, -{from: 93, to: 277}, -{from: 93, to: 286}, -{from: 93, to: 332}, -{from: 93, to: 333}, -{from: 93, to: 428}, -{from: 93, to: 511}, -{from: 93, to: 528}, -{from: 93, to: 571}, -{from: 93, to: 579}, -{from: 93, to: 580}, -{from: 93, to: 593}, -{from: 93, to: 601}, -{from: 93, to: 618}, -{from: 93, to: 619}, -{from: 93, to: 652}, -{from: 93, to: 703}, -{from: 93, to: 716}, -{from: 94, to: 133}, -{from: 94, to: 149}, -{from: 94, to: 171}, -{from: 94, to: 244}, -{from: 94, to: 314}, -{from: 94, to: 325}, -{from: 94, to: 338}, -{from: 94, to: 345}, -{from: 94, to: 350}, -{from: 94, to: 396}, -{from: 94, to: 417}, -{from: 94, to: 496}, -{from: 94, to: 507}, -{from: 94, to: 534}, -{from: 94, to: 566}, -{from: 94, to: 606}, -{from: 94, to: 613}, -{from: 94, to: 659}, -{from: 94, to: 673}, -{from: 94, to: 682}, -{from: 94, to: 714}, -{from: 95, to: 109}, -{from: 95, to: 119}, -{from: 95, to: 128}, -{from: 95, to: 136}, -{from: 95, to: 152}, -{from: 95, to: 163}, -{from: 95, to: 206}, -{from: 95, to: 244}, -{from: 95, to: 248}, -{from: 95, to: 336}, -{from: 95, to: 343}, -{from: 95, to: 360}, -{from: 95, to: 378}, -{from: 95, to: 388}, -{from: 95, to: 448}, -{from: 95, to: 496}, -{from: 95, to: 501}, -{from: 95, to: 506}, -{from: 95, to: 550}, -{from: 95, to: 563}, -{from: 95, to: 588}, -{from: 95, to: 617}, -{from: 95, to: 630}, -{from: 95, to: 712}, -{from: 95, to: 727}, -{from: 96, to: 99}, -{from: 96, to: 100}, -{from: 96, to: 105}, -{from: 96, to: 106}, -{from: 96, to: 130}, -{from: 96, to: 153}, -{from: 96, to: 181}, -{from: 96, to: 186}, -{from: 96, to: 219}, -{from: 96, to: 234}, -{from: 96, to: 304}, -{from: 96, to: 309}, -{from: 96, to: 366}, -{from: 96, to: 369}, -{from: 96, to: 370}, -{from: 96, to: 457}, -{from: 96, to: 554}, -{from: 96, to: 630}, -{from: 96, to: 672}, -{from: 96, to: 701}, -{from: 97, to: 108}, -{from: 97, to: 173}, -{from: 97, to: 195}, -{from: 97, to: 205}, -{from: 97, to: 218}, -{from: 97, to: 274}, -{from: 97, to: 296}, -{from: 97, to: 418}, -{from: 97, to: 435}, -{from: 97, to: 437}, -{from: 97, to: 493}, -{from: 97, to: 494}, -{from: 97, to: 519}, -{from: 97, to: 525}, -{from: 97, to: 526}, -{from: 97, to: 582}, -{from: 97, to: 585}, -{from: 97, to: 605}, -{from: 97, to: 631}, -{from: 97, to: 655}, -{from: 97, to: 722}, -{from: 98, to: 176}, -{from: 98, to: 178}, -{from: 98, to: 197}, -{from: 98, to: 328}, -{from: 98, to: 329}, -{from: 98, to: 351}, -{from: 98, to: 367}, -{from: 98, to: 372}, -{from: 98, to: 426}, -{from: 98, to: 427}, -{from: 98, to: 456}, -{from: 98, to: 464}, -{from: 98, to: 492}, -{from: 98, to: 536}, -{from: 98, to: 549}, -{from: 98, to: 551}, -{from: 98, to: 609}, -{from: 98, to: 615}, -{from: 98, to: 700}, -{from: 98, to: 718}, -{from: 99, to: 100}, -{from: 99, to: 105}, -{from: 99, to: 106}, -{from: 99, to: 130}, -{from: 99, to: 153}, -{from: 99, to: 181}, -{from: 99, to: 219}, -{from: 99, to: 234}, -{from: 99, to: 300}, -{from: 99, to: 304}, -{from: 99, to: 309}, -{from: 99, to: 366}, -{from: 99, to: 369}, -{from: 99, to: 370}, -{from: 99, to: 457}, -{from: 99, to: 554}, -{from: 99, to: 630}, -{from: 99, to: 662}, -{from: 99, to: 672}, -{from: 99, to: 701}, -{from: 100, to: 105}, -{from: 100, to: 106}, -{from: 100, to: 130}, -{from: 100, to: 153}, -{from: 100, to: 181}, -{from: 100, to: 219}, -{from: 100, to: 234}, -{from: 100, to: 304}, -{from: 100, to: 309}, -{from: 100, to: 366}, -{from: 100, to: 369}, -{from: 100, to: 370}, -{from: 100, to: 457}, -{from: 100, to: 554}, -{from: 100, to: 630}, -{from: 100, to: 672}, -{from: 100, to: 701}, -{from: 101, to: 112}, -{from: 101, to: 124}, -{from: 101, to: 132}, -{from: 101, to: 189}, -{from: 101, to: 207}, -{from: 101, to: 209}, -{from: 101, to: 214}, -{from: 101, to: 223}, -{from: 101, to: 230}, -{from: 101, to: 239}, -{from: 101, to: 262}, -{from: 101, to: 320}, -{from: 101, to: 344}, -{from: 101, to: 354}, -{from: 101, to: 361}, -{from: 101, to: 362}, -{from: 101, to: 445}, -{from: 101, to: 457}, -{from: 101, to: 483}, -{from: 101, to: 484}, -{from: 101, to: 512}, -{from: 102, to: 120}, -{from: 102, to: 186}, -{from: 102, to: 201}, -{from: 102, to: 222}, -{from: 102, to: 228}, -{from: 102, to: 235}, -{from: 102, to: 236}, -{from: 102, to: 264}, -{from: 102, to: 305}, -{from: 102, to: 324}, -{from: 102, to: 334}, -{from: 102, to: 353}, -{from: 102, to: 368}, -{from: 102, to: 429}, -{from: 102, to: 489}, -{from: 102, to: 499}, -{from: 102, to: 548}, -{from: 102, to: 552}, -{from: 102, to: 595}, -{from: 102, to: 697}, -{from: 102, to: 710}, -{from: 103, to: 104}, -{from: 103, to: 169}, -{from: 103, to: 229}, -{from: 103, to: 256}, -{from: 103, to: 267}, -{from: 103, to: 275}, -{from: 103, to: 276}, -{from: 103, to: 295}, -{from: 103, to: 317}, -{from: 103, to: 318}, -{from: 103, to: 355}, -{from: 103, to: 357}, -{from: 103, to: 446}, -{from: 103, to: 509}, -{from: 103, to: 510}, -{from: 103, to: 546}, -{from: 103, to: 564}, -{from: 103, to: 581}, -{from: 103, to: 592}, -{from: 104, to: 169}, -{from: 104, to: 229}, -{from: 104, to: 256}, -{from: 104, to: 267}, -{from: 104, to: 275}, -{from: 104, to: 276}, -{from: 104, to: 295}, -{from: 104, to: 317}, -{from: 104, to: 318}, -{from: 104, to: 355}, -{from: 104, to: 357}, -{from: 104, to: 446}, -{from: 104, to: 509}, -{from: 104, to: 510}, -{from: 104, to: 546}, -{from: 104, to: 564}, -{from: 104, to: 581}, -{from: 104, to: 592}, -{from: 105, to: 106}, -{from: 105, to: 130}, -{from: 105, to: 153}, -{from: 105, to: 181}, -{from: 105, to: 219}, -{from: 105, to: 234}, -{from: 105, to: 304}, -{from: 105, to: 309}, -{from: 105, to: 366}, -{from: 105, to: 369}, -{from: 105, to: 370}, -{from: 105, to: 457}, -{from: 105, to: 554}, -{from: 105, to: 630}, -{from: 105, to: 672}, -{from: 105, to: 701}, -{from: 106, to: 130}, -{from: 106, to: 153}, -{from: 106, to: 181}, -{from: 106, to: 219}, -{from: 106, to: 234}, -{from: 106, to: 304}, -{from: 106, to: 309}, -{from: 106, to: 366}, -{from: 106, to: 369}, -{from: 106, to: 370}, -{from: 106, to: 457}, -{from: 106, to: 554}, -{from: 106, to: 630}, -{from: 106, to: 672}, -{from: 106, to: 701}, -{from: 107, to: 113}, -{from: 107, to: 146}, -{from: 107, to: 182}, -{from: 107, to: 210}, -{from: 107, to: 217}, -{from: 107, to: 278}, -{from: 107, to: 321}, -{from: 107, to: 337}, -{from: 107, to: 407}, -{from: 107, to: 420}, -{from: 107, to: 488}, -{from: 107, to: 533}, -{from: 107, to: 579}, -{from: 107, to: 626}, -{from: 107, to: 627}, -{from: 107, to: 662}, -{from: 107, to: 705}, -{from: 108, to: 173}, -{from: 108, to: 195}, -{from: 108, to: 205}, -{from: 108, to: 218}, -{from: 108, to: 274}, -{from: 108, to: 296}, -{from: 108, to: 418}, -{from: 108, to: 435}, -{from: 108, to: 493}, -{from: 108, to: 494}, -{from: 108, to: 519}, -{from: 108, to: 525}, -{from: 108, to: 526}, -{from: 108, to: 582}, -{from: 108, to: 585}, -{from: 108, to: 605}, -{from: 108, to: 631}, -{from: 108, to: 655}, -{from: 108, to: 722}, -{from: 109, to: 119}, -{from: 109, to: 128}, -{from: 109, to: 136}, -{from: 109, to: 152}, -{from: 109, to: 163}, -{from: 109, to: 206}, -{from: 109, to: 248}, -{from: 109, to: 336}, -{from: 109, to: 343}, -{from: 109, to: 360}, -{from: 109, to: 378}, -{from: 109, to: 388}, -{from: 109, to: 448}, -{from: 109, to: 501}, -{from: 109, to: 506}, -{from: 109, to: 550}, -{from: 109, to: 563}, -{from: 109, to: 588}, -{from: 109, to: 617}, -{from: 109, to: 712}, -{from: 109, to: 727}, -{from: 110, to: 111}, -{from: 110, to: 150}, -{from: 110, to: 151}, -{from: 110, to: 154}, -{from: 110, to: 155}, -{from: 110, to: 164}, -{from: 110, to: 179}, -{from: 110, to: 227}, -{from: 110, to: 231}, -{from: 110, to: 238}, -{from: 110, to: 245}, -{from: 110, to: 294}, -{from: 110, to: 316}, -{from: 110, to: 342}, -{from: 110, to: 349}, -{from: 110, to: 371}, -{from: 110, to: 373}, -{from: 110, to: 397}, -{from: 110, to: 547}, -{from: 110, to: 569}, -{from: 110, to: 572}, -{from: 110, to: 586}, -{from: 110, to: 591}, -{from: 110, to: 627}, -{from: 110, to: 629}, -{from: 110, to: 643}, -{from: 110, to: 644}, -{from: 110, to: 717}, -{from: 110, to: 719}, -{from: 110, to: 720}, -{from: 111, to: 135}, -{from: 111, to: 150}, -{from: 111, to: 154}, -{from: 111, to: 155}, -{from: 111, to: 164}, -{from: 111, to: 227}, -{from: 111, to: 245}, -{from: 111, to: 294}, -{from: 111, to: 316}, -{from: 111, to: 319}, -{from: 111, to: 349}, -{from: 111, to: 368}, -{from: 111, to: 371}, -{from: 111, to: 373}, -{from: 111, to: 397}, -{from: 111, to: 419}, -{from: 111, to: 429}, -{from: 111, to: 489}, -{from: 111, to: 529}, -{from: 111, to: 569}, -{from: 111, to: 572}, -{from: 111, to: 591}, -{from: 111, to: 629}, -{from: 111, to: 643}, -{from: 111, to: 644}, -{from: 111, to: 719}, -{from: 111, to: 720}, -{from: 112, to: 124}, -{from: 112, to: 132}, -{from: 112, to: 189}, -{from: 112, to: 207}, -{from: 112, to: 209}, -{from: 112, to: 214}, -{from: 112, to: 223}, -{from: 112, to: 230}, -{from: 112, to: 239}, -{from: 112, to: 262}, -{from: 112, to: 320}, -{from: 112, to: 344}, -{from: 112, to: 354}, -{from: 112, to: 361}, -{from: 112, to: 362}, -{from: 112, to: 445}, -{from: 112, to: 483}, -{from: 112, to: 484}, -{from: 112, to: 512}, -{from: 113, to: 146}, -{from: 113, to: 182}, -{from: 113, to: 210}, -{from: 113, to: 217}, -{from: 113, to: 278}, -{from: 113, to: 321}, -{from: 113, to: 337}, -{from: 113, to: 407}, -{from: 113, to: 420}, -{from: 113, to: 488}, -{from: 113, to: 533}, -{from: 113, to: 579}, -{from: 113, to: 626}, -{from: 113, to: 627}, -{from: 113, to: 662}, -{from: 113, to: 674}, -{from: 113, to: 705}, -{from: 114, to: 127}, -{from: 114, to: 159}, -{from: 114, to: 160}, -{from: 114, to: 161}, -{from: 114, to: 246}, -{from: 114, to: 257}, -{from: 114, to: 297}, -{from: 114, to: 322}, -{from: 114, to: 398}, -{from: 114, to: 421}, -{from: 114, to: 472}, -{from: 114, to: 474}, -{from: 114, to: 485}, -{from: 114, to: 523}, -{from: 114, to: 553}, -{from: 114, to: 621}, -{from: 114, to: 632}, -{from: 114, to: 638}, -{from: 114, to: 639}, -{from: 114, to: 649}, -{from: 114, to: 657}, -{from: 114, to: 671}, -{from: 114, to: 682}, -{from: 114, to: 715}, -{from: 114, to: 726}, -{from: 115, to: 190}, -{from: 115, to: 194}, -{from: 115, to: 200}, -{from: 115, to: 259}, -{from: 115, to: 342}, -{from: 115, to: 363}, -{from: 115, to: 379}, -{from: 115, to: 383}, -{from: 115, to: 403}, -{from: 115, to: 500}, -{from: 115, to: 505}, -{from: 115, to: 537}, -{from: 115, to: 574}, -{from: 115, to: 587}, -{from: 115, to: 597}, -{from: 115, to: 649}, -{from: 115, to: 691}, -{from: 115, to: 702}, -{from: 115, to: 706}, -{from: 116, to: 139}, -{from: 116, to: 144}, -{from: 116, to: 150}, -{from: 116, to: 231}, -{from: 116, to: 232}, -{from: 116, to: 238}, -{from: 116, to: 258}, -{from: 116, to: 303}, -{from: 116, to: 308}, -{from: 116, to: 318}, -{from: 116, to: 335}, -{from: 116, to: 348}, -{from: 116, to: 371}, -{from: 116, to: 415}, -{from: 116, to: 434}, -{from: 116, to: 460}, -{from: 116, to: 528}, -{from: 116, to: 562}, -{from: 116, to: 575}, -{from: 116, to: 576}, -{from: 116, to: 583}, -{from: 116, to: 603}, -{from: 116, to: 606}, -{from: 116, to: 616}, -{from: 116, to: 646}, -{from: 116, to: 668}, -{from: 116, to: 713}, -{from: 117, to: 126}, -{from: 117, to: 134}, -{from: 117, to: 156}, -{from: 117, to: 213}, -{from: 117, to: 242}, -{from: 117, to: 265}, -{from: 117, to: 326}, -{from: 117, to: 341}, -{from: 117, to: 365}, -{from: 117, to: 375}, -{from: 117, to: 406}, -{from: 117, to: 476}, -{from: 117, to: 502}, -{from: 117, to: 513}, -{from: 117, to: 530}, -{from: 117, to: 544}, -{from: 117, to: 681}, -{from: 117, to: 683}, -{from: 117, to: 700}, -{from: 118, to: 138}, -{from: 118, to: 199}, -{from: 118, to: 220}, -{from: 118, to: 272}, -{from: 118, to: 340}, -{from: 118, to: 346}, -{from: 118, to: 347}, -{from: 118, to: 387}, -{from: 118, to: 404}, -{from: 118, to: 437}, -{from: 118, to: 503}, -{from: 118, to: 520}, -{from: 118, to: 590}, -{from: 118, to: 595}, -{from: 118, to: 628}, -{from: 118, to: 664}, -{from: 118, to: 670}, -{from: 118, to: 709}, -{from: 119, to: 128}, -{from: 119, to: 136}, -{from: 119, to: 152}, -{from: 119, to: 163}, -{from: 119, to: 206}, -{from: 119, to: 248}, -{from: 119, to: 336}, -{from: 119, to: 343}, -{from: 119, to: 360}, -{from: 119, to: 378}, -{from: 119, to: 388}, -{from: 119, to: 448}, -{from: 119, to: 501}, -{from: 119, to: 506}, -{from: 119, to: 550}, -{from: 119, to: 563}, -{from: 119, to: 588}, -{from: 119, to: 617}, -{from: 119, to: 712}, -{from: 119, to: 727}, -{from: 120, to: 186}, -{from: 120, to: 201}, -{from: 120, to: 222}, -{from: 120, to: 228}, -{from: 120, to: 235}, -{from: 120, to: 236}, -{from: 120, to: 293}, -{from: 120, to: 305}, -{from: 120, to: 324}, -{from: 120, to: 334}, -{from: 120, to: 353}, -{from: 120, to: 368}, -{from: 120, to: 429}, -{from: 120, to: 489}, -{from: 120, to: 499}, -{from: 120, to: 548}, -{from: 120, to: 552}, -{from: 120, to: 595}, -{from: 120, to: 708}, -{from: 120, to: 710}, -{from: 120, to: 733}, -{from: 121, to: 129}, -{from: 121, to: 165}, -{from: 121, to: 166}, -{from: 121, to: 167}, -{from: 121, to: 168}, -{from: 121, to: 185}, -{from: 121, to: 191}, -{from: 121, to: 226}, -{from: 121, to: 240}, -{from: 121, to: 276}, -{from: 121, to: 352}, -{from: 121, to: 359}, -{from: 121, to: 430}, -{from: 121, to: 461}, -{from: 121, to: 463}, -{from: 121, to: 486}, -{from: 121, to: 531}, -{from: 121, to: 607}, -{from: 121, to: 634}, -{from: 121, to: 711}, -{from: 122, to: 203}, -{from: 122, to: 266}, -{from: 122, to: 327}, -{from: 122, to: 374}, -{from: 122, to: 385}, -{from: 122, to: 433}, -{from: 122, to: 442}, -{from: 122, to: 454}, -{from: 122, to: 475}, -{from: 122, to: 480}, -{from: 122, to: 498}, -{from: 122, to: 517}, -{from: 122, to: 573}, -{from: 122, to: 577}, -{from: 122, to: 611}, -{from: 122, to: 614}, -{from: 122, to: 623}, -{from: 122, to: 648}, -{from: 122, to: 678}, -{from: 122, to: 687}, -{from: 123, to: 125}, -{from: 123, to: 141}, -{from: 123, to: 237}, -{from: 123, to: 249}, -{from: 123, to: 252}, -{from: 123, to: 291}, -{from: 123, to: 416}, -{from: 123, to: 422}, -{from: 123, to: 447}, -{from: 123, to: 449}, -{from: 123, to: 452}, -{from: 123, to: 478}, -{from: 123, to: 481}, -{from: 123, to: 482}, -{from: 123, to: 558}, -{from: 123, to: 622}, -{from: 123, to: 675}, -{from: 124, to: 132}, -{from: 124, to: 189}, -{from: 124, to: 207}, -{from: 124, to: 209}, -{from: 124, to: 214}, -{from: 124, to: 223}, -{from: 124, to: 230}, -{from: 124, to: 239}, -{from: 124, to: 262}, -{from: 124, to: 271}, -{from: 124, to: 320}, -{from: 124, to: 344}, -{from: 124, to: 354}, -{from: 124, to: 361}, -{from: 124, to: 362}, -{from: 124, to: 445}, -{from: 124, to: 483}, -{from: 124, to: 484}, -{from: 124, to: 512}, -{from: 124, to: 615}, -{from: 125, to: 141}, -{from: 125, to: 237}, -{from: 125, to: 249}, -{from: 125, to: 252}, -{from: 125, to: 291}, -{from: 125, to: 404}, -{from: 125, to: 416}, -{from: 125, to: 422}, -{from: 125, to: 447}, -{from: 125, to: 449}, -{from: 125, to: 452}, -{from: 125, to: 461}, -{from: 125, to: 478}, -{from: 125, to: 481}, -{from: 125, to: 482}, -{from: 125, to: 483}, -{from: 125, to: 565}, -{from: 125, to: 622}, -{from: 125, to: 661}, -{from: 125, to: 675}, -{from: 126, to: 134}, -{from: 126, to: 156}, -{from: 126, to: 213}, -{from: 126, to: 242}, -{from: 126, to: 265}, -{from: 126, to: 326}, -{from: 126, to: 341}, -{from: 126, to: 365}, -{from: 126, to: 375}, -{from: 126, to: 406}, -{from: 126, to: 476}, -{from: 126, to: 502}, -{from: 126, to: 513}, -{from: 126, to: 530}, -{from: 126, to: 544}, -{from: 126, to: 681}, -{from: 126, to: 683}, -{from: 127, to: 159}, -{from: 127, to: 160}, -{from: 127, to: 161}, -{from: 127, to: 246}, -{from: 127, to: 257}, -{from: 127, to: 297}, -{from: 127, to: 322}, -{from: 127, to: 398}, -{from: 127, to: 474}, -{from: 127, to: 485}, -{from: 127, to: 553}, -{from: 127, to: 621}, -{from: 127, to: 632}, -{from: 127, to: 638}, -{from: 127, to: 639}, -{from: 127, to: 657}, -{from: 127, to: 671}, -{from: 127, to: 690}, -{from: 127, to: 715}, -{from: 127, to: 726}, -{from: 128, to: 136}, -{from: 128, to: 152}, -{from: 128, to: 163}, -{from: 128, to: 206}, -{from: 128, to: 248}, -{from: 128, to: 336}, -{from: 128, to: 343}, -{from: 128, to: 360}, -{from: 128, to: 378}, -{from: 128, to: 388}, -{from: 128, to: 448}, -{from: 128, to: 501}, -{from: 128, to: 506}, -{from: 128, to: 513}, -{from: 128, to: 550}, -{from: 128, to: 563}, -{from: 128, to: 588}, -{from: 128, to: 617}, -{from: 128, to: 712}, -{from: 128, to: 727}, -{from: 129, to: 155}, -{from: 129, to: 164}, -{from: 129, to: 165}, -{from: 129, to: 166}, -{from: 129, to: 167}, -{from: 129, to: 168}, -{from: 129, to: 185}, -{from: 129, to: 191}, -{from: 129, to: 226}, -{from: 129, to: 240}, -{from: 129, to: 352}, -{from: 129, to: 359}, -{from: 129, to: 373}, -{from: 129, to: 397}, -{from: 129, to: 430}, -{from: 129, to: 461}, -{from: 129, to: 463}, -{from: 129, to: 486}, -{from: 129, to: 531}, -{from: 129, to: 607}, -{from: 129, to: 634}, -{from: 129, to: 677}, -{from: 129, to: 685}, -{from: 129, to: 711}, -{from: 130, to: 153}, -{from: 130, to: 181}, -{from: 130, to: 219}, -{from: 130, to: 234}, -{from: 130, to: 291}, -{from: 130, to: 304}, -{from: 130, to: 309}, -{from: 130, to: 366}, -{from: 130, to: 369}, -{from: 130, to: 370}, -{from: 130, to: 382}, -{from: 130, to: 452}, -{from: 130, to: 457}, -{from: 130, to: 481}, -{from: 130, to: 503}, -{from: 130, to: 534}, -{from: 130, to: 554}, -{from: 130, to: 630}, -{from: 130, to: 670}, -{from: 130, to: 672}, -{from: 130, to: 701}, -{from: 131, to: 180}, -{from: 131, to: 188}, -{from: 131, to: 216}, -{from: 131, to: 277}, -{from: 131, to: 286}, -{from: 131, to: 294}, -{from: 131, to: 332}, -{from: 131, to: 333}, -{from: 131, to: 381}, -{from: 131, to: 428}, -{from: 131, to: 432}, -{from: 131, to: 443}, -{from: 131, to: 511}, -{from: 131, to: 528}, -{from: 131, to: 571}, -{from: 131, to: 580}, -{from: 131, to: 589}, -{from: 131, to: 593}, -{from: 131, to: 601}, -{from: 131, to: 618}, -{from: 131, to: 619}, -{from: 131, to: 623}, -{from: 131, to: 644}, -{from: 131, to: 652}, -{from: 131, to: 703}, -{from: 131, to: 716}, -{from: 131, to: 719}, -{from: 132, to: 189}, -{from: 132, to: 207}, -{from: 132, to: 209}, -{from: 132, to: 214}, -{from: 132, to: 223}, -{from: 132, to: 230}, -{from: 132, to: 239}, -{from: 132, to: 262}, -{from: 132, to: 320}, -{from: 132, to: 344}, -{from: 132, to: 354}, -{from: 132, to: 361}, -{from: 132, to: 362}, -{from: 132, to: 445}, -{from: 132, to: 483}, -{from: 132, to: 484}, -{from: 132, to: 512}, -{from: 133, to: 149}, -{from: 133, to: 171}, -{from: 133, to: 244}, -{from: 133, to: 314}, -{from: 133, to: 325}, -{from: 133, to: 338}, -{from: 133, to: 345}, -{from: 133, to: 350}, -{from: 133, to: 396}, -{from: 133, to: 417}, -{from: 133, to: 496}, -{from: 133, to: 507}, -{from: 133, to: 534}, -{from: 133, to: 566}, -{from: 133, to: 606}, -{from: 133, to: 613}, -{from: 133, to: 659}, -{from: 133, to: 673}, -{from: 133, to: 682}, -{from: 133, to: 714}, -{from: 134, to: 156}, -{from: 134, to: 213}, -{from: 134, to: 242}, -{from: 134, to: 265}, -{from: 134, to: 326}, -{from: 134, to: 341}, -{from: 134, to: 365}, -{from: 134, to: 375}, -{from: 134, to: 406}, -{from: 134, to: 476}, -{from: 134, to: 502}, -{from: 134, to: 513}, -{from: 134, to: 530}, -{from: 134, to: 544}, -{from: 134, to: 681}, -{from: 134, to: 683}, -{from: 135, to: 145}, -{from: 135, to: 151}, -{from: 135, to: 224}, -{from: 135, to: 233}, -{from: 135, to: 245}, -{from: 135, to: 279}, -{from: 135, to: 280}, -{from: 135, to: 289}, -{from: 135, to: 319}, -{from: 135, to: 323}, -{from: 135, to: 331}, -{from: 135, to: 349}, -{from: 135, to: 368}, -{from: 135, to: 376}, -{from: 135, to: 419}, -{from: 135, to: 429}, -{from: 135, to: 431}, -{from: 135, to: 436}, -{from: 135, to: 443}, -{from: 135, to: 489}, -{from: 135, to: 490}, -{from: 135, to: 529}, -{from: 135, to: 547}, -{from: 135, to: 567}, -{from: 135, to: 569}, -{from: 135, to: 586}, -{from: 135, to: 643}, -{from: 135, to: 676}, -{from: 135, to: 699}, -{from: 135, to: 717}, -{from: 135, to: 720}, -{from: 136, to: 152}, -{from: 136, to: 163}, -{from: 136, to: 206}, -{from: 136, to: 248}, -{from: 136, to: 336}, -{from: 136, to: 343}, -{from: 136, to: 360}, -{from: 136, to: 378}, -{from: 136, to: 388}, -{from: 136, to: 448}, -{from: 136, to: 501}, -{from: 136, to: 506}, -{from: 136, to: 550}, -{from: 136, to: 563}, -{from: 136, to: 588}, -{from: 136, to: 617}, -{from: 136, to: 712}, -{from: 136, to: 727}, -{from: 137, to: 192}, -{from: 137, to: 204}, -{from: 137, to: 247}, -{from: 137, to: 273}, -{from: 137, to: 284}, -{from: 137, to: 306}, -{from: 137, to: 315}, -{from: 137, to: 380}, -{from: 137, to: 389}, -{from: 137, to: 467}, -{from: 137, to: 495}, -{from: 137, to: 570}, -{from: 137, to: 584}, -{from: 137, to: 598}, -{from: 137, to: 599}, -{from: 137, to: 666}, -{from: 138, to: 193}, -{from: 138, to: 199}, -{from: 138, to: 220}, -{from: 138, to: 272}, -{from: 138, to: 340}, -{from: 138, to: 346}, -{from: 138, to: 347}, -{from: 138, to: 387}, -{from: 138, to: 404}, -{from: 138, to: 408}, -{from: 138, to: 437}, -{from: 138, to: 503}, -{from: 138, to: 520}, -{from: 138, to: 590}, -{from: 138, to: 628}, -{from: 138, to: 664}, -{from: 138, to: 670}, -{from: 138, to: 709}, -{from: 139, to: 144}, -{from: 139, to: 231}, -{from: 139, to: 232}, -{from: 139, to: 238}, -{from: 139, to: 258}, -{from: 139, to: 303}, -{from: 139, to: 308}, -{from: 139, to: 335}, -{from: 139, to: 348}, -{from: 139, to: 398}, -{from: 139, to: 415}, -{from: 139, to: 430}, -{from: 139, to: 434}, -{from: 139, to: 440}, -{from: 139, to: 575}, -{from: 139, to: 576}, -{from: 139, to: 583}, -{from: 139, to: 603}, -{from: 139, to: 616}, -{from: 139, to: 654}, -{from: 139, to: 668}, -{from: 139, to: 702}, -{from: 139, to: 713}, -{from: 140, to: 145}, -{from: 140, to: 172}, -{from: 140, to: 177}, -{from: 140, to: 179}, -{from: 140, to: 311}, -{from: 140, to: 316}, -{from: 140, to: 327}, -{from: 140, to: 375}, -{from: 140, to: 384}, -{from: 140, to: 386}, -{from: 140, to: 408}, -{from: 140, to: 442}, -{from: 140, to: 454}, -{from: 140, to: 455}, -{from: 140, to: 460}, -{from: 140, to: 522}, -{from: 140, to: 527}, -{from: 140, to: 532}, -{from: 140, to: 577}, -{from: 140, to: 612}, -{from: 140, to: 625}, -{from: 140, to: 654}, -{from: 140, to: 667}, -{from: 140, to: 677}, -{from: 140, to: 678}, -{from: 140, to: 679}, -{from: 140, to: 685}, -{from: 140, to: 687}, -{from: 140, to: 707}, -{from: 140, to: 721}, -{from: 141, to: 237}, -{from: 141, to: 246}, -{from: 141, to: 249}, -{from: 141, to: 252}, -{from: 141, to: 291}, -{from: 141, to: 416}, -{from: 141, to: 422}, -{from: 141, to: 436}, -{from: 141, to: 447}, -{from: 141, to: 449}, -{from: 141, to: 452}, -{from: 141, to: 478}, -{from: 141, to: 481}, -{from: 141, to: 482}, -{from: 141, to: 516}, -{from: 141, to: 622}, -{from: 141, to: 675}, -{from: 141, to: 696}, -{from: 142, to: 143}, -{from: 142, to: 147}, -{from: 142, to: 157}, -{from: 142, to: 175}, -{from: 142, to: 187}, -{from: 142, to: 263}, -{from: 142, to: 299}, -{from: 142, to: 300}, -{from: 142, to: 301}, -{from: 142, to: 432}, -{from: 142, to: 444}, -{from: 142, to: 455}, -{from: 142, to: 469}, -{from: 142, to: 514}, -{from: 142, to: 535}, -{from: 142, to: 539}, -{from: 142, to: 542}, -{from: 142, to: 624}, -{from: 142, to: 653}, -{from: 142, to: 660}, -{from: 142, to: 669}, -{from: 142, to: 698}, -{from: 143, to: 147}, -{from: 143, to: 157}, -{from: 143, to: 175}, -{from: 143, to: 187}, -{from: 143, to: 263}, -{from: 143, to: 299}, -{from: 143, to: 300}, -{from: 143, to: 301}, -{from: 143, to: 309}, -{from: 143, to: 332}, -{from: 143, to: 432}, -{from: 143, to: 444}, -{from: 143, to: 455}, -{from: 143, to: 469}, -{from: 143, to: 514}, -{from: 143, to: 535}, -{from: 143, to: 539}, -{from: 143, to: 542}, -{from: 143, to: 624}, -{from: 143, to: 645}, -{from: 143, to: 653}, -{from: 143, to: 669}, -{from: 143, to: 698}, -{from: 144, to: 150}, -{from: 144, to: 231}, -{from: 144, to: 232}, -{from: 144, to: 238}, -{from: 144, to: 258}, -{from: 144, to: 303}, -{from: 144, to: 308}, -{from: 144, to: 318}, -{from: 144, to: 335}, -{from: 144, to: 348}, -{from: 144, to: 371}, -{from: 144, to: 415}, -{from: 144, to: 434}, -{from: 144, to: 460}, -{from: 144, to: 528}, -{from: 144, to: 562}, -{from: 144, to: 575}, -{from: 144, to: 576}, -{from: 144, to: 583}, -{from: 144, to: 603}, -{from: 144, to: 606}, -{from: 144, to: 616}, -{from: 144, to: 646}, -{from: 144, to: 668}, -{from: 144, to: 713}, -{from: 145, to: 151}, -{from: 145, to: 224}, -{from: 145, to: 233}, -{from: 145, to: 279}, -{from: 145, to: 280}, -{from: 145, to: 289}, -{from: 145, to: 316}, -{from: 145, to: 323}, -{from: 145, to: 327}, -{from: 145, to: 331}, -{from: 145, to: 375}, -{from: 145, to: 376}, -{from: 145, to: 431}, -{from: 145, to: 436}, -{from: 145, to: 442}, -{from: 145, to: 443}, -{from: 145, to: 454}, -{from: 145, to: 455}, -{from: 145, to: 490}, -{from: 145, to: 529}, -{from: 145, to: 547}, -{from: 145, to: 567}, -{from: 145, to: 577}, -{from: 145, to: 586}, -{from: 145, to: 676}, -{from: 145, to: 678}, -{from: 145, to: 687}, -{from: 145, to: 699}, -{from: 145, to: 717}, -{from: 145, to: 721}, -{from: 146, to: 182}, -{from: 146, to: 210}, -{from: 146, to: 217}, -{from: 146, to: 278}, -{from: 146, to: 286}, -{from: 146, to: 321}, -{from: 146, to: 326}, -{from: 146, to: 337}, -{from: 146, to: 407}, -{from: 146, to: 420}, -{from: 146, to: 488}, -{from: 146, to: 533}, -{from: 146, to: 579}, -{from: 146, to: 626}, -{from: 146, to: 627}, -{from: 146, to: 662}, -{from: 146, to: 705}, -{from: 147, to: 157}, -{from: 147, to: 175}, -{from: 147, to: 187}, -{from: 147, to: 263}, -{from: 147, to: 299}, -{from: 147, to: 300}, -{from: 147, to: 301}, -{from: 147, to: 432}, -{from: 147, to: 444}, -{from: 147, to: 455}, -{from: 147, to: 469}, -{from: 147, to: 514}, -{from: 147, to: 535}, -{from: 147, to: 539}, -{from: 147, to: 542}, -{from: 147, to: 624}, -{from: 147, to: 653}, -{from: 147, to: 669}, -{from: 147, to: 698}, -{from: 148, to: 208}, -{from: 148, to: 298}, -{from: 148, to: 307}, -{from: 148, to: 310}, -{from: 148, to: 313}, -{from: 148, to: 458}, -{from: 148, to: 459}, -{from: 148, to: 468}, -{from: 148, to: 470}, -{from: 148, to: 471}, -{from: 148, to: 477}, -{from: 148, to: 479}, -{from: 148, to: 515}, -{from: 148, to: 518}, -{from: 148, to: 541}, -{from: 148, to: 620}, -{from: 148, to: 680}, -{from: 148, to: 686}, -{from: 149, to: 171}, -{from: 149, to: 244}, -{from: 149, to: 314}, -{from: 149, to: 325}, -{from: 149, to: 338}, -{from: 149, to: 345}, -{from: 149, to: 350}, -{from: 149, to: 396}, -{from: 149, to: 417}, -{from: 149, to: 496}, -{from: 149, to: 507}, -{from: 149, to: 534}, -{from: 149, to: 566}, -{from: 149, to: 606}, -{from: 149, to: 613}, -{from: 149, to: 659}, -{from: 149, to: 673}, -{from: 149, to: 682}, -{from: 149, to: 714}, -{from: 150, to: 154}, -{from: 150, to: 155}, -{from: 150, to: 164}, -{from: 150, to: 227}, -{from: 150, to: 245}, -{from: 150, to: 294}, -{from: 150, to: 316}, -{from: 150, to: 318}, -{from: 150, to: 349}, -{from: 150, to: 371}, -{from: 150, to: 373}, -{from: 150, to: 397}, -{from: 150, to: 460}, -{from: 150, to: 528}, -{from: 150, to: 562}, -{from: 150, to: 569}, -{from: 150, to: 572}, -{from: 150, to: 576}, -{from: 150, to: 591}, -{from: 150, to: 606}, -{from: 150, to: 629}, -{from: 150, to: 643}, -{from: 150, to: 644}, -{from: 150, to: 646}, -{from: 150, to: 713}, -{from: 150, to: 719}, -{from: 150, to: 720}, -{from: 151, to: 179}, -{from: 151, to: 224}, -{from: 151, to: 227}, -{from: 151, to: 231}, -{from: 151, to: 233}, -{from: 151, to: 238}, -{from: 151, to: 279}, -{from: 151, to: 280}, -{from: 151, to: 289}, -{from: 151, to: 323}, -{from: 151, to: 331}, -{from: 151, to: 342}, -{from: 151, to: 376}, -{from: 151, to: 431}, -{from: 151, to: 436}, -{from: 151, to: 443}, -{from: 151, to: 490}, -{from: 151, to: 529}, -{from: 151, to: 547}, -{from: 151, to: 567}, -{from: 151, to: 586}, -{from: 151, to: 627}, -{from: 151, to: 676}, -{from: 151, to: 699}, -{from: 151, to: 717}, -{from: 152, to: 163}, -{from: 152, to: 206}, -{from: 152, to: 248}, -{from: 152, to: 336}, -{from: 152, to: 343}, -{from: 152, to: 360}, -{from: 152, to: 378}, -{from: 152, to: 388}, -{from: 152, to: 448}, -{from: 152, to: 501}, -{from: 152, to: 506}, -{from: 152, to: 550}, -{from: 152, to: 563}, -{from: 152, to: 588}, -{from: 152, to: 617}, -{from: 152, to: 712}, -{from: 152, to: 727}, -{from: 153, to: 181}, -{from: 153, to: 219}, -{from: 153, to: 234}, -{from: 153, to: 304}, -{from: 153, to: 309}, -{from: 153, to: 366}, -{from: 153, to: 369}, -{from: 153, to: 370}, -{from: 153, to: 457}, -{from: 153, to: 554}, -{from: 153, to: 630}, -{from: 153, to: 672}, -{from: 153, to: 701}, -{from: 154, to: 155}, -{from: 154, to: 164}, -{from: 154, to: 183}, -{from: 154, to: 224}, -{from: 154, to: 227}, -{from: 154, to: 245}, -{from: 154, to: 294}, -{from: 154, to: 308}, -{from: 154, to: 316}, -{from: 154, to: 335}, -{from: 154, to: 349}, -{from: 154, to: 371}, -{from: 154, to: 373}, -{from: 154, to: 397}, -{from: 154, to: 462}, -{from: 154, to: 555}, -{from: 154, to: 569}, -{from: 154, to: 572}, -{from: 154, to: 591}, -{from: 154, to: 629}, -{from: 154, to: 642}, -{from: 154, to: 643}, -{from: 154, to: 644}, -{from: 154, to: 707}, -{from: 154, to: 719}, -{from: 154, to: 720}, -{from: 154, to: 726}, -{from: 155, to: 164}, -{from: 155, to: 166}, -{from: 155, to: 227}, -{from: 155, to: 245}, -{from: 155, to: 294}, -{from: 155, to: 316}, -{from: 155, to: 349}, -{from: 155, to: 359}, -{from: 155, to: 371}, -{from: 155, to: 373}, -{from: 155, to: 397}, -{from: 155, to: 569}, -{from: 155, to: 572}, -{from: 155, to: 591}, -{from: 155, to: 629}, -{from: 155, to: 643}, -{from: 155, to: 644}, -{from: 155, to: 677}, -{from: 155, to: 685}, -{from: 155, to: 719}, -{from: 155, to: 720}, -{from: 156, to: 213}, -{from: 156, to: 242}, -{from: 156, to: 265}, -{from: 156, to: 326}, -{from: 156, to: 341}, -{from: 156, to: 365}, -{from: 156, to: 375}, -{from: 156, to: 406}, -{from: 156, to: 476}, -{from: 156, to: 502}, -{from: 156, to: 513}, -{from: 156, to: 530}, -{from: 156, to: 544}, -{from: 156, to: 681}, -{from: 156, to: 683}, -{from: 157, to: 175}, -{from: 157, to: 187}, -{from: 157, to: 240}, -{from: 157, to: 263}, -{from: 157, to: 299}, -{from: 157, to: 300}, -{from: 157, to: 301}, -{from: 157, to: 432}, -{from: 157, to: 434}, -{from: 157, to: 444}, -{from: 157, to: 455}, -{from: 157, to: 469}, -{from: 157, to: 491}, -{from: 157, to: 514}, -{from: 157, to: 521}, -{from: 157, to: 535}, -{from: 157, to: 539}, -{from: 157, to: 542}, -{from: 157, to: 603}, -{from: 157, to: 624}, -{from: 157, to: 653}, -{from: 157, to: 669}, -{from: 157, to: 698}, -{from: 158, to: 174}, -{from: 158, to: 243}, -{from: 158, to: 292}, -{from: 158, to: 293}, -{from: 158, to: 439}, -{from: 158, to: 540}, -{from: 158, to: 568}, -{from: 158, to: 640}, -{from: 158, to: 641}, -{from: 158, to: 695}, -{from: 158, to: 704}, -{from: 158, to: 708}, -{from: 158, to: 732}, -{from: 158, to: 733}, -{from: 159, to: 160}, -{from: 159, to: 161}, -{from: 159, to: 226}, -{from: 159, to: 246}, -{from: 159, to: 257}, -{from: 159, to: 297}, -{from: 159, to: 302}, -{from: 159, to: 322}, -{from: 159, to: 398}, -{from: 159, to: 474}, -{from: 159, to: 485}, -{from: 159, to: 553}, -{from: 159, to: 621}, -{from: 159, to: 632}, -{from: 159, to: 638}, -{from: 159, to: 639}, -{from: 159, to: 657}, -{from: 159, to: 671}, -{from: 159, to: 714}, -{from: 159, to: 715}, -{from: 159, to: 726}, -{from: 160, to: 161}, -{from: 160, to: 246}, -{from: 160, to: 257}, -{from: 160, to: 281}, -{from: 160, to: 297}, -{from: 160, to: 322}, -{from: 160, to: 398}, -{from: 160, to: 474}, -{from: 160, to: 485}, -{from: 160, to: 553}, -{from: 160, to: 614}, -{from: 160, to: 621}, -{from: 160, to: 632}, -{from: 160, to: 638}, -{from: 160, to: 639}, -{from: 160, to: 657}, -{from: 160, to: 671}, -{from: 160, to: 715}, -{from: 160, to: 726}, -{from: 161, to: 246}, -{from: 161, to: 257}, -{from: 161, to: 297}, -{from: 161, to: 322}, -{from: 161, to: 398}, -{from: 161, to: 474}, -{from: 161, to: 485}, -{from: 161, to: 553}, -{from: 161, to: 621}, -{from: 161, to: 632}, -{from: 161, to: 638}, -{from: 161, to: 639}, -{from: 161, to: 657}, -{from: 161, to: 671}, -{from: 161, to: 715}, -{from: 161, to: 726}, -{from: 162, to: 215}, -{from: 162, to: 241}, -{from: 162, to: 260}, -{from: 162, to: 266}, -{from: 162, to: 271}, -{from: 162, to: 299}, -{from: 162, to: 301}, -{from: 162, to: 339}, -{from: 162, to: 364}, -{from: 162, to: 384}, -{from: 162, to: 431}, -{from: 162, to: 453}, -{from: 162, to: 504}, -{from: 162, to: 578}, -{from: 162, to: 596}, -{from: 162, to: 602}, -{from: 162, to: 610}, -{from: 162, to: 661}, -{from: 162, to: 665}, -{from: 162, to: 690}, -{from: 162, to: 692}, -{from: 162, to: 693}, -{from: 162, to: 703}, -{from: 162, to: 721}, -{from: 162, to: 723}, -{from: 163, to: 206}, -{from: 163, to: 248}, -{from: 163, to: 336}, -{from: 163, to: 343}, -{from: 163, to: 360}, -{from: 163, to: 378}, -{from: 163, to: 388}, -{from: 163, to: 448}, -{from: 163, to: 501}, -{from: 163, to: 506}, -{from: 163, to: 550}, -{from: 163, to: 563}, -{from: 163, to: 588}, -{from: 163, to: 617}, -{from: 163, to: 712}, -{from: 163, to: 727}, -{from: 164, to: 166}, -{from: 164, to: 227}, -{from: 164, to: 245}, -{from: 164, to: 294}, -{from: 164, to: 316}, -{from: 164, to: 349}, -{from: 164, to: 359}, -{from: 164, to: 371}, -{from: 164, to: 373}, -{from: 164, to: 397}, -{from: 164, to: 569}, -{from: 164, to: 572}, -{from: 164, to: 591}, -{from: 164, to: 629}, -{from: 164, to: 643}, -{from: 164, to: 644}, -{from: 164, to: 677}, -{from: 164, to: 685}, -{from: 164, to: 719}, -{from: 164, to: 720}, -{from: 165, to: 166}, -{from: 165, to: 167}, -{from: 165, to: 168}, -{from: 165, to: 185}, -{from: 165, to: 191}, -{from: 165, to: 226}, -{from: 165, to: 240}, -{from: 165, to: 285}, -{from: 165, to: 352}, -{from: 165, to: 359}, -{from: 165, to: 430}, -{from: 165, to: 461}, -{from: 165, to: 463}, -{from: 165, to: 486}, -{from: 165, to: 531}, -{from: 165, to: 607}, -{from: 165, to: 634}, -{from: 165, to: 711}, -{from: 165, to: 729}, -{from: 166, to: 167}, -{from: 166, to: 168}, -{from: 166, to: 185}, -{from: 166, to: 191}, -{from: 166, to: 226}, -{from: 166, to: 240}, -{from: 166, to: 352}, -{from: 166, to: 359}, -{from: 166, to: 373}, -{from: 166, to: 397}, -{from: 166, to: 430}, -{from: 166, to: 461}, -{from: 166, to: 463}, -{from: 166, to: 486}, -{from: 166, to: 531}, -{from: 166, to: 607}, -{from: 166, to: 634}, -{from: 166, to: 677}, -{from: 166, to: 685}, -{from: 166, to: 711}, -{from: 167, to: 168}, -{from: 167, to: 185}, -{from: 167, to: 191}, -{from: 167, to: 226}, -{from: 167, to: 240}, -{from: 167, to: 352}, -{from: 167, to: 359}, -{from: 167, to: 430}, -{from: 167, to: 461}, -{from: 167, to: 463}, -{from: 167, to: 486}, -{from: 167, to: 531}, -{from: 167, to: 607}, -{from: 167, to: 634}, -{from: 167, to: 711}, -{from: 168, to: 185}, -{from: 168, to: 191}, -{from: 168, to: 226}, -{from: 168, to: 240}, -{from: 168, to: 352}, -{from: 168, to: 359}, -{from: 168, to: 410}, -{from: 168, to: 430}, -{from: 168, to: 461}, -{from: 168, to: 463}, -{from: 168, to: 486}, -{from: 168, to: 531}, -{from: 168, to: 557}, -{from: 168, to: 607}, -{from: 168, to: 634}, -{from: 168, to: 711}, -{from: 169, to: 196}, -{from: 169, to: 229}, -{from: 169, to: 256}, -{from: 169, to: 267}, -{from: 169, to: 275}, -{from: 169, to: 276}, -{from: 169, to: 295}, -{from: 169, to: 304}, -{from: 169, to: 317}, -{from: 169, to: 318}, -{from: 169, to: 352}, -{from: 169, to: 355}, -{from: 169, to: 357}, -{from: 169, to: 369}, -{from: 169, to: 446}, -{from: 169, to: 509}, -{from: 169, to: 510}, -{from: 169, to: 526}, -{from: 169, to: 546}, -{from: 169, to: 564}, -{from: 169, to: 581}, -{from: 169, to: 592}, -{from: 169, to: 652}, -{from: 169, to: 667}, -{from: 170, to: 250}, -{from: 170, to: 251}, -{from: 170, to: 253}, -{from: 170, to: 254}, -{from: 170, to: 255}, -{from: 170, to: 356}, -{from: 170, to: 400}, -{from: 170, to: 401}, -{from: 170, to: 402}, -{from: 170, to: 410}, -{from: 170, to: 423}, -{from: 170, to: 545}, -{from: 170, to: 556}, -{from: 170, to: 557}, -{from: 170, to: 558}, -{from: 170, to: 656}, -{from: 170, to: 660}, -{from: 170, to: 674}, -{from: 170, to: 694}, -{from: 170, to: 696}, -{from: 171, to: 200}, -{from: 171, to: 244}, -{from: 171, to: 314}, -{from: 171, to: 325}, -{from: 171, to: 338}, -{from: 171, to: 345}, -{from: 171, to: 350}, -{from: 171, to: 396}, -{from: 171, to: 417}, -{from: 171, to: 496}, -{from: 171, to: 507}, -{from: 171, to: 534}, -{from: 171, to: 566}, -{from: 171, to: 579}, -{from: 171, to: 593}, -{from: 171, to: 606}, -{from: 171, to: 613}, -{from: 171, to: 659}, -{from: 171, to: 673}, -{from: 171, to: 682}, -{from: 171, to: 714}, -{from: 172, to: 177}, -{from: 172, to: 179}, -{from: 172, to: 311}, -{from: 172, to: 384}, -{from: 172, to: 386}, -{from: 172, to: 408}, -{from: 172, to: 460}, -{from: 172, to: 522}, -{from: 172, to: 527}, -{from: 172, to: 532}, -{from: 172, to: 604}, -{from: 172, to: 612}, -{from: 172, to: 621}, -{from: 172, to: 625}, -{from: 172, to: 654}, -{from: 172, to: 667}, -{from: 172, to: 677}, -{from: 172, to: 679}, -{from: 172, to: 685}, -{from: 172, to: 706}, -{from: 172, to: 707}, -{from: 173, to: 195}, -{from: 173, to: 205}, -{from: 173, to: 218}, -{from: 173, to: 274}, -{from: 173, to: 296}, -{from: 173, to: 418}, -{from: 173, to: 435}, -{from: 173, to: 493}, -{from: 173, to: 494}, -{from: 173, to: 519}, -{from: 173, to: 525}, -{from: 173, to: 526}, -{from: 173, to: 582}, -{from: 173, to: 585}, -{from: 173, to: 605}, -{from: 173, to: 631}, -{from: 173, to: 655}, -{from: 173, to: 722}, -{from: 174, to: 243}, -{from: 174, to: 292}, -{from: 174, to: 293}, -{from: 174, to: 439}, -{from: 174, to: 540}, -{from: 174, to: 568}, -{from: 174, to: 640}, -{from: 174, to: 641}, -{from: 174, to: 695}, -{from: 174, to: 704}, -{from: 174, to: 708}, -{from: 174, to: 732}, -{from: 174, to: 733}, -{from: 175, to: 187}, -{from: 175, to: 263}, -{from: 175, to: 299}, -{from: 175, to: 300}, -{from: 175, to: 301}, -{from: 175, to: 325}, -{from: 175, to: 432}, -{from: 175, to: 444}, -{from: 175, to: 455}, -{from: 175, to: 469}, -{from: 175, to: 511}, -{from: 175, to: 514}, -{from: 175, to: 535}, -{from: 175, to: 539}, -{from: 175, to: 542}, -{from: 175, to: 624}, -{from: 175, to: 653}, -{from: 175, to: 669}, -{from: 175, to: 698}, -{from: 176, to: 178}, -{from: 176, to: 197}, -{from: 176, to: 328}, -{from: 176, to: 329}, -{from: 176, to: 351}, -{from: 176, to: 367}, -{from: 176, to: 372}, -{from: 176, to: 426}, -{from: 176, to: 427}, -{from: 176, to: 456}, -{from: 176, to: 464}, -{from: 176, to: 492}, -{from: 176, to: 536}, -{from: 176, to: 549}, -{from: 176, to: 551}, -{from: 176, to: 609}, -{from: 176, to: 615}, -{from: 176, to: 700}, -{from: 176, to: 718}, -{from: 177, to: 179}, -{from: 177, to: 218}, -{from: 177, to: 221}, -{from: 177, to: 260}, -{from: 177, to: 261}, -{from: 177, to: 279}, -{from: 177, to: 311}, -{from: 177, to: 366}, -{from: 177, to: 384}, -{from: 177, to: 386}, -{from: 177, to: 408}, -{from: 177, to: 422}, -{from: 177, to: 460}, -{from: 177, to: 522}, -{from: 177, to: 527}, -{from: 177, to: 532}, -{from: 177, to: 572}, -{from: 177, to: 591}, -{from: 177, to: 612}, -{from: 177, to: 625}, -{from: 177, to: 654}, -{from: 177, to: 667}, -{from: 177, to: 677}, -{from: 177, to: 679}, -{from: 177, to: 685}, -{from: 177, to: 693}, -{from: 177, to: 707}, -{from: 178, to: 197}, -{from: 178, to: 328}, -{from: 178, to: 329}, -{from: 178, to: 351}, -{from: 178, to: 367}, -{from: 178, to: 372}, -{from: 178, to: 426}, -{from: 178, to: 427}, -{from: 178, to: 456}, -{from: 178, to: 464}, -{from: 178, to: 492}, -{from: 178, to: 536}, -{from: 178, to: 549}, -{from: 178, to: 551}, -{from: 178, to: 609}, -{from: 178, to: 615}, -{from: 178, to: 700}, -{from: 178, to: 718}, -{from: 179, to: 227}, -{from: 179, to: 231}, -{from: 179, to: 238}, -{from: 179, to: 311}, -{from: 179, to: 342}, -{from: 179, to: 384}, -{from: 179, to: 386}, -{from: 179, to: 408}, -{from: 179, to: 460}, -{from: 179, to: 522}, -{from: 179, to: 527}, -{from: 179, to: 532}, -{from: 179, to: 547}, -{from: 179, to: 586}, -{from: 179, to: 612}, -{from: 179, to: 625}, -{from: 179, to: 627}, -{from: 179, to: 654}, -{from: 179, to: 667}, -{from: 179, to: 677}, -{from: 179, to: 679}, -{from: 179, to: 685}, -{from: 179, to: 707}, -{from: 179, to: 717}, -{from: 180, to: 188}, -{from: 180, to: 216}, -{from: 180, to: 277}, -{from: 180, to: 286}, -{from: 180, to: 332}, -{from: 180, to: 333}, -{from: 180, to: 428}, -{from: 180, to: 511}, -{from: 180, to: 528}, -{from: 180, to: 571}, -{from: 180, to: 580}, -{from: 180, to: 593}, -{from: 180, to: 601}, -{from: 180, to: 618}, -{from: 180, to: 619}, -{from: 180, to: 652}, -{from: 180, to: 703}, -{from: 180, to: 716}, -{from: 181, to: 219}, -{from: 181, to: 234}, -{from: 181, to: 304}, -{from: 181, to: 309}, -{from: 181, to: 366}, -{from: 181, to: 369}, -{from: 181, to: 370}, -{from: 181, to: 457}, -{from: 181, to: 554}, -{from: 181, to: 630}, -{from: 181, to: 672}, -{from: 181, to: 701}, -{from: 182, to: 210}, -{from: 182, to: 217}, -{from: 182, to: 278}, -{from: 182, to: 321}, -{from: 182, to: 337}, -{from: 182, to: 407}, -{from: 182, to: 420}, -{from: 182, to: 488}, -{from: 182, to: 533}, -{from: 182, to: 579}, -{from: 182, to: 626}, -{from: 182, to: 627}, -{from: 182, to: 662}, -{from: 182, to: 705}, -{from: 183, to: 184}, -{from: 183, to: 198}, -{from: 183, to: 204}, -{from: 183, to: 224}, -{from: 183, to: 270}, -{from: 183, to: 302}, -{from: 183, to: 308}, -{from: 183, to: 312}, -{from: 183, to: 335}, -{from: 183, to: 462}, -{from: 183, to: 497}, -{from: 183, to: 516}, -{from: 183, to: 524}, -{from: 183, to: 538}, -{from: 183, to: 555}, -{from: 183, to: 633}, -{from: 183, to: 635}, -{from: 183, to: 636}, -{from: 183, to: 637}, -{from: 183, to: 642}, -{from: 183, to: 684}, -{from: 183, to: 688}, -{from: 183, to: 697}, -{from: 183, to: 707}, -{from: 183, to: 726}, -{from: 183, to: 736}, -{from: 184, to: 198}, -{from: 184, to: 204}, -{from: 184, to: 270}, -{from: 184, to: 302}, -{from: 184, to: 312}, -{from: 184, to: 497}, -{from: 184, to: 516}, -{from: 184, to: 524}, -{from: 184, to: 538}, -{from: 184, to: 633}, -{from: 184, to: 635}, -{from: 184, to: 636}, -{from: 184, to: 637}, -{from: 184, to: 684}, -{from: 184, to: 688}, -{from: 184, to: 697}, -{from: 184, to: 736}, -{from: 185, to: 191}, -{from: 185, to: 212}, -{from: 185, to: 226}, -{from: 185, to: 240}, -{from: 185, to: 352}, -{from: 185, to: 359}, -{from: 185, to: 425}, -{from: 185, to: 430}, -{from: 185, to: 449}, -{from: 185, to: 461}, -{from: 185, to: 463}, -{from: 185, to: 486}, -{from: 185, to: 490}, -{from: 185, to: 531}, -{from: 185, to: 607}, -{from: 185, to: 622}, -{from: 185, to: 634}, -{from: 185, to: 675}, -{from: 185, to: 676}, -{from: 185, to: 711}, -{from: 185, to: 728}, -{from: 186, to: 201}, -{from: 186, to: 222}, -{from: 186, to: 228}, -{from: 186, to: 235}, -{from: 186, to: 236}, -{from: 186, to: 305}, -{from: 186, to: 324}, -{from: 186, to: 334}, -{from: 186, to: 353}, -{from: 186, to: 368}, -{from: 186, to: 429}, -{from: 186, to: 489}, -{from: 186, to: 499}, -{from: 186, to: 548}, -{from: 186, to: 552}, -{from: 186, to: 595}, -{from: 186, to: 710}, -{from: 187, to: 263}, -{from: 187, to: 299}, -{from: 187, to: 300}, -{from: 187, to: 301}, -{from: 187, to: 432}, -{from: 187, to: 444}, -{from: 187, to: 455}, -{from: 187, to: 469}, -{from: 187, to: 514}, -{from: 187, to: 535}, -{from: 187, to: 539}, -{from: 187, to: 542}, -{from: 187, to: 624}, -{from: 187, to: 653}, -{from: 187, to: 669}, -{from: 187, to: 698}, -{from: 188, to: 216}, -{from: 188, to: 277}, -{from: 188, to: 286}, -{from: 188, to: 332}, -{from: 188, to: 333}, -{from: 188, to: 428}, -{from: 188, to: 511}, -{from: 188, to: 528}, -{from: 188, to: 571}, -{from: 188, to: 580}, -{from: 188, to: 593}, -{from: 188, to: 601}, -{from: 188, to: 618}, -{from: 188, to: 619}, -{from: 188, to: 652}, -{from: 188, to: 703}, -{from: 188, to: 716}, -{from: 189, to: 207}, -{from: 189, to: 209}, -{from: 189, to: 214}, -{from: 189, to: 223}, -{from: 189, to: 230}, -{from: 189, to: 239}, -{from: 189, to: 262}, -{from: 189, to: 320}, -{from: 189, to: 333}, -{from: 189, to: 344}, -{from: 189, to: 354}, -{from: 189, to: 361}, -{from: 189, to: 362}, -{from: 189, to: 445}, -{from: 189, to: 483}, -{from: 189, to: 484}, -{from: 189, to: 512}, -{from: 189, to: 578}, -{from: 189, to: 601}, -{from: 189, to: 655}, -{from: 190, to: 194}, -{from: 190, to: 197}, -{from: 190, to: 200}, -{from: 190, to: 232}, -{from: 190, to: 254}, -{from: 190, to: 259}, -{from: 190, to: 342}, -{from: 190, to: 363}, -{from: 190, to: 379}, -{from: 190, to: 383}, -{from: 190, to: 403}, -{from: 190, to: 500}, -{from: 190, to: 505}, -{from: 190, to: 537}, -{from: 190, to: 574}, -{from: 190, to: 587}, -{from: 190, to: 597}, -{from: 190, to: 649}, -{from: 190, to: 691}, -{from: 190, to: 702}, -{from: 190, to: 706}, -{from: 191, to: 226}, -{from: 191, to: 240}, -{from: 191, to: 324}, -{from: 191, to: 352}, -{from: 191, to: 359}, -{from: 191, to: 430}, -{from: 191, to: 461}, -{from: 191, to: 463}, -{from: 191, to: 486}, -{from: 191, to: 531}, -{from: 191, to: 607}, -{from: 191, to: 634}, -{from: 191, to: 711}, -{from: 192, to: 247}, -{from: 192, to: 273}, -{from: 192, to: 284}, -{from: 192, to: 306}, -{from: 192, to: 315}, -{from: 192, to: 380}, -{from: 192, to: 389}, -{from: 192, to: 467}, -{from: 192, to: 495}, -{from: 192, to: 570}, -{from: 192, to: 584}, -{from: 192, to: 598}, -{from: 192, to: 599}, -{from: 192, to: 666}, -{from: 193, to: 264}, -{from: 193, to: 281}, -{from: 193, to: 282}, -{from: 193, to: 285}, -{from: 193, to: 382}, -{from: 193, to: 408}, -{from: 193, to: 438}, -{from: 193, to: 441}, -{from: 193, to: 465}, -{from: 193, to: 466}, -{from: 193, to: 491}, -{from: 193, to: 646}, -{from: 193, to: 647}, -{from: 193, to: 650}, -{from: 193, to: 651}, -{from: 193, to: 689}, -{from: 193, to: 724}, -{from: 193, to: 725}, -{from: 193, to: 729}, -{from: 193, to: 730}, -{from: 193, to: 734}, -{from: 193, to: 735}, -{from: 194, to: 200}, -{from: 194, to: 259}, -{from: 194, to: 342}, -{from: 194, to: 363}, -{from: 194, to: 379}, -{from: 194, to: 383}, -{from: 194, to: 403}, -{from: 194, to: 500}, -{from: 194, to: 505}, -{from: 194, to: 537}, -{from: 194, to: 574}, -{from: 194, to: 587}, -{from: 194, to: 597}, -{from: 194, to: 649}, -{from: 194, to: 691}, -{from: 194, to: 702}, -{from: 194, to: 706}, -{from: 195, to: 205}, -{from: 195, to: 218}, -{from: 195, to: 274}, -{from: 195, to: 296}, -{from: 195, to: 418}, -{from: 195, to: 435}, -{from: 195, to: 444}, -{from: 195, to: 493}, -{from: 195, to: 494}, -{from: 195, to: 519}, -{from: 195, to: 525}, -{from: 195, to: 526}, -{from: 195, to: 582}, -{from: 195, to: 585}, -{from: 195, to: 605}, -{from: 195, to: 631}, -{from: 195, to: 655}, -{from: 195, to: 722}, -{from: 196, to: 275}, -{from: 196, to: 288}, -{from: 196, to: 304}, -{from: 196, to: 352}, -{from: 196, to: 369}, -{from: 196, to: 381}, -{from: 196, to: 409}, -{from: 196, to: 421}, -{from: 196, to: 425}, -{from: 196, to: 440}, -{from: 196, to: 472}, -{from: 196, to: 473}, -{from: 196, to: 508}, -{from: 196, to: 521}, -{from: 196, to: 523}, -{from: 196, to: 526}, -{from: 196, to: 543}, -{from: 196, to: 562}, -{from: 196, to: 565}, -{from: 196, to: 589}, -{from: 196, to: 594}, -{from: 196, to: 604}, -{from: 196, to: 652}, -{from: 196, to: 663}, -{from: 196, to: 667}, -{from: 196, to: 728}, -{from: 197, to: 232}, -{from: 197, to: 254}, -{from: 197, to: 328}, -{from: 197, to: 329}, -{from: 197, to: 351}, -{from: 197, to: 367}, -{from: 197, to: 372}, -{from: 197, to: 426}, -{from: 197, to: 427}, -{from: 197, to: 456}, -{from: 197, to: 464}, -{from: 197, to: 492}, -{from: 197, to: 536}, -{from: 197, to: 549}, -{from: 197, to: 551}, -{from: 197, to: 609}, -{from: 197, to: 615}, -{from: 197, to: 700}, -{from: 197, to: 718}, -{from: 198, to: 204}, -{from: 198, to: 270}, -{from: 198, to: 302}, -{from: 198, to: 312}, -{from: 198, to: 497}, -{from: 198, to: 516}, -{from: 198, to: 524}, -{from: 198, to: 538}, -{from: 198, to: 633}, -{from: 198, to: 635}, -{from: 198, to: 636}, -{from: 198, to: 637}, -{from: 198, to: 658}, -{from: 198, to: 684}, -{from: 198, to: 688}, -{from: 198, to: 697}, -{from: 198, to: 736}, -{from: 199, to: 220}, -{from: 199, to: 272}, -{from: 199, to: 274}, -{from: 199, to: 340}, -{from: 199, to: 346}, -{from: 199, to: 347}, -{from: 199, to: 387}, -{from: 199, to: 404}, -{from: 199, to: 437}, -{from: 199, to: 503}, -{from: 199, to: 520}, -{from: 199, to: 590}, -{from: 199, to: 628}, -{from: 199, to: 664}, -{from: 199, to: 670}, -{from: 199, to: 709}, -{from: 200, to: 259}, -{from: 200, to: 342}, -{from: 200, to: 363}, -{from: 200, to: 379}, -{from: 200, to: 383}, -{from: 200, to: 403}, -{from: 200, to: 500}, -{from: 200, to: 505}, -{from: 200, to: 537}, -{from: 200, to: 574}, -{from: 200, to: 579}, -{from: 200, to: 587}, -{from: 200, to: 593}, -{from: 200, to: 597}, -{from: 200, to: 649}, -{from: 200, to: 691}, -{from: 200, to: 702}, -{from: 200, to: 706}, -{from: 201, to: 222}, -{from: 201, to: 228}, -{from: 201, to: 235}, -{from: 201, to: 236}, -{from: 201, to: 305}, -{from: 201, to: 324}, -{from: 201, to: 334}, -{from: 201, to: 353}, -{from: 201, to: 368}, -{from: 201, to: 429}, -{from: 201, to: 489}, -{from: 201, to: 499}, -{from: 201, to: 548}, -{from: 201, to: 552}, -{from: 201, to: 595}, -{from: 201, to: 710}, -{from: 202, to: 211}, -{from: 202, to: 212}, -{from: 202, to: 221}, -{from: 202, to: 225}, -{from: 202, to: 261}, -{from: 202, to: 287}, -{from: 202, to: 319}, -{from: 202, to: 358}, -{from: 202, to: 419}, -{from: 202, to: 424}, -{from: 202, to: 450}, -{from: 202, to: 451}, -{from: 202, to: 462}, -{from: 202, to: 486}, -{from: 202, to: 487}, -{from: 202, to: 555}, -{from: 202, to: 600}, -{from: 202, to: 608}, -{from: 202, to: 618}, -{from: 202, to: 642}, -{from: 202, to: 645}, -{from: 203, to: 327}, -{from: 203, to: 374}, -{from: 203, to: 385}, -{from: 203, to: 433}, -{from: 203, to: 442}, -{from: 203, to: 454}, -{from: 203, to: 475}, -{from: 203, to: 480}, -{from: 203, to: 498}, -{from: 203, to: 517}, -{from: 203, to: 518}, -{from: 203, to: 573}, -{from: 203, to: 577}, -{from: 203, to: 611}, -{from: 203, to: 614}, -{from: 203, to: 623}, -{from: 203, to: 648}, -{from: 203, to: 656}, -{from: 203, to: 678}, -{from: 203, to: 687}, -{from: 204, to: 270}, -{from: 204, to: 302}, -{from: 204, to: 312}, -{from: 204, to: 497}, -{from: 204, to: 516}, -{from: 204, to: 524}, -{from: 204, to: 538}, -{from: 204, to: 633}, -{from: 204, to: 635}, -{from: 204, to: 636}, -{from: 204, to: 637}, -{from: 204, to: 684}, -{from: 204, to: 688}, -{from: 204, to: 697}, -{from: 204, to: 736}, -{from: 205, to: 218}, -{from: 205, to: 274}, -{from: 205, to: 296}, -{from: 205, to: 418}, -{from: 205, to: 435}, -{from: 205, to: 493}, -{from: 205, to: 494}, -{from: 205, to: 519}, -{from: 205, to: 525}, -{from: 205, to: 526}, -{from: 205, to: 559}, -{from: 205, to: 582}, -{from: 205, to: 585}, -{from: 205, to: 605}, -{from: 205, to: 631}, -{from: 205, to: 655}, -{from: 205, to: 722}, -{from: 206, to: 248}, -{from: 206, to: 336}, -{from: 206, to: 343}, -{from: 206, to: 360}, -{from: 206, to: 378}, -{from: 206, to: 388}, -{from: 206, to: 448}, -{from: 206, to: 501}, -{from: 206, to: 506}, -{from: 206, to: 550}, -{from: 206, to: 563}, -{from: 206, to: 588}, -{from: 206, to: 617}, -{from: 206, to: 712}, -{from: 206, to: 727}, -{from: 207, to: 209}, -{from: 207, to: 214}, -{from: 207, to: 223}, -{from: 207, to: 230}, -{from: 207, to: 239}, -{from: 207, to: 262}, -{from: 207, to: 320}, -{from: 207, to: 344}, -{from: 207, to: 354}, -{from: 207, to: 361}, -{from: 207, to: 362}, -{from: 207, to: 445}, -{from: 207, to: 483}, -{from: 207, to: 484}, -{from: 207, to: 512}, -{from: 208, to: 298}, -{from: 208, to: 307}, -{from: 208, to: 310}, -{from: 208, to: 313}, -{from: 208, to: 458}, -{from: 208, to: 459}, -{from: 208, to: 468}, -{from: 208, to: 470}, -{from: 208, to: 471}, -{from: 208, to: 477}, -{from: 208, to: 479}, -{from: 208, to: 515}, -{from: 208, to: 518}, -{from: 208, to: 541}, -{from: 208, to: 620}, -{from: 208, to: 680}, -{from: 208, to: 686}, -{from: 209, to: 214}, -{from: 209, to: 223}, -{from: 209, to: 230}, -{from: 209, to: 239}, -{from: 209, to: 262}, -{from: 209, to: 320}, -{from: 209, to: 344}, -{from: 209, to: 354}, -{from: 209, to: 361}, -{from: 209, to: 362}, -{from: 209, to: 445}, -{from: 209, to: 483}, -{from: 209, to: 484}, -{from: 209, to: 512}, -{from: 210, to: 217}, -{from: 210, to: 278}, -{from: 210, to: 321}, -{from: 210, to: 337}, -{from: 210, to: 407}, -{from: 210, to: 420}, -{from: 210, to: 488}, -{from: 210, to: 533}, -{from: 210, to: 579}, -{from: 210, to: 626}, -{from: 210, to: 627}, -{from: 210, to: 662}, -{from: 210, to: 705}, -{from: 211, to: 212}, -{from: 211, to: 221}, -{from: 211, to: 225}, -{from: 211, to: 261}, -{from: 211, to: 287}, -{from: 211, to: 319}, -{from: 211, to: 358}, -{from: 211, to: 419}, -{from: 211, to: 424}, -{from: 211, to: 450}, -{from: 211, to: 451}, -{from: 211, to: 462}, -{from: 211, to: 486}, -{from: 211, to: 487}, -{from: 211, to: 555}, -{from: 211, to: 600}, -{from: 211, to: 608}, -{from: 211, to: 618}, -{from: 211, to: 642}, -{from: 211, to: 645}, -{from: 212, to: 221}, -{from: 212, to: 225}, -{from: 212, to: 261}, -{from: 212, to: 287}, -{from: 212, to: 319}, -{from: 212, to: 358}, -{from: 212, to: 419}, -{from: 212, to: 424}, -{from: 212, to: 425}, -{from: 212, to: 449}, -{from: 212, to: 450}, -{from: 212, to: 451}, -{from: 212, to: 462}, -{from: 212, to: 487}, -{from: 212, to: 490}, -{from: 212, to: 555}, -{from: 212, to: 600}, -{from: 212, to: 608}, -{from: 212, to: 622}, -{from: 212, to: 642}, -{from: 212, to: 645}, -{from: 212, to: 675}, -{from: 212, to: 676}, -{from: 212, to: 728}, -{from: 213, to: 242}, -{from: 213, to: 265}, -{from: 213, to: 326}, -{from: 213, to: 341}, -{from: 213, to: 365}, -{from: 213, to: 375}, -{from: 213, to: 406}, -{from: 213, to: 476}, -{from: 213, to: 502}, -{from: 213, to: 513}, -{from: 213, to: 530}, -{from: 213, to: 544}, -{from: 213, to: 635}, -{from: 213, to: 681}, -{from: 213, to: 683}, -{from: 214, to: 223}, -{from: 214, to: 230}, -{from: 214, to: 239}, -{from: 214, to: 262}, -{from: 214, to: 320}, -{from: 214, to: 344}, -{from: 214, to: 354}, -{from: 214, to: 361}, -{from: 214, to: 362}, -{from: 214, to: 445}, -{from: 214, to: 483}, -{from: 214, to: 484}, -{from: 214, to: 512}, -{from: 215, to: 241}, -{from: 215, to: 257}, -{from: 215, to: 260}, -{from: 215, to: 266}, -{from: 215, to: 271}, -{from: 215, to: 339}, -{from: 215, to: 364}, -{from: 215, to: 445}, -{from: 215, to: 453}, -{from: 215, to: 504}, -{from: 215, to: 578}, -{from: 215, to: 596}, -{from: 215, to: 602}, -{from: 215, to: 610}, -{from: 215, to: 638}, -{from: 215, to: 661}, -{from: 215, to: 665}, -{from: 215, to: 690}, -{from: 215, to: 692}, -{from: 215, to: 693}, -{from: 215, to: 721}, -{from: 215, to: 723}, -{from: 216, to: 277}, -{from: 216, to: 286}, -{from: 216, to: 294}, -{from: 216, to: 332}, -{from: 216, to: 333}, -{from: 216, to: 381}, -{from: 216, to: 428}, -{from: 216, to: 432}, -{from: 216, to: 443}, -{from: 216, to: 511}, -{from: 216, to: 528}, -{from: 216, to: 571}, -{from: 216, to: 580}, -{from: 216, to: 589}, -{from: 216, to: 593}, -{from: 216, to: 601}, -{from: 216, to: 618}, -{from: 216, to: 619}, -{from: 216, to: 623}, -{from: 216, to: 644}, -{from: 216, to: 652}, -{from: 216, to: 703}, -{from: 216, to: 716}, -{from: 216, to: 719}, -{from: 217, to: 278}, -{from: 217, to: 321}, -{from: 217, to: 337}, -{from: 217, to: 407}, -{from: 217, to: 420}, -{from: 217, to: 488}, -{from: 217, to: 533}, -{from: 217, to: 579}, -{from: 217, to: 625}, -{from: 217, to: 626}, -{from: 217, to: 627}, -{from: 217, to: 662}, -{from: 217, to: 705}, -{from: 218, to: 221}, -{from: 218, to: 260}, -{from: 218, to: 261}, -{from: 218, to: 274}, -{from: 218, to: 279}, -{from: 218, to: 296}, -{from: 218, to: 366}, -{from: 218, to: 418}, -{from: 218, to: 422}, -{from: 218, to: 435}, -{from: 218, to: 493}, -{from: 218, to: 494}, -{from: 218, to: 519}, -{from: 218, to: 525}, -{from: 218, to: 526}, -{from: 218, to: 572}, -{from: 218, to: 582}, -{from: 218, to: 585}, -{from: 218, to: 591}, -{from: 218, to: 605}, -{from: 218, to: 631}, -{from: 218, to: 655}, -{from: 218, to: 693}, -{from: 218, to: 722}, -{from: 219, to: 234}, -{from: 219, to: 304}, -{from: 219, to: 309}, -{from: 219, to: 366}, -{from: 219, to: 369}, -{from: 219, to: 370}, -{from: 219, to: 457}, -{from: 219, to: 554}, -{from: 219, to: 630}, -{from: 219, to: 672}, -{from: 219, to: 701}, -{from: 220, to: 272}, -{from: 220, to: 340}, -{from: 220, to: 346}, -{from: 220, to: 347}, -{from: 220, to: 387}, -{from: 220, to: 404}, -{from: 220, to: 437}, -{from: 220, to: 503}, -{from: 220, to: 520}, -{from: 220, to: 590}, -{from: 220, to: 628}, -{from: 220, to: 664}, -{from: 220, to: 670}, -{from: 220, to: 709}, -{from: 221, to: 225}, -{from: 221, to: 260}, -{from: 221, to: 261}, -{from: 221, to: 279}, -{from: 221, to: 287}, -{from: 221, to: 319}, -{from: 221, to: 358}, -{from: 221, to: 366}, -{from: 221, to: 419}, -{from: 221, to: 422}, -{from: 221, to: 424}, -{from: 221, to: 450}, -{from: 221, to: 451}, -{from: 221, to: 462}, -{from: 221, to: 487}, -{from: 221, to: 555}, -{from: 221, to: 572}, -{from: 221, to: 591}, -{from: 221, to: 600}, -{from: 221, to: 608}, -{from: 221, to: 642}, -{from: 221, to: 645}, -{from: 221, to: 693}, -{from: 222, to: 228}, -{from: 222, to: 235}, -{from: 222, to: 236}, -{from: 222, to: 305}, -{from: 222, to: 324}, -{from: 222, to: 334}, -{from: 222, to: 353}, -{from: 222, to: 368}, -{from: 222, to: 429}, -{from: 222, to: 489}, -{from: 222, to: 499}, -{from: 222, to: 548}, -{from: 222, to: 552}, -{from: 222, to: 595}, -{from: 222, to: 710}, -{from: 223, to: 230}, -{from: 223, to: 239}, -{from: 223, to: 262}, -{from: 223, to: 320}, -{from: 223, to: 344}, -{from: 223, to: 354}, -{from: 223, to: 361}, -{from: 223, to: 362}, -{from: 223, to: 445}, -{from: 223, to: 483}, -{from: 223, to: 484}, -{from: 223, to: 512}, -{from: 224, to: 233}, -{from: 224, to: 279}, -{from: 224, to: 280}, -{from: 224, to: 289}, -{from: 224, to: 308}, -{from: 224, to: 323}, -{from: 224, to: 331}, -{from: 224, to: 335}, -{from: 224, to: 376}, -{from: 224, to: 431}, -{from: 224, to: 436}, -{from: 224, to: 443}, -{from: 224, to: 462}, -{from: 224, to: 490}, -{from: 224, to: 529}, -{from: 224, to: 547}, -{from: 224, to: 555}, -{from: 224, to: 567}, -{from: 224, to: 586}, -{from: 224, to: 642}, -{from: 224, to: 676}, -{from: 224, to: 699}, -{from: 224, to: 707}, -{from: 224, to: 717}, -{from: 224, to: 726}, -{from: 225, to: 261}, -{from: 225, to: 287}, -{from: 225, to: 319}, -{from: 225, to: 358}, -{from: 225, to: 419}, -{from: 225, to: 424}, -{from: 225, to: 450}, -{from: 225, to: 451}, -{from: 225, to: 462}, -{from: 225, to: 487}, -{from: 225, to: 555}, -{from: 225, to: 600}, -{from: 225, to: 608}, -{from: 225, to: 642}, -{from: 225, to: 645}, -{from: 226, to: 240}, -{from: 226, to: 302}, -{from: 226, to: 352}, -{from: 226, to: 359}, -{from: 226, to: 430}, -{from: 226, to: 461}, -{from: 226, to: 463}, -{from: 226, to: 486}, -{from: 226, to: 531}, -{from: 226, to: 607}, -{from: 226, to: 634}, -{from: 226, to: 711}, -{from: 226, to: 714}, -{from: 227, to: 231}, -{from: 227, to: 238}, -{from: 227, to: 245}, -{from: 227, to: 294}, -{from: 227, to: 316}, -{from: 227, to: 342}, -{from: 227, to: 349}, -{from: 227, to: 371}, -{from: 227, to: 373}, -{from: 227, to: 397}, -{from: 227, to: 547}, -{from: 227, to: 569}, -{from: 227, to: 572}, -{from: 227, to: 586}, -{from: 227, to: 591}, -{from: 227, to: 627}, -{from: 227, to: 629}, -{from: 227, to: 643}, -{from: 227, to: 644}, -{from: 227, to: 717}, -{from: 227, to: 719}, -{from: 227, to: 720}, -{from: 228, to: 235}, -{from: 228, to: 236}, -{from: 228, to: 305}, -{from: 228, to: 324}, -{from: 228, to: 334}, -{from: 228, to: 353}, -{from: 228, to: 368}, -{from: 228, to: 429}, -{from: 228, to: 489}, -{from: 228, to: 499}, -{from: 228, to: 548}, -{from: 228, to: 552}, -{from: 228, to: 595}, -{from: 228, to: 710}, -{from: 229, to: 256}, -{from: 229, to: 267}, -{from: 229, to: 275}, -{from: 229, to: 276}, -{from: 229, to: 295}, -{from: 229, to: 317}, -{from: 229, to: 318}, -{from: 229, to: 355}, -{from: 229, to: 357}, -{from: 229, to: 446}, -{from: 229, to: 509}, -{from: 229, to: 510}, -{from: 229, to: 546}, -{from: 229, to: 564}, -{from: 229, to: 581}, -{from: 229, to: 592}, -{from: 230, to: 239}, -{from: 230, to: 262}, -{from: 230, to: 320}, -{from: 230, to: 344}, -{from: 230, to: 354}, -{from: 230, to: 361}, -{from: 230, to: 362}, -{from: 230, to: 445}, -{from: 230, to: 483}, -{from: 230, to: 484}, -{from: 230, to: 512}, -{from: 231, to: 232}, -{from: 231, to: 238}, -{from: 231, to: 258}, -{from: 231, to: 303}, -{from: 231, to: 308}, -{from: 231, to: 335}, -{from: 231, to: 342}, -{from: 231, to: 348}, -{from: 231, to: 415}, -{from: 231, to: 434}, -{from: 231, to: 547}, -{from: 231, to: 575}, -{from: 231, to: 576}, -{from: 231, to: 583}, -{from: 231, to: 586}, -{from: 231, to: 603}, -{from: 231, to: 616}, -{from: 231, to: 627}, -{from: 231, to: 668}, -{from: 231, to: 713}, -{from: 231, to: 717}, -{from: 232, to: 238}, -{from: 232, to: 254}, -{from: 232, to: 258}, -{from: 232, to: 303}, -{from: 232, to: 308}, -{from: 232, to: 335}, -{from: 232, to: 348}, -{from: 232, to: 415}, -{from: 232, to: 434}, -{from: 232, to: 575}, -{from: 232, to: 576}, -{from: 232, to: 583}, -{from: 232, to: 603}, -{from: 232, to: 616}, -{from: 232, to: 668}, -{from: 232, to: 713}, -{from: 233, to: 279}, -{from: 233, to: 280}, -{from: 233, to: 289}, -{from: 233, to: 323}, -{from: 233, to: 331}, -{from: 233, to: 376}, -{from: 233, to: 431}, -{from: 233, to: 436}, -{from: 233, to: 443}, -{from: 233, to: 490}, -{from: 233, to: 529}, -{from: 233, to: 547}, -{from: 233, to: 567}, -{from: 233, to: 586}, -{from: 233, to: 676}, -{from: 233, to: 699}, -{from: 233, to: 717}, -{from: 234, to: 280}, -{from: 234, to: 287}, -{from: 234, to: 304}, -{from: 234, to: 309}, -{from: 234, to: 366}, -{from: 234, to: 369}, -{from: 234, to: 370}, -{from: 234, to: 457}, -{from: 234, to: 469}, -{from: 234, to: 554}, -{from: 234, to: 600}, -{from: 234, to: 608}, -{from: 234, to: 630}, -{from: 234, to: 631}, -{from: 234, to: 672}, -{from: 234, to: 701}, -{from: 234, to: 734}, -{from: 235, to: 236}, -{from: 235, to: 305}, -{from: 235, to: 324}, -{from: 235, to: 334}, -{from: 235, to: 353}, -{from: 235, to: 368}, -{from: 235, to: 429}, -{from: 235, to: 489}, -{from: 235, to: 499}, -{from: 235, to: 548}, -{from: 235, to: 552}, -{from: 235, to: 595}, -{from: 235, to: 710}, -{from: 236, to: 305}, -{from: 236, to: 324}, -{from: 236, to: 334}, -{from: 236, to: 353}, -{from: 236, to: 368}, -{from: 236, to: 429}, -{from: 236, to: 489}, -{from: 236, to: 499}, -{from: 236, to: 548}, -{from: 236, to: 552}, -{from: 236, to: 595}, -{from: 236, to: 710}, -{from: 237, to: 249}, -{from: 237, to: 252}, -{from: 237, to: 291}, -{from: 237, to: 416}, -{from: 237, to: 422}, -{from: 237, to: 447}, -{from: 237, to: 449}, -{from: 237, to: 452}, -{from: 237, to: 478}, -{from: 237, to: 481}, -{from: 237, to: 482}, -{from: 237, to: 622}, -{from: 237, to: 675}, -{from: 237, to: 711}, -{from: 238, to: 258}, -{from: 238, to: 303}, -{from: 238, to: 308}, -{from: 238, to: 335}, -{from: 238, to: 342}, -{from: 238, to: 348}, -{from: 238, to: 415}, -{from: 238, to: 434}, -{from: 238, to: 547}, -{from: 238, to: 575}, -{from: 238, to: 576}, -{from: 238, to: 583}, -{from: 238, to: 586}, -{from: 238, to: 603}, -{from: 238, to: 616}, -{from: 238, to: 627}, -{from: 238, to: 668}, -{from: 238, to: 713}, -{from: 238, to: 717}, -{from: 239, to: 262}, -{from: 239, to: 320}, -{from: 239, to: 344}, -{from: 239, to: 354}, -{from: 239, to: 361}, -{from: 239, to: 362}, -{from: 239, to: 391}, -{from: 239, to: 445}, -{from: 239, to: 483}, -{from: 239, to: 484}, -{from: 239, to: 512}, -{from: 240, to: 352}, -{from: 240, to: 359}, -{from: 240, to: 430}, -{from: 240, to: 434}, -{from: 240, to: 461}, -{from: 240, to: 463}, -{from: 240, to: 486}, -{from: 240, to: 491}, -{from: 240, to: 521}, -{from: 240, to: 531}, -{from: 240, to: 603}, -{from: 240, to: 607}, -{from: 240, to: 634}, -{from: 240, to: 711}, -{from: 241, to: 260}, -{from: 241, to: 266}, -{from: 241, to: 271}, -{from: 241, to: 339}, -{from: 241, to: 364}, -{from: 241, to: 453}, -{from: 241, to: 480}, -{from: 241, to: 497}, -{from: 241, to: 504}, -{from: 241, to: 578}, -{from: 241, to: 596}, -{from: 241, to: 602}, -{from: 241, to: 610}, -{from: 241, to: 661}, -{from: 241, to: 665}, -{from: 241, to: 690}, -{from: 241, to: 692}, -{from: 241, to: 693}, -{from: 241, to: 721}, -{from: 241, to: 723}, -{from: 242, to: 265}, -{from: 242, to: 326}, -{from: 242, to: 341}, -{from: 242, to: 365}, -{from: 242, to: 375}, -{from: 242, to: 406}, -{from: 242, to: 476}, -{from: 242, to: 502}, -{from: 242, to: 513}, -{from: 242, to: 530}, -{from: 242, to: 544}, -{from: 242, to: 574}, -{from: 242, to: 681}, -{from: 242, to: 683}, -{from: 242, to: 718}, -{from: 243, to: 292}, -{from: 243, to: 293}, -{from: 243, to: 439}, -{from: 243, to: 540}, -{from: 243, to: 568}, -{from: 243, to: 640}, -{from: 243, to: 641}, -{from: 243, to: 695}, -{from: 243, to: 704}, -{from: 243, to: 708}, -{from: 243, to: 732}, -{from: 243, to: 733}, -{from: 244, to: 314}, -{from: 244, to: 325}, -{from: 244, to: 338}, -{from: 244, to: 345}, -{from: 244, to: 350}, -{from: 244, to: 396}, -{from: 244, to: 417}, -{from: 244, to: 496}, -{from: 244, to: 507}, -{from: 244, to: 534}, -{from: 244, to: 566}, -{from: 244, to: 606}, -{from: 244, to: 613}, -{from: 244, to: 630}, -{from: 244, to: 659}, -{from: 244, to: 673}, -{from: 244, to: 682}, -{from: 244, to: 714}, -{from: 245, to: 294}, -{from: 245, to: 316}, -{from: 245, to: 319}, -{from: 245, to: 349}, -{from: 245, to: 368}, -{from: 245, to: 371}, -{from: 245, to: 373}, -{from: 245, to: 397}, -{from: 245, to: 419}, -{from: 245, to: 429}, -{from: 245, to: 489}, -{from: 245, to: 529}, -{from: 245, to: 569}, -{from: 245, to: 572}, -{from: 245, to: 591}, -{from: 245, to: 629}, -{from: 245, to: 643}, -{from: 245, to: 644}, -{from: 245, to: 719}, -{from: 245, to: 720}, -{from: 246, to: 257}, -{from: 246, to: 297}, -{from: 246, to: 322}, -{from: 246, to: 398}, -{from: 246, to: 436}, -{from: 246, to: 474}, -{from: 246, to: 485}, -{from: 246, to: 516}, -{from: 246, to: 553}, -{from: 246, to: 621}, -{from: 246, to: 632}, -{from: 246, to: 638}, -{from: 246, to: 639}, -{from: 246, to: 657}, -{from: 246, to: 671}, -{from: 246, to: 696}, -{from: 246, to: 715}, -{from: 246, to: 726}, -{from: 247, to: 273}, -{from: 247, to: 284}, -{from: 247, to: 306}, -{from: 247, to: 315}, -{from: 247, to: 380}, -{from: 247, to: 389}, -{from: 247, to: 467}, -{from: 247, to: 495}, -{from: 247, to: 570}, -{from: 247, to: 584}, -{from: 247, to: 598}, -{from: 247, to: 599}, -{from: 247, to: 666}, -{from: 248, to: 336}, -{from: 248, to: 343}, -{from: 248, to: 360}, -{from: 248, to: 378}, -{from: 248, to: 388}, -{from: 248, to: 448}, -{from: 248, to: 501}, -{from: 248, to: 506}, -{from: 248, to: 550}, -{from: 248, to: 563}, -{from: 248, to: 588}, -{from: 248, to: 617}, -{from: 248, to: 712}, -{from: 248, to: 727}, -{from: 249, to: 252}, -{from: 249, to: 291}, -{from: 249, to: 404}, -{from: 249, to: 416}, -{from: 249, to: 422}, -{from: 249, to: 447}, -{from: 249, to: 449}, -{from: 249, to: 452}, -{from: 249, to: 461}, -{from: 249, to: 478}, -{from: 249, to: 481}, -{from: 249, to: 482}, -{from: 249, to: 483}, -{from: 249, to: 565}, -{from: 249, to: 622}, -{from: 249, to: 661}, -{from: 249, to: 675}, -{from: 250, to: 251}, -{from: 250, to: 253}, -{from: 250, to: 254}, -{from: 250, to: 255}, -{from: 250, to: 356}, -{from: 250, to: 400}, -{from: 250, to: 401}, -{from: 250, to: 402}, -{from: 250, to: 410}, -{from: 250, to: 423}, -{from: 250, to: 482}, -{from: 250, to: 545}, -{from: 250, to: 556}, -{from: 250, to: 557}, -{from: 250, to: 558}, -{from: 250, to: 653}, -{from: 250, to: 656}, -{from: 250, to: 660}, -{from: 250, to: 674}, -{from: 250, to: 694}, -{from: 250, to: 696}, -{from: 251, to: 253}, -{from: 251, to: 254}, -{from: 251, to: 255}, -{from: 251, to: 336}, -{from: 251, to: 356}, -{from: 251, to: 400}, -{from: 251, to: 401}, -{from: 251, to: 402}, -{from: 251, to: 410}, -{from: 251, to: 423}, -{from: 251, to: 545}, -{from: 251, to: 556}, -{from: 251, to: 557}, -{from: 251, to: 558}, -{from: 251, to: 656}, -{from: 251, to: 660}, -{from: 251, to: 674}, -{from: 251, to: 694}, -{from: 251, to: 696}, -{from: 252, to: 291}, -{from: 252, to: 404}, -{from: 252, to: 416}, -{from: 252, to: 422}, -{from: 252, to: 447}, -{from: 252, to: 449}, -{from: 252, to: 452}, -{from: 252, to: 461}, -{from: 252, to: 478}, -{from: 252, to: 481}, -{from: 252, to: 482}, -{from: 252, to: 483}, -{from: 252, to: 565}, -{from: 252, to: 622}, -{from: 252, to: 661}, -{from: 252, to: 675}, -{from: 253, to: 254}, -{from: 253, to: 255}, -{from: 253, to: 356}, -{from: 253, to: 400}, -{from: 253, to: 401}, -{from: 253, to: 402}, -{from: 253, to: 410}, -{from: 253, to: 423}, -{from: 253, to: 545}, -{from: 253, to: 556}, -{from: 253, to: 557}, -{from: 253, to: 558}, -{from: 253, to: 656}, -{from: 253, to: 660}, -{from: 253, to: 674}, -{from: 253, to: 694}, -{from: 253, to: 696}, -{from: 254, to: 255}, -{from: 254, to: 356}, -{from: 254, to: 400}, -{from: 254, to: 401}, -{from: 254, to: 402}, -{from: 254, to: 410}, -{from: 254, to: 423}, -{from: 254, to: 545}, -{from: 254, to: 556}, -{from: 254, to: 557}, -{from: 254, to: 558}, -{from: 254, to: 656}, -{from: 254, to: 660}, -{from: 254, to: 674}, -{from: 254, to: 694}, -{from: 254, to: 696}, -{from: 255, to: 356}, -{from: 255, to: 400}, -{from: 255, to: 401}, -{from: 255, to: 402}, -{from: 255, to: 410}, -{from: 255, to: 423}, -{from: 255, to: 545}, -{from: 255, to: 556}, -{from: 255, to: 557}, -{from: 255, to: 558}, -{from: 255, to: 656}, -{from: 255, to: 660}, -{from: 255, to: 674}, -{from: 255, to: 694}, -{from: 255, to: 696}, -{from: 256, to: 267}, -{from: 256, to: 275}, -{from: 256, to: 276}, -{from: 256, to: 295}, -{from: 256, to: 317}, -{from: 256, to: 318}, -{from: 256, to: 355}, -{from: 256, to: 357}, -{from: 256, to: 446}, -{from: 256, to: 509}, -{from: 256, to: 510}, -{from: 256, to: 546}, -{from: 256, to: 564}, -{from: 256, to: 581}, -{from: 256, to: 592}, -{from: 257, to: 297}, -{from: 257, to: 322}, -{from: 257, to: 398}, -{from: 257, to: 445}, -{from: 257, to: 474}, -{from: 257, to: 485}, -{from: 257, to: 553}, -{from: 257, to: 621}, -{from: 257, to: 632}, -{from: 257, to: 638}, -{from: 257, to: 639}, -{from: 257, to: 657}, -{from: 257, to: 671}, -{from: 257, to: 692}, -{from: 257, to: 715}, -{from: 257, to: 723}, -{from: 257, to: 726}, -{from: 258, to: 303}, -{from: 258, to: 308}, -{from: 258, to: 335}, -{from: 258, to: 348}, -{from: 258, to: 398}, -{from: 258, to: 415}, -{from: 258, to: 430}, -{from: 258, to: 434}, -{from: 258, to: 440}, -{from: 258, to: 575}, -{from: 258, to: 576}, -{from: 258, to: 583}, -{from: 258, to: 603}, -{from: 258, to: 616}, -{from: 258, to: 654}, -{from: 258, to: 668}, -{from: 258, to: 702}, -{from: 258, to: 713}, -{from: 259, to: 342}, -{from: 259, to: 363}, -{from: 259, to: 379}, -{from: 259, to: 383}, -{from: 259, to: 403}, -{from: 259, to: 500}, -{from: 259, to: 505}, -{from: 259, to: 537}, -{from: 259, to: 553}, -{from: 259, to: 574}, -{from: 259, to: 587}, -{from: 259, to: 597}, -{from: 259, to: 649}, -{from: 259, to: 691}, -{from: 259, to: 702}, -{from: 259, to: 706}, -{from: 260, to: 261}, -{from: 260, to: 266}, -{from: 260, to: 271}, -{from: 260, to: 279}, -{from: 260, to: 339}, -{from: 260, to: 364}, -{from: 260, to: 366}, -{from: 260, to: 422}, -{from: 260, to: 453}, -{from: 260, to: 504}, -{from: 260, to: 572}, -{from: 260, to: 578}, -{from: 260, to: 591}, -{from: 260, to: 596}, -{from: 260, to: 602}, -{from: 260, to: 610}, -{from: 260, to: 661}, -{from: 260, to: 665}, -{from: 260, to: 690}, -{from: 260, to: 692}, -{from: 260, to: 693}, -{from: 260, to: 721}, -{from: 260, to: 723}, -{from: 261, to: 279}, -{from: 261, to: 287}, -{from: 261, to: 319}, -{from: 261, to: 358}, -{from: 261, to: 366}, -{from: 261, to: 419}, -{from: 261, to: 422}, -{from: 261, to: 424}, -{from: 261, to: 450}, -{from: 261, to: 451}, -{from: 261, to: 462}, -{from: 261, to: 487}, -{from: 261, to: 555}, -{from: 261, to: 572}, -{from: 261, to: 591}, -{from: 261, to: 600}, -{from: 261, to: 608}, -{from: 261, to: 642}, -{from: 261, to: 645}, -{from: 261, to: 693}, -{from: 262, to: 320}, -{from: 262, to: 344}, -{from: 262, to: 354}, -{from: 262, to: 361}, -{from: 262, to: 362}, -{from: 262, to: 445}, -{from: 262, to: 483}, -{from: 262, to: 484}, -{from: 262, to: 512}, -{from: 263, to: 299}, -{from: 263, to: 300}, -{from: 263, to: 301}, -{from: 263, to: 432}, -{from: 263, to: 444}, -{from: 263, to: 455}, -{from: 263, to: 469}, -{from: 263, to: 514}, -{from: 263, to: 535}, -{from: 263, to: 539}, -{from: 263, to: 542}, -{from: 263, to: 624}, -{from: 263, to: 653}, -{from: 263, to: 660}, -{from: 263, to: 669}, -{from: 263, to: 698}, -{from: 264, to: 281}, -{from: 264, to: 282}, -{from: 264, to: 285}, -{from: 264, to: 382}, -{from: 264, to: 438}, -{from: 264, to: 441}, -{from: 264, to: 465}, -{from: 264, to: 466}, -{from: 264, to: 491}, -{from: 264, to: 646}, -{from: 264, to: 647}, -{from: 264, to: 650}, -{from: 264, to: 651}, -{from: 264, to: 689}, -{from: 264, to: 697}, -{from: 264, to: 724}, -{from: 264, to: 725}, -{from: 264, to: 729}, -{from: 264, to: 730}, -{from: 264, to: 734}, -{from: 264, to: 735}, -{from: 265, to: 326}, -{from: 265, to: 341}, -{from: 265, to: 365}, -{from: 265, to: 375}, -{from: 265, to: 406}, -{from: 265, to: 476}, -{from: 265, to: 502}, -{from: 265, to: 513}, -{from: 265, to: 530}, -{from: 265, to: 544}, -{from: 265, to: 681}, -{from: 265, to: 683}, -{from: 266, to: 271}, -{from: 266, to: 339}, -{from: 266, to: 364}, -{from: 266, to: 453}, -{from: 266, to: 504}, -{from: 266, to: 578}, -{from: 266, to: 596}, -{from: 266, to: 602}, -{from: 266, to: 610}, -{from: 266, to: 661}, -{from: 266, to: 665}, -{from: 266, to: 690}, -{from: 266, to: 692}, -{from: 266, to: 693}, -{from: 266, to: 721}, -{from: 266, to: 723}, -{from: 267, to: 275}, -{from: 267, to: 276}, -{from: 267, to: 295}, -{from: 267, to: 317}, -{from: 267, to: 318}, -{from: 267, to: 355}, -{from: 267, to: 357}, -{from: 267, to: 446}, -{from: 267, to: 494}, -{from: 267, to: 509}, -{from: 267, to: 510}, -{from: 267, to: 546}, -{from: 267, to: 564}, -{from: 267, to: 581}, -{from: 267, to: 592}, -{from: 268, to: 269}, -{from: 268, to: 283}, -{from: 268, to: 290}, -{from: 268, to: 330}, -{from: 268, to: 377}, -{from: 268, to: 390}, -{from: 268, to: 391}, -{from: 268, to: 392}, -{from: 268, to: 393}, -{from: 268, to: 394}, -{from: 268, to: 395}, -{from: 268, to: 399}, -{from: 268, to: 405}, -{from: 268, to: 411}, -{from: 268, to: 412}, -{from: 268, to: 413}, -{from: 268, to: 414}, -{from: 268, to: 559}, -{from: 268, to: 560}, -{from: 268, to: 561}, -{from: 268, to: 658}, -{from: 268, to: 731}, -{from: 269, to: 283}, -{from: 269, to: 290}, -{from: 269, to: 330}, -{from: 269, to: 377}, -{from: 269, to: 390}, -{from: 269, to: 391}, -{from: 269, to: 392}, -{from: 269, to: 393}, -{from: 269, to: 394}, -{from: 269, to: 395}, -{from: 269, to: 399}, -{from: 269, to: 405}, -{from: 269, to: 411}, -{from: 269, to: 412}, -{from: 269, to: 413}, -{from: 269, to: 414}, -{from: 269, to: 559}, -{from: 269, to: 560}, -{from: 269, to: 561}, -{from: 269, to: 658}, -{from: 269, to: 731}, -{from: 270, to: 302}, -{from: 270, to: 312}, -{from: 270, to: 497}, -{from: 270, to: 516}, -{from: 270, to: 524}, -{from: 270, to: 538}, -{from: 270, to: 633}, -{from: 270, to: 635}, -{from: 270, to: 636}, -{from: 270, to: 637}, -{from: 270, to: 684}, -{from: 270, to: 688}, -{from: 270, to: 697}, -{from: 270, to: 736}, -{from: 271, to: 339}, -{from: 271, to: 364}, -{from: 271, to: 453}, -{from: 271, to: 504}, -{from: 271, to: 578}, -{from: 271, to: 596}, -{from: 271, to: 602}, -{from: 271, to: 610}, -{from: 271, to: 615}, -{from: 271, to: 661}, -{from: 271, to: 665}, -{from: 271, to: 690}, -{from: 271, to: 692}, -{from: 271, to: 693}, -{from: 271, to: 721}, -{from: 271, to: 723}, -{from: 272, to: 340}, -{from: 272, to: 346}, -{from: 272, to: 347}, -{from: 272, to: 387}, -{from: 272, to: 404}, -{from: 272, to: 437}, -{from: 272, to: 503}, -{from: 272, to: 520}, -{from: 272, to: 590}, -{from: 272, to: 628}, -{from: 272, to: 664}, -{from: 272, to: 670}, -{from: 272, to: 709}, -{from: 273, to: 284}, -{from: 273, to: 306}, -{from: 273, to: 315}, -{from: 273, to: 380}, -{from: 273, to: 389}, -{from: 273, to: 467}, -{from: 273, to: 495}, -{from: 273, to: 570}, -{from: 273, to: 584}, -{from: 273, to: 598}, -{from: 273, to: 599}, -{from: 273, to: 666}, -{from: 274, to: 296}, -{from: 274, to: 418}, -{from: 274, to: 435}, -{from: 274, to: 493}, -{from: 274, to: 494}, -{from: 274, to: 519}, -{from: 274, to: 525}, -{from: 274, to: 526}, -{from: 274, to: 582}, -{from: 274, to: 585}, -{from: 274, to: 605}, -{from: 274, to: 631}, -{from: 274, to: 655}, -{from: 274, to: 722}, -{from: 275, to: 276}, -{from: 275, to: 295}, -{from: 275, to: 304}, -{from: 275, to: 317}, -{from: 275, to: 318}, -{from: 275, to: 352}, -{from: 275, to: 355}, -{from: 275, to: 357}, -{from: 275, to: 369}, -{from: 275, to: 446}, -{from: 275, to: 509}, -{from: 275, to: 510}, -{from: 275, to: 526}, -{from: 275, to: 546}, -{from: 275, to: 564}, -{from: 275, to: 581}, -{from: 275, to: 592}, -{from: 275, to: 652}, -{from: 275, to: 667}, -{from: 276, to: 295}, -{from: 276, to: 317}, -{from: 276, to: 318}, -{from: 276, to: 355}, -{from: 276, to: 357}, -{from: 276, to: 446}, -{from: 276, to: 509}, -{from: 276, to: 510}, -{from: 276, to: 546}, -{from: 276, to: 564}, -{from: 276, to: 581}, -{from: 276, to: 592}, -{from: 277, to: 286}, -{from: 277, to: 332}, -{from: 277, to: 333}, -{from: 277, to: 424}, -{from: 277, to: 428}, -{from: 277, to: 511}, -{from: 277, to: 517}, -{from: 277, to: 528}, -{from: 277, to: 537}, -{from: 277, to: 571}, -{from: 277, to: 580}, -{from: 277, to: 593}, -{from: 277, to: 601}, -{from: 277, to: 618}, -{from: 277, to: 619}, -{from: 277, to: 636}, -{from: 277, to: 652}, -{from: 277, to: 703}, -{from: 277, to: 716}, -{from: 278, to: 321}, -{from: 278, to: 337}, -{from: 278, to: 407}, -{from: 278, to: 420}, -{from: 278, to: 488}, -{from: 278, to: 533}, -{from: 278, to: 579}, -{from: 278, to: 626}, -{from: 278, to: 627}, -{from: 278, to: 662}, -{from: 278, to: 705}, -{from: 279, to: 280}, -{from: 279, to: 289}, -{from: 279, to: 323}, -{from: 279, to: 331}, -{from: 279, to: 366}, -{from: 279, to: 376}, -{from: 279, to: 422}, -{from: 279, to: 431}, -{from: 279, to: 436}, -{from: 279, to: 443}, -{from: 279, to: 490}, -{from: 279, to: 529}, -{from: 279, to: 547}, -{from: 279, to: 567}, -{from: 279, to: 572}, -{from: 279, to: 586}, -{from: 279, to: 591}, -{from: 279, to: 676}, -{from: 279, to: 693}, -{from: 279, to: 699}, -{from: 279, to: 717}, -{from: 280, to: 287}, -{from: 280, to: 289}, -{from: 280, to: 323}, -{from: 280, to: 331}, -{from: 280, to: 376}, -{from: 280, to: 431}, -{from: 280, to: 436}, -{from: 280, to: 443}, -{from: 280, to: 469}, -{from: 280, to: 490}, -{from: 280, to: 529}, -{from: 280, to: 547}, -{from: 280, to: 567}, -{from: 280, to: 586}, -{from: 280, to: 600}, -{from: 280, to: 608}, -{from: 280, to: 631}, -{from: 280, to: 676}, -{from: 280, to: 699}, -{from: 280, to: 717}, -{from: 280, to: 734}, -{from: 281, to: 282}, -{from: 281, to: 285}, -{from: 281, to: 382}, -{from: 281, to: 438}, -{from: 281, to: 441}, -{from: 281, to: 465}, -{from: 281, to: 466}, -{from: 281, to: 491}, -{from: 281, to: 614}, -{from: 281, to: 646}, -{from: 281, to: 647}, -{from: 281, to: 650}, -{from: 281, to: 651}, -{from: 281, to: 689}, -{from: 281, to: 724}, -{from: 281, to: 725}, -{from: 281, to: 729}, -{from: 281, to: 730}, -{from: 281, to: 734}, -{from: 281, to: 735}, -{from: 282, to: 285}, -{from: 282, to: 364}, -{from: 282, to: 382}, -{from: 282, to: 438}, -{from: 282, to: 441}, -{from: 282, to: 465}, -{from: 282, to: 466}, -{from: 282, to: 491}, -{from: 282, to: 646}, -{from: 282, to: 647}, -{from: 282, to: 650}, -{from: 282, to: 651}, -{from: 282, to: 683}, -{from: 282, to: 689}, -{from: 282, to: 724}, -{from: 282, to: 725}, -{from: 282, to: 729}, -{from: 282, to: 730}, -{from: 282, to: 734}, -{from: 282, to: 735}, -{from: 283, to: 290}, -{from: 283, to: 330}, -{from: 283, to: 377}, -{from: 283, to: 390}, -{from: 283, to: 391}, -{from: 283, to: 392}, -{from: 283, to: 393}, -{from: 283, to: 394}, -{from: 283, to: 395}, -{from: 283, to: 399}, -{from: 283, to: 405}, -{from: 283, to: 411}, -{from: 283, to: 412}, -{from: 283, to: 413}, -{from: 283, to: 414}, -{from: 283, to: 559}, -{from: 283, to: 560}, -{from: 283, to: 561}, -{from: 283, to: 566}, -{from: 283, to: 658}, -{from: 283, to: 731}, -{from: 284, to: 306}, -{from: 284, to: 315}, -{from: 284, to: 380}, -{from: 284, to: 389}, -{from: 284, to: 467}, -{from: 284, to: 495}, -{from: 284, to: 570}, -{from: 284, to: 584}, -{from: 284, to: 598}, -{from: 284, to: 599}, -{from: 284, to: 666}, -{from: 285, to: 382}, -{from: 285, to: 438}, -{from: 285, to: 441}, -{from: 285, to: 465}, -{from: 285, to: 466}, -{from: 285, to: 491}, -{from: 285, to: 646}, -{from: 285, to: 647}, -{from: 285, to: 650}, -{from: 285, to: 651}, -{from: 285, to: 689}, -{from: 285, to: 724}, -{from: 285, to: 725}, -{from: 285, to: 729}, -{from: 285, to: 730}, -{from: 285, to: 734}, -{from: 285, to: 735}, -{from: 286, to: 326}, -{from: 286, to: 332}, -{from: 286, to: 333}, -{from: 286, to: 428}, -{from: 286, to: 511}, -{from: 286, to: 528}, -{from: 286, to: 571}, -{from: 286, to: 580}, -{from: 286, to: 593}, -{from: 286, to: 601}, -{from: 286, to: 618}, -{from: 286, to: 619}, -{from: 286, to: 652}, -{from: 286, to: 703}, -{from: 286, to: 716}, -{from: 287, to: 319}, -{from: 287, to: 358}, -{from: 287, to: 419}, -{from: 287, to: 424}, -{from: 287, to: 450}, -{from: 287, to: 451}, -{from: 287, to: 462}, -{from: 287, to: 469}, -{from: 287, to: 487}, -{from: 287, to: 555}, -{from: 287, to: 600}, -{from: 287, to: 608}, -{from: 287, to: 631}, -{from: 287, to: 642}, -{from: 287, to: 645}, -{from: 287, to: 734}, -{from: 288, to: 311}, -{from: 288, to: 381}, -{from: 288, to: 409}, -{from: 288, to: 421}, -{from: 288, to: 425}, -{from: 288, to: 440}, -{from: 288, to: 472}, -{from: 288, to: 473}, -{from: 288, to: 508}, -{from: 288, to: 521}, -{from: 288, to: 522}, -{from: 288, to: 523}, -{from: 288, to: 525}, -{from: 288, to: 527}, -{from: 288, to: 543}, -{from: 288, to: 562}, -{from: 288, to: 565}, -{from: 288, to: 567}, -{from: 288, to: 589}, -{from: 288, to: 594}, -{from: 288, to: 604}, -{from: 288, to: 663}, -{from: 288, to: 728}, -{from: 289, to: 323}, -{from: 289, to: 331}, -{from: 289, to: 376}, -{from: 289, to: 428}, -{from: 289, to: 431}, -{from: 289, to: 436}, -{from: 289, to: 443}, -{from: 289, to: 490}, -{from: 289, to: 529}, -{from: 289, to: 532}, -{from: 289, to: 540}, -{from: 289, to: 547}, -{from: 289, to: 567}, -{from: 289, to: 586}, -{from: 289, to: 676}, -{from: 289, to: 699}, -{from: 289, to: 704}, -{from: 289, to: 717}, -{from: 289, to: 732}, -{from: 290, to: 330}, -{from: 290, to: 377}, -{from: 290, to: 390}, -{from: 290, to: 391}, -{from: 290, to: 392}, -{from: 290, to: 393}, -{from: 290, to: 394}, -{from: 290, to: 395}, -{from: 290, to: 399}, -{from: 290, to: 405}, -{from: 290, to: 411}, -{from: 290, to: 412}, -{from: 290, to: 413}, -{from: 290, to: 414}, -{from: 290, to: 559}, -{from: 290, to: 560}, -{from: 290, to: 561}, -{from: 290, to: 658}, -{from: 290, to: 689}, -{from: 290, to: 731}, -{from: 291, to: 382}, -{from: 291, to: 416}, -{from: 291, to: 422}, -{from: 291, to: 447}, -{from: 291, to: 449}, -{from: 291, to: 452}, -{from: 291, to: 478}, -{from: 291, to: 481}, -{from: 291, to: 482}, -{from: 291, to: 503}, -{from: 291, to: 534}, -{from: 291, to: 622}, -{from: 291, to: 670}, -{from: 291, to: 675}, -{from: 292, to: 293}, -{from: 292, to: 439}, -{from: 292, to: 540}, -{from: 292, to: 568}, -{from: 292, to: 640}, -{from: 292, to: 641}, -{from: 292, to: 695}, -{from: 292, to: 704}, -{from: 292, to: 708}, -{from: 292, to: 732}, -{from: 292, to: 733}, -{from: 293, to: 439}, -{from: 293, to: 540}, -{from: 293, to: 568}, -{from: 293, to: 640}, -{from: 293, to: 641}, -{from: 293, to: 695}, -{from: 293, to: 704}, -{from: 293, to: 708}, -{from: 293, to: 732}, -{from: 293, to: 733}, -{from: 294, to: 316}, -{from: 294, to: 349}, -{from: 294, to: 371}, -{from: 294, to: 373}, -{from: 294, to: 381}, -{from: 294, to: 397}, -{from: 294, to: 432}, -{from: 294, to: 443}, -{from: 294, to: 569}, -{from: 294, to: 571}, -{from: 294, to: 572}, -{from: 294, to: 589}, -{from: 294, to: 591}, -{from: 294, to: 623}, -{from: 294, to: 629}, -{from: 294, to: 643}, -{from: 294, to: 644}, -{from: 294, to: 719}, -{from: 294, to: 720}, -{from: 295, to: 317}, -{from: 295, to: 318}, -{from: 295, to: 355}, -{from: 295, to: 357}, -{from: 295, to: 446}, -{from: 295, to: 509}, -{from: 295, to: 510}, -{from: 295, to: 546}, -{from: 295, to: 564}, -{from: 295, to: 581}, -{from: 295, to: 592}, -{from: 296, to: 418}, -{from: 296, to: 435}, -{from: 296, to: 450}, -{from: 296, to: 493}, -{from: 296, to: 494}, -{from: 296, to: 519}, -{from: 296, to: 525}, -{from: 296, to: 526}, -{from: 296, to: 582}, -{from: 296, to: 585}, -{from: 296, to: 605}, -{from: 296, to: 619}, -{from: 296, to: 631}, -{from: 296, to: 655}, -{from: 296, to: 716}, -{from: 296, to: 722}, -{from: 297, to: 322}, -{from: 297, to: 398}, -{from: 297, to: 474}, -{from: 297, to: 485}, -{from: 297, to: 553}, -{from: 297, to: 621}, -{from: 297, to: 632}, -{from: 297, to: 638}, -{from: 297, to: 639}, -{from: 297, to: 657}, -{from: 297, to: 663}, -{from: 297, to: 671}, -{from: 297, to: 715}, -{from: 297, to: 726}, -{from: 298, to: 307}, -{from: 298, to: 310}, -{from: 298, to: 313}, -{from: 298, to: 458}, -{from: 298, to: 459}, -{from: 298, to: 468}, -{from: 298, to: 470}, -{from: 298, to: 471}, -{from: 298, to: 477}, -{from: 298, to: 479}, -{from: 298, to: 515}, -{from: 298, to: 518}, -{from: 298, to: 541}, -{from: 298, to: 620}, -{from: 298, to: 680}, -{from: 298, to: 686}, -{from: 299, to: 300}, -{from: 299, to: 301}, -{from: 299, to: 384}, -{from: 299, to: 431}, -{from: 299, to: 432}, -{from: 299, to: 444}, -{from: 299, to: 455}, -{from: 299, to: 469}, -{from: 299, to: 514}, -{from: 299, to: 535}, -{from: 299, to: 539}, -{from: 299, to: 542}, -{from: 299, to: 602}, -{from: 299, to: 624}, -{from: 299, to: 653}, -{from: 299, to: 669}, -{from: 299, to: 698}, -{from: 299, to: 703}, -{from: 300, to: 301}, -{from: 300, to: 432}, -{from: 300, to: 444}, -{from: 300, to: 455}, -{from: 300, to: 469}, -{from: 300, to: 514}, -{from: 300, to: 535}, -{from: 300, to: 539}, -{from: 300, to: 542}, -{from: 300, to: 624}, -{from: 300, to: 653}, -{from: 300, to: 662}, -{from: 300, to: 669}, -{from: 300, to: 698}, -{from: 301, to: 384}, -{from: 301, to: 431}, -{from: 301, to: 432}, -{from: 301, to: 444}, -{from: 301, to: 455}, -{from: 301, to: 469}, -{from: 301, to: 514}, -{from: 301, to: 535}, -{from: 301, to: 539}, -{from: 301, to: 542}, -{from: 301, to: 602}, -{from: 301, to: 624}, -{from: 301, to: 653}, -{from: 301, to: 669}, -{from: 301, to: 698}, -{from: 301, to: 703}, -{from: 302, to: 312}, -{from: 302, to: 497}, -{from: 302, to: 516}, -{from: 302, to: 524}, -{from: 302, to: 538}, -{from: 302, to: 633}, -{from: 302, to: 635}, -{from: 302, to: 636}, -{from: 302, to: 637}, -{from: 302, to: 684}, -{from: 302, to: 688}, -{from: 302, to: 697}, -{from: 302, to: 714}, -{from: 302, to: 736}, -{from: 303, to: 308}, -{from: 303, to: 335}, -{from: 303, to: 348}, -{from: 303, to: 409}, -{from: 303, to: 415}, -{from: 303, to: 433}, -{from: 303, to: 434}, -{from: 303, to: 498}, -{from: 303, to: 543}, -{from: 303, to: 573}, -{from: 303, to: 575}, -{from: 303, to: 576}, -{from: 303, to: 583}, -{from: 303, to: 603}, -{from: 303, to: 616}, -{from: 303, to: 629}, -{from: 303, to: 668}, -{from: 303, to: 679}, -{from: 303, to: 713}, -{from: 304, to: 309}, -{from: 304, to: 352}, -{from: 304, to: 366}, -{from: 304, to: 369}, -{from: 304, to: 370}, -{from: 304, to: 457}, -{from: 304, to: 526}, -{from: 304, to: 554}, -{from: 304, to: 630}, -{from: 304, to: 652}, -{from: 304, to: 667}, -{from: 304, to: 672}, -{from: 304, to: 701}, -{from: 305, to: 324}, -{from: 305, to: 334}, -{from: 305, to: 353}, -{from: 305, to: 368}, -{from: 305, to: 429}, -{from: 305, to: 489}, -{from: 305, to: 499}, -{from: 305, to: 548}, -{from: 305, to: 552}, -{from: 305, to: 595}, -{from: 305, to: 710}, -{from: 306, to: 315}, -{from: 306, to: 380}, -{from: 306, to: 389}, -{from: 306, to: 467}, -{from: 306, to: 495}, -{from: 306, to: 570}, -{from: 306, to: 584}, -{from: 306, to: 598}, -{from: 306, to: 599}, -{from: 306, to: 666}, -{from: 307, to: 310}, -{from: 307, to: 313}, -{from: 307, to: 458}, -{from: 307, to: 459}, -{from: 307, to: 468}, -{from: 307, to: 470}, -{from: 307, to: 471}, -{from: 307, to: 477}, -{from: 307, to: 479}, -{from: 307, to: 515}, -{from: 307, to: 518}, -{from: 307, to: 541}, -{from: 307, to: 620}, -{from: 307, to: 680}, -{from: 307, to: 686}, -{from: 308, to: 335}, -{from: 308, to: 348}, -{from: 308, to: 415}, -{from: 308, to: 434}, -{from: 308, to: 462}, -{from: 308, to: 555}, -{from: 308, to: 575}, -{from: 308, to: 576}, -{from: 308, to: 583}, -{from: 308, to: 603}, -{from: 308, to: 616}, -{from: 308, to: 642}, -{from: 308, to: 668}, -{from: 308, to: 707}, -{from: 308, to: 713}, -{from: 308, to: 726}, -{from: 309, to: 332}, -{from: 309, to: 366}, -{from: 309, to: 369}, -{from: 309, to: 370}, -{from: 309, to: 457}, -{from: 309, to: 554}, -{from: 309, to: 630}, -{from: 309, to: 645}, -{from: 309, to: 672}, -{from: 309, to: 701}, -{from: 310, to: 313}, -{from: 310, to: 458}, -{from: 310, to: 459}, -{from: 310, to: 468}, -{from: 310, to: 470}, -{from: 310, to: 471}, -{from: 310, to: 477}, -{from: 310, to: 479}, -{from: 310, to: 515}, -{from: 310, to: 518}, -{from: 310, to: 541}, -{from: 310, to: 620}, -{from: 310, to: 680}, -{from: 310, to: 686}, -{from: 311, to: 384}, -{from: 311, to: 386}, -{from: 311, to: 408}, -{from: 311, to: 460}, -{from: 311, to: 522}, -{from: 311, to: 525}, -{from: 311, to: 527}, -{from: 311, to: 532}, -{from: 311, to: 567}, -{from: 311, to: 612}, -{from: 311, to: 625}, -{from: 311, to: 654}, -{from: 311, to: 667}, -{from: 311, to: 677}, -{from: 311, to: 679}, -{from: 311, to: 685}, -{from: 311, to: 707}, -{from: 312, to: 497}, -{from: 312, to: 516}, -{from: 312, to: 524}, -{from: 312, to: 538}, -{from: 312, to: 633}, -{from: 312, to: 635}, -{from: 312, to: 636}, -{from: 312, to: 637}, -{from: 312, to: 684}, -{from: 312, to: 688}, -{from: 312, to: 697}, -{from: 312, to: 736}, -{from: 313, to: 458}, -{from: 313, to: 459}, -{from: 313, to: 468}, -{from: 313, to: 470}, -{from: 313, to: 471}, -{from: 313, to: 477}, -{from: 313, to: 479}, -{from: 313, to: 515}, -{from: 313, to: 518}, -{from: 313, to: 541}, -{from: 313, to: 620}, -{from: 313, to: 680}, -{from: 313, to: 686}, -{from: 314, to: 325}, -{from: 314, to: 338}, -{from: 314, to: 345}, -{from: 314, to: 350}, -{from: 314, to: 396}, -{from: 314, to: 417}, -{from: 314, to: 496}, -{from: 314, to: 507}, -{from: 314, to: 534}, -{from: 314, to: 566}, -{from: 314, to: 606}, -{from: 314, to: 613}, -{from: 314, to: 659}, -{from: 314, to: 673}, -{from: 314, to: 682}, -{from: 314, to: 714}, -{from: 315, to: 380}, -{from: 315, to: 389}, -{from: 315, to: 467}, -{from: 315, to: 495}, -{from: 315, to: 570}, -{from: 315, to: 584}, -{from: 315, to: 598}, -{from: 315, to: 599}, -{from: 315, to: 666}, -{from: 316, to: 327}, -{from: 316, to: 349}, -{from: 316, to: 371}, -{from: 316, to: 373}, -{from: 316, to: 375}, -{from: 316, to: 397}, -{from: 316, to: 442}, -{from: 316, to: 454}, -{from: 316, to: 455}, -{from: 316, to: 569}, -{from: 316, to: 572}, -{from: 316, to: 577}, -{from: 316, to: 591}, -{from: 316, to: 629}, -{from: 316, to: 643}, -{from: 316, to: 644}, -{from: 316, to: 678}, -{from: 316, to: 687}, -{from: 316, to: 719}, -{from: 316, to: 720}, -{from: 316, to: 721}, -{from: 317, to: 318}, -{from: 317, to: 355}, -{from: 317, to: 357}, -{from: 317, to: 446}, -{from: 317, to: 509}, -{from: 317, to: 510}, -{from: 317, to: 546}, -{from: 317, to: 564}, -{from: 317, to: 581}, -{from: 317, to: 592}, -{from: 318, to: 355}, -{from: 318, to: 357}, -{from: 318, to: 371}, -{from: 318, to: 446}, -{from: 318, to: 460}, -{from: 318, to: 509}, -{from: 318, to: 510}, -{from: 318, to: 528}, -{from: 318, to: 546}, -{from: 318, to: 562}, -{from: 318, to: 564}, -{from: 318, to: 576}, -{from: 318, to: 581}, -{from: 318, to: 592}, -{from: 318, to: 606}, -{from: 318, to: 646}, -{from: 318, to: 713}, -{from: 319, to: 349}, -{from: 319, to: 358}, -{from: 319, to: 368}, -{from: 319, to: 419}, -{from: 319, to: 424}, -{from: 319, to: 429}, -{from: 319, to: 450}, -{from: 319, to: 451}, -{from: 319, to: 462}, -{from: 319, to: 487}, -{from: 319, to: 489}, -{from: 319, to: 529}, -{from: 319, to: 555}, -{from: 319, to: 569}, -{from: 319, to: 600}, -{from: 319, to: 608}, -{from: 319, to: 642}, -{from: 319, to: 643}, -{from: 319, to: 645}, -{from: 319, to: 720}, -{from: 320, to: 344}, -{from: 320, to: 354}, -{from: 320, to: 361}, -{from: 320, to: 362}, -{from: 320, to: 367}, -{from: 320, to: 445}, -{from: 320, to: 483}, -{from: 320, to: 484}, -{from: 320, to: 512}, -{from: 320, to: 609}, -{from: 321, to: 337}, -{from: 321, to: 340}, -{from: 321, to: 407}, -{from: 321, to: 420}, -{from: 321, to: 488}, -{from: 321, to: 533}, -{from: 321, to: 579}, -{from: 321, to: 626}, -{from: 321, to: 627}, -{from: 321, to: 662}, -{from: 321, to: 705}, -{from: 322, to: 398}, -{from: 322, to: 474}, -{from: 322, to: 485}, -{from: 322, to: 553}, -{from: 322, to: 621}, -{from: 322, to: 632}, -{from: 322, to: 638}, -{from: 322, to: 639}, -{from: 322, to: 657}, -{from: 322, to: 671}, -{from: 322, to: 715}, -{from: 322, to: 726}, -{from: 323, to: 331}, -{from: 323, to: 376}, -{from: 323, to: 431}, -{from: 323, to: 436}, -{from: 323, to: 443}, -{from: 323, to: 490}, -{from: 323, to: 529}, -{from: 323, to: 547}, -{from: 323, to: 567}, -{from: 323, to: 586}, -{from: 323, to: 676}, -{from: 323, to: 699}, -{from: 323, to: 717}, -{from: 324, to: 334}, -{from: 324, to: 353}, -{from: 324, to: 368}, -{from: 324, to: 429}, -{from: 324, to: 489}, -{from: 324, to: 499}, -{from: 324, to: 548}, -{from: 324, to: 552}, -{from: 324, to: 595}, -{from: 324, to: 710}, -{from: 325, to: 338}, -{from: 325, to: 345}, -{from: 325, to: 350}, -{from: 325, to: 396}, -{from: 325, to: 417}, -{from: 325, to: 496}, -{from: 325, to: 507}, -{from: 325, to: 511}, -{from: 325, to: 534}, -{from: 325, to: 539}, -{from: 325, to: 566}, -{from: 325, to: 606}, -{from: 325, to: 613}, -{from: 325, to: 659}, -{from: 325, to: 673}, -{from: 325, to: 682}, -{from: 325, to: 714}, -{from: 326, to: 341}, -{from: 326, to: 365}, -{from: 326, to: 375}, -{from: 326, to: 406}, -{from: 326, to: 476}, -{from: 326, to: 502}, -{from: 326, to: 513}, -{from: 326, to: 530}, -{from: 326, to: 544}, -{from: 326, to: 681}, -{from: 326, to: 683}, -{from: 327, to: 374}, -{from: 327, to: 375}, -{from: 327, to: 385}, -{from: 327, to: 433}, -{from: 327, to: 442}, -{from: 327, to: 454}, -{from: 327, to: 455}, -{from: 327, to: 475}, -{from: 327, to: 480}, -{from: 327, to: 498}, -{from: 327, to: 517}, -{from: 327, to: 573}, -{from: 327, to: 577}, -{from: 327, to: 611}, -{from: 327, to: 614}, -{from: 327, to: 623}, -{from: 327, to: 648}, -{from: 327, to: 678}, -{from: 327, to: 687}, -{from: 327, to: 721}, -{from: 328, to: 329}, -{from: 328, to: 351}, -{from: 328, to: 367}, -{from: 328, to: 372}, -{from: 328, to: 426}, -{from: 328, to: 427}, -{from: 328, to: 456}, -{from: 328, to: 464}, -{from: 328, to: 492}, -{from: 328, to: 536}, -{from: 328, to: 549}, -{from: 328, to: 551}, -{from: 328, to: 609}, -{from: 328, to: 615}, -{from: 328, to: 700}, -{from: 328, to: 718}, -{from: 329, to: 343}, -{from: 329, to: 351}, -{from: 329, to: 367}, -{from: 329, to: 372}, -{from: 329, to: 426}, -{from: 329, to: 427}, -{from: 329, to: 456}, -{from: 329, to: 464}, -{from: 329, to: 492}, -{from: 329, to: 536}, -{from: 329, to: 549}, -{from: 329, to: 551}, -{from: 329, to: 563}, -{from: 329, to: 609}, -{from: 329, to: 615}, -{from: 329, to: 700}, -{from: 329, to: 718}, -{from: 330, to: 377}, -{from: 330, to: 390}, -{from: 330, to: 391}, -{from: 330, to: 392}, -{from: 330, to: 393}, -{from: 330, to: 394}, -{from: 330, to: 395}, -{from: 330, to: 399}, -{from: 330, to: 405}, -{from: 330, to: 411}, -{from: 330, to: 412}, -{from: 330, to: 413}, -{from: 330, to: 414}, -{from: 330, to: 559}, -{from: 330, to: 560}, -{from: 330, to: 561}, -{from: 330, to: 566}, -{from: 330, to: 658}, -{from: 330, to: 731}, -{from: 331, to: 376}, -{from: 331, to: 431}, -{from: 331, to: 436}, -{from: 331, to: 443}, -{from: 331, to: 490}, -{from: 331, to: 529}, -{from: 331, to: 547}, -{from: 331, to: 567}, -{from: 331, to: 586}, -{from: 331, to: 676}, -{from: 331, to: 699}, -{from: 331, to: 717}, -{from: 332, to: 333}, -{from: 332, to: 428}, -{from: 332, to: 511}, -{from: 332, to: 528}, -{from: 332, to: 571}, -{from: 332, to: 580}, -{from: 332, to: 593}, -{from: 332, to: 601}, -{from: 332, to: 618}, -{from: 332, to: 619}, -{from: 332, to: 645}, -{from: 332, to: 652}, -{from: 332, to: 703}, -{from: 332, to: 716}, -{from: 333, to: 428}, -{from: 333, to: 511}, -{from: 333, to: 528}, -{from: 333, to: 571}, -{from: 333, to: 578}, -{from: 333, to: 580}, -{from: 333, to: 593}, -{from: 333, to: 601}, -{from: 333, to: 618}, -{from: 333, to: 619}, -{from: 333, to: 652}, -{from: 333, to: 655}, -{from: 333, to: 703}, -{from: 333, to: 716}, -{from: 334, to: 353}, -{from: 334, to: 355}, -{from: 334, to: 368}, -{from: 334, to: 429}, -{from: 334, to: 446}, -{from: 334, to: 489}, -{from: 334, to: 499}, -{from: 334, to: 548}, -{from: 334, to: 552}, -{from: 334, to: 595}, -{from: 334, to: 710}, -{from: 335, to: 348}, -{from: 335, to: 415}, -{from: 335, to: 434}, -{from: 335, to: 462}, -{from: 335, to: 555}, -{from: 335, to: 575}, -{from: 335, to: 576}, -{from: 335, to: 583}, -{from: 335, to: 603}, -{from: 335, to: 616}, -{from: 335, to: 642}, -{from: 335, to: 668}, -{from: 335, to: 707}, -{from: 335, to: 713}, -{from: 335, to: 726}, -{from: 336, to: 343}, -{from: 336, to: 356}, -{from: 336, to: 360}, -{from: 336, to: 378}, -{from: 336, to: 388}, -{from: 336, to: 401}, -{from: 336, to: 448}, -{from: 336, to: 501}, -{from: 336, to: 506}, -{from: 336, to: 550}, -{from: 336, to: 563}, -{from: 336, to: 588}, -{from: 336, to: 617}, -{from: 336, to: 712}, -{from: 336, to: 727}, -{from: 337, to: 374}, -{from: 337, to: 387}, -{from: 337, to: 396}, -{from: 337, to: 407}, -{from: 337, to: 420}, -{from: 337, to: 488}, -{from: 337, to: 533}, -{from: 337, to: 579}, -{from: 337, to: 626}, -{from: 337, to: 627}, -{from: 337, to: 633}, -{from: 337, to: 662}, -{from: 337, to: 705}, -{from: 338, to: 345}, -{from: 338, to: 350}, -{from: 338, to: 396}, -{from: 338, to: 417}, -{from: 338, to: 496}, -{from: 338, to: 507}, -{from: 338, to: 534}, -{from: 338, to: 566}, -{from: 338, to: 606}, -{from: 338, to: 613}, -{from: 338, to: 659}, -{from: 338, to: 673}, -{from: 338, to: 682}, -{from: 338, to: 714}, -{from: 339, to: 364}, -{from: 339, to: 453}, -{from: 339, to: 504}, -{from: 339, to: 514}, -{from: 339, to: 578}, -{from: 339, to: 596}, -{from: 339, to: 602}, -{from: 339, to: 610}, -{from: 339, to: 661}, -{from: 339, to: 665}, -{from: 339, to: 690}, -{from: 339, to: 692}, -{from: 339, to: 693}, -{from: 339, to: 721}, -{from: 339, to: 723}, -{from: 340, to: 346}, -{from: 340, to: 347}, -{from: 340, to: 387}, -{from: 340, to: 404}, -{from: 340, to: 437}, -{from: 340, to: 503}, -{from: 340, to: 520}, -{from: 340, to: 590}, -{from: 340, to: 628}, -{from: 340, to: 664}, -{from: 340, to: 670}, -{from: 340, to: 709}, -{from: 341, to: 365}, -{from: 341, to: 375}, -{from: 341, to: 406}, -{from: 341, to: 476}, -{from: 341, to: 502}, -{from: 341, to: 513}, -{from: 341, to: 530}, -{from: 341, to: 544}, -{from: 341, to: 681}, -{from: 341, to: 683}, -{from: 342, to: 363}, -{from: 342, to: 379}, -{from: 342, to: 383}, -{from: 342, to: 403}, -{from: 342, to: 500}, -{from: 342, to: 505}, -{from: 342, to: 537}, -{from: 342, to: 547}, -{from: 342, to: 574}, -{from: 342, to: 586}, -{from: 342, to: 587}, -{from: 342, to: 597}, -{from: 342, to: 627}, -{from: 342, to: 649}, -{from: 342, to: 691}, -{from: 342, to: 702}, -{from: 342, to: 706}, -{from: 342, to: 717}, -{from: 343, to: 360}, -{from: 343, to: 378}, -{from: 343, to: 388}, -{from: 343, to: 448}, -{from: 343, to: 501}, -{from: 343, to: 506}, -{from: 343, to: 550}, -{from: 343, to: 563}, -{from: 343, to: 588}, -{from: 343, to: 617}, -{from: 343, to: 712}, -{from: 343, to: 727}, -{from: 344, to: 354}, -{from: 344, to: 361}, -{from: 344, to: 362}, -{from: 344, to: 445}, -{from: 344, to: 483}, -{from: 344, to: 484}, -{from: 344, to: 512}, -{from: 345, to: 350}, -{from: 345, to: 396}, -{from: 345, to: 417}, -{from: 345, to: 496}, -{from: 345, to: 507}, -{from: 345, to: 534}, -{from: 345, to: 566}, -{from: 345, to: 606}, -{from: 345, to: 613}, -{from: 345, to: 659}, -{from: 345, to: 673}, -{from: 345, to: 682}, -{from: 345, to: 714}, -{from: 345, to: 715}, -{from: 346, to: 347}, -{from: 346, to: 387}, -{from: 346, to: 404}, -{from: 346, to: 437}, -{from: 346, to: 503}, -{from: 346, to: 520}, -{from: 346, to: 590}, -{from: 346, to: 628}, -{from: 346, to: 664}, -{from: 346, to: 670}, -{from: 346, to: 709}, -{from: 347, to: 387}, -{from: 347, to: 404}, -{from: 347, to: 437}, -{from: 347, to: 503}, -{from: 347, to: 520}, -{from: 347, to: 590}, -{from: 347, to: 628}, -{from: 347, to: 664}, -{from: 347, to: 670}, -{from: 347, to: 709}, -{from: 348, to: 398}, -{from: 348, to: 415}, -{from: 348, to: 430}, -{from: 348, to: 434}, -{from: 348, to: 440}, -{from: 348, to: 575}, -{from: 348, to: 576}, -{from: 348, to: 583}, -{from: 348, to: 603}, -{from: 348, to: 616}, -{from: 348, to: 654}, -{from: 348, to: 668}, -{from: 348, to: 702}, -{from: 348, to: 713}, -{from: 349, to: 368}, -{from: 349, to: 371}, -{from: 349, to: 373}, -{from: 349, to: 397}, -{from: 349, to: 419}, -{from: 349, to: 429}, -{from: 349, to: 489}, -{from: 349, to: 529}, -{from: 349, to: 569}, -{from: 349, to: 572}, -{from: 349, to: 591}, -{from: 349, to: 629}, -{from: 349, to: 643}, -{from: 349, to: 644}, -{from: 349, to: 719}, -{from: 349, to: 720}, -{from: 350, to: 396}, -{from: 350, to: 417}, -{from: 350, to: 496}, -{from: 350, to: 507}, -{from: 350, to: 534}, -{from: 350, to: 566}, -{from: 350, to: 606}, -{from: 350, to: 613}, -{from: 350, to: 659}, -{from: 350, to: 673}, -{from: 350, to: 682}, -{from: 350, to: 714}, -{from: 351, to: 367}, -{from: 351, to: 372}, -{from: 351, to: 426}, -{from: 351, to: 427}, -{from: 351, to: 456}, -{from: 351, to: 464}, -{from: 351, to: 492}, -{from: 351, to: 536}, -{from: 351, to: 549}, -{from: 351, to: 551}, -{from: 351, to: 609}, -{from: 351, to: 615}, -{from: 351, to: 700}, -{from: 351, to: 718}, -{from: 352, to: 359}, -{from: 352, to: 369}, -{from: 352, to: 430}, -{from: 352, to: 461}, -{from: 352, to: 463}, -{from: 352, to: 486}, -{from: 352, to: 526}, -{from: 352, to: 531}, -{from: 352, to: 607}, -{from: 352, to: 634}, -{from: 352, to: 652}, -{from: 352, to: 667}, -{from: 352, to: 711}, -{from: 353, to: 368}, -{from: 353, to: 429}, -{from: 353, to: 489}, -{from: 353, to: 499}, -{from: 353, to: 548}, -{from: 353, to: 552}, -{from: 353, to: 595}, -{from: 353, to: 710}, -{from: 354, to: 361}, -{from: 354, to: 362}, -{from: 354, to: 445}, -{from: 354, to: 483}, -{from: 354, to: 484}, -{from: 354, to: 512}, -{from: 355, to: 357}, -{from: 355, to: 446}, -{from: 355, to: 509}, -{from: 355, to: 510}, -{from: 355, to: 546}, -{from: 355, to: 564}, -{from: 355, to: 581}, -{from: 355, to: 592}, -{from: 356, to: 400}, -{from: 356, to: 401}, -{from: 356, to: 402}, -{from: 356, to: 410}, -{from: 356, to: 423}, -{from: 356, to: 545}, -{from: 356, to: 556}, -{from: 356, to: 557}, -{from: 356, to: 558}, -{from: 356, to: 656}, -{from: 356, to: 660}, -{from: 356, to: 674}, -{from: 356, to: 694}, -{from: 356, to: 696}, -{from: 357, to: 446}, -{from: 357, to: 509}, -{from: 357, to: 510}, -{from: 357, to: 546}, -{from: 357, to: 564}, -{from: 357, to: 581}, -{from: 357, to: 592}, -{from: 358, to: 419}, -{from: 358, to: 424}, -{from: 358, to: 450}, -{from: 358, to: 451}, -{from: 358, to: 462}, -{from: 358, to: 487}, -{from: 358, to: 555}, -{from: 358, to: 600}, -{from: 358, to: 608}, -{from: 358, to: 642}, -{from: 358, to: 645}, -{from: 359, to: 373}, -{from: 359, to: 397}, -{from: 359, to: 430}, -{from: 359, to: 461}, -{from: 359, to: 463}, -{from: 359, to: 486}, -{from: 359, to: 531}, -{from: 359, to: 607}, -{from: 359, to: 634}, -{from: 359, to: 677}, -{from: 359, to: 685}, -{from: 359, to: 711}, -{from: 360, to: 378}, -{from: 360, to: 388}, -{from: 360, to: 448}, -{from: 360, to: 501}, -{from: 360, to: 506}, -{from: 360, to: 550}, -{from: 360, to: 563}, -{from: 360, to: 588}, -{from: 360, to: 617}, -{from: 360, to: 712}, -{from: 360, to: 727}, -{from: 361, to: 362}, -{from: 361, to: 445}, -{from: 361, to: 483}, -{from: 361, to: 484}, -{from: 361, to: 512}, -{from: 362, to: 445}, -{from: 362, to: 483}, -{from: 362, to: 484}, -{from: 362, to: 512}, -{from: 363, to: 379}, -{from: 363, to: 383}, -{from: 363, to: 403}, -{from: 363, to: 417}, -{from: 363, to: 500}, -{from: 363, to: 505}, -{from: 363, to: 537}, -{from: 363, to: 574}, -{from: 363, to: 587}, -{from: 363, to: 597}, -{from: 363, to: 649}, -{from: 363, to: 691}, -{from: 363, to: 702}, -{from: 363, to: 706}, -{from: 364, to: 438}, -{from: 364, to: 453}, -{from: 364, to: 504}, -{from: 364, to: 578}, -{from: 364, to: 596}, -{from: 364, to: 602}, -{from: 364, to: 610}, -{from: 364, to: 661}, -{from: 364, to: 665}, -{from: 364, to: 683}, -{from: 364, to: 690}, -{from: 364, to: 692}, -{from: 364, to: 693}, -{from: 364, to: 721}, -{from: 364, to: 723}, -{from: 365, to: 375}, -{from: 365, to: 390}, -{from: 365, to: 406}, -{from: 365, to: 476}, -{from: 365, to: 502}, -{from: 365, to: 513}, -{from: 365, to: 530}, -{from: 365, to: 544}, -{from: 365, to: 681}, -{from: 365, to: 683}, -{from: 366, to: 369}, -{from: 366, to: 370}, -{from: 366, to: 422}, -{from: 366, to: 457}, -{from: 366, to: 554}, -{from: 366, to: 572}, -{from: 366, to: 591}, -{from: 366, to: 630}, -{from: 366, to: 672}, -{from: 366, to: 693}, -{from: 366, to: 701}, -{from: 367, to: 372}, -{from: 367, to: 426}, -{from: 367, to: 427}, -{from: 367, to: 456}, -{from: 367, to: 464}, -{from: 367, to: 492}, -{from: 367, to: 536}, -{from: 367, to: 549}, -{from: 367, to: 551}, -{from: 367, to: 609}, -{from: 367, to: 615}, -{from: 367, to: 700}, -{from: 367, to: 718}, -{from: 368, to: 419}, -{from: 368, to: 429}, -{from: 368, to: 489}, -{from: 368, to: 499}, -{from: 368, to: 529}, -{from: 368, to: 548}, -{from: 368, to: 552}, -{from: 368, to: 569}, -{from: 368, to: 595}, -{from: 368, to: 643}, -{from: 368, to: 710}, -{from: 368, to: 720}, -{from: 369, to: 370}, -{from: 369, to: 457}, -{from: 369, to: 526}, -{from: 369, to: 554}, -{from: 369, to: 630}, -{from: 369, to: 652}, -{from: 369, to: 667}, -{from: 369, to: 672}, -{from: 369, to: 701}, -{from: 370, to: 457}, -{from: 370, to: 554}, -{from: 370, to: 630}, -{from: 370, to: 672}, -{from: 370, to: 701}, -{from: 371, to: 373}, -{from: 371, to: 397}, -{from: 371, to: 460}, -{from: 371, to: 528}, -{from: 371, to: 562}, -{from: 371, to: 569}, -{from: 371, to: 572}, -{from: 371, to: 576}, -{from: 371, to: 591}, -{from: 371, to: 606}, -{from: 371, to: 629}, -{from: 371, to: 643}, -{from: 371, to: 644}, -{from: 371, to: 646}, -{from: 371, to: 713}, -{from: 371, to: 719}, -{from: 371, to: 720}, -{from: 372, to: 426}, -{from: 372, to: 427}, -{from: 372, to: 456}, -{from: 372, to: 464}, -{from: 372, to: 492}, -{from: 372, to: 536}, -{from: 372, to: 549}, -{from: 372, to: 551}, -{from: 372, to: 609}, -{from: 372, to: 615}, -{from: 372, to: 700}, -{from: 372, to: 718}, -{from: 373, to: 397}, -{from: 373, to: 569}, -{from: 373, to: 572}, -{from: 373, to: 591}, -{from: 373, to: 629}, -{from: 373, to: 643}, -{from: 373, to: 644}, -{from: 373, to: 677}, -{from: 373, to: 685}, -{from: 373, to: 719}, -{from: 373, to: 720}, -{from: 374, to: 385}, -{from: 374, to: 387}, -{from: 374, to: 396}, -{from: 374, to: 433}, -{from: 374, to: 442}, -{from: 374, to: 454}, -{from: 374, to: 475}, -{from: 374, to: 480}, -{from: 374, to: 498}, -{from: 374, to: 517}, -{from: 374, to: 573}, -{from: 374, to: 577}, -{from: 374, to: 611}, -{from: 374, to: 614}, -{from: 374, to: 623}, -{from: 374, to: 633}, -{from: 374, to: 648}, -{from: 374, to: 678}, -{from: 374, to: 687}, -{from: 375, to: 406}, -{from: 375, to: 442}, -{from: 375, to: 454}, -{from: 375, to: 455}, -{from: 375, to: 476}, -{from: 375, to: 502}, -{from: 375, to: 513}, -{from: 375, to: 530}, -{from: 375, to: 544}, -{from: 375, to: 577}, -{from: 375, to: 678}, -{from: 375, to: 681}, -{from: 375, to: 683}, -{from: 375, to: 687}, -{from: 375, to: 721}, -{from: 376, to: 431}, -{from: 376, to: 436}, -{from: 376, to: 443}, -{from: 376, to: 490}, -{from: 376, to: 502}, -{from: 376, to: 529}, -{from: 376, to: 547}, -{from: 376, to: 567}, -{from: 376, to: 586}, -{from: 376, to: 676}, -{from: 376, to: 699}, -{from: 376, to: 717}, -{from: 377, to: 390}, -{from: 377, to: 391}, -{from: 377, to: 392}, -{from: 377, to: 393}, -{from: 377, to: 394}, -{from: 377, to: 395}, -{from: 377, to: 399}, -{from: 377, to: 405}, -{from: 377, to: 411}, -{from: 377, to: 412}, -{from: 377, to: 413}, -{from: 377, to: 414}, -{from: 377, to: 559}, -{from: 377, to: 560}, -{from: 377, to: 561}, -{from: 377, to: 658}, -{from: 377, to: 731}, -{from: 378, to: 388}, -{from: 378, to: 399}, -{from: 378, to: 448}, -{from: 378, to: 488}, -{from: 378, to: 501}, -{from: 378, to: 506}, -{from: 378, to: 550}, -{from: 378, to: 561}, -{from: 378, to: 563}, -{from: 378, to: 588}, -{from: 378, to: 617}, -{from: 378, to: 647}, -{from: 378, to: 712}, -{from: 378, to: 727}, -{from: 379, to: 383}, -{from: 379, to: 403}, -{from: 379, to: 500}, -{from: 379, to: 505}, -{from: 379, to: 537}, -{from: 379, to: 574}, -{from: 379, to: 587}, -{from: 379, to: 597}, -{from: 379, to: 649}, -{from: 379, to: 691}, -{from: 379, to: 702}, -{from: 379, to: 706}, -{from: 380, to: 389}, -{from: 380, to: 467}, -{from: 380, to: 495}, -{from: 380, to: 570}, -{from: 380, to: 584}, -{from: 380, to: 598}, -{from: 380, to: 599}, -{from: 380, to: 666}, -{from: 381, to: 409}, -{from: 381, to: 421}, -{from: 381, to: 425}, -{from: 381, to: 432}, -{from: 381, to: 440}, -{from: 381, to: 443}, -{from: 381, to: 472}, -{from: 381, to: 473}, -{from: 381, to: 508}, -{from: 381, to: 521}, -{from: 381, to: 523}, -{from: 381, to: 543}, -{from: 381, to: 562}, -{from: 381, to: 565}, -{from: 381, to: 571}, -{from: 381, to: 589}, -{from: 381, to: 594}, -{from: 381, to: 604}, -{from: 381, to: 623}, -{from: 381, to: 644}, -{from: 381, to: 663}, -{from: 381, to: 719}, -{from: 381, to: 728}, -{from: 382, to: 438}, -{from: 382, to: 441}, -{from: 382, to: 452}, -{from: 382, to: 465}, -{from: 382, to: 466}, -{from: 382, to: 481}, -{from: 382, to: 491}, -{from: 382, to: 503}, -{from: 382, to: 534}, -{from: 382, to: 646}, -{from: 382, to: 647}, -{from: 382, to: 650}, -{from: 382, to: 651}, -{from: 382, to: 670}, -{from: 382, to: 689}, -{from: 382, to: 724}, -{from: 382, to: 725}, -{from: 382, to: 729}, -{from: 382, to: 730}, -{from: 382, to: 734}, -{from: 382, to: 735}, -{from: 383, to: 403}, -{from: 383, to: 500}, -{from: 383, to: 505}, -{from: 383, to: 537}, -{from: 383, to: 574}, -{from: 383, to: 587}, -{from: 383, to: 597}, -{from: 383, to: 649}, -{from: 383, to: 691}, -{from: 383, to: 702}, -{from: 383, to: 706}, -{from: 384, to: 386}, -{from: 384, to: 408}, -{from: 384, to: 431}, -{from: 384, to: 460}, -{from: 384, to: 522}, -{from: 384, to: 527}, -{from: 384, to: 532}, -{from: 384, to: 602}, -{from: 384, to: 612}, -{from: 384, to: 625}, -{from: 384, to: 654}, -{from: 384, to: 667}, -{from: 384, to: 677}, -{from: 384, to: 679}, -{from: 384, to: 685}, -{from: 384, to: 703}, -{from: 384, to: 707}, -{from: 385, to: 433}, -{from: 385, to: 442}, -{from: 385, to: 454}, -{from: 385, to: 475}, -{from: 385, to: 480}, -{from: 385, to: 498}, -{from: 385, to: 517}, -{from: 385, to: 518}, -{from: 385, to: 573}, -{from: 385, to: 577}, -{from: 385, to: 611}, -{from: 385, to: 614}, -{from: 385, to: 623}, -{from: 385, to: 648}, -{from: 385, to: 656}, -{from: 385, to: 678}, -{from: 385, to: 687}, -{from: 386, to: 408}, -{from: 386, to: 415}, -{from: 386, to: 460}, -{from: 386, to: 522}, -{from: 386, to: 527}, -{from: 386, to: 532}, -{from: 386, to: 575}, -{from: 386, to: 612}, -{from: 386, to: 616}, -{from: 386, to: 625}, -{from: 386, to: 654}, -{from: 386, to: 667}, -{from: 386, to: 677}, -{from: 386, to: 679}, -{from: 386, to: 681}, -{from: 386, to: 685}, -{from: 386, to: 707}, -{from: 387, to: 396}, -{from: 387, to: 404}, -{from: 387, to: 437}, -{from: 387, to: 503}, -{from: 387, to: 520}, -{from: 387, to: 590}, -{from: 387, to: 628}, -{from: 387, to: 633}, -{from: 387, to: 664}, -{from: 387, to: 670}, -{from: 387, to: 709}, -{from: 388, to: 423}, -{from: 388, to: 448}, -{from: 388, to: 501}, -{from: 388, to: 506}, -{from: 388, to: 550}, -{from: 388, to: 563}, -{from: 388, to: 588}, -{from: 388, to: 617}, -{from: 388, to: 712}, -{from: 388, to: 727}, -{from: 389, to: 467}, -{from: 389, to: 495}, -{from: 389, to: 570}, -{from: 389, to: 584}, -{from: 389, to: 598}, -{from: 389, to: 599}, -{from: 389, to: 666}, -{from: 390, to: 391}, -{from: 390, to: 392}, -{from: 390, to: 393}, -{from: 390, to: 394}, -{from: 390, to: 395}, -{from: 390, to: 399}, -{from: 390, to: 405}, -{from: 390, to: 411}, -{from: 390, to: 412}, -{from: 390, to: 413}, -{from: 390, to: 414}, -{from: 390, to: 559}, -{from: 390, to: 560}, -{from: 390, to: 561}, -{from: 390, to: 658}, -{from: 390, to: 731}, -{from: 391, to: 392}, -{from: 391, to: 393}, -{from: 391, to: 394}, -{from: 391, to: 395}, -{from: 391, to: 399}, -{from: 391, to: 405}, -{from: 391, to: 411}, -{from: 391, to: 412}, -{from: 391, to: 413}, -{from: 391, to: 414}, -{from: 391, to: 559}, -{from: 391, to: 560}, -{from: 391, to: 561}, -{from: 391, to: 658}, -{from: 391, to: 731}, -{from: 392, to: 393}, -{from: 392, to: 394}, -{from: 392, to: 395}, -{from: 392, to: 399}, -{from: 392, to: 405}, -{from: 392, to: 411}, -{from: 392, to: 412}, -{from: 392, to: 413}, -{from: 392, to: 414}, -{from: 392, to: 559}, -{from: 392, to: 560}, -{from: 392, to: 561}, -{from: 392, to: 658}, -{from: 392, to: 731}, -{from: 393, to: 394}, -{from: 393, to: 395}, -{from: 393, to: 399}, -{from: 393, to: 405}, -{from: 393, to: 411}, -{from: 393, to: 412}, -{from: 393, to: 413}, -{from: 393, to: 414}, -{from: 393, to: 559}, -{from: 393, to: 560}, -{from: 393, to: 561}, -{from: 393, to: 658}, -{from: 393, to: 731}, -{from: 394, to: 395}, -{from: 394, to: 399}, -{from: 394, to: 405}, -{from: 394, to: 411}, -{from: 394, to: 412}, -{from: 394, to: 413}, -{from: 394, to: 414}, -{from: 394, to: 559}, -{from: 394, to: 560}, -{from: 394, to: 561}, -{from: 394, to: 658}, -{from: 394, to: 731}, -{from: 395, to: 399}, -{from: 395, to: 405}, -{from: 395, to: 411}, -{from: 395, to: 412}, -{from: 395, to: 413}, -{from: 395, to: 414}, -{from: 395, to: 559}, -{from: 395, to: 560}, -{from: 395, to: 561}, -{from: 395, to: 658}, -{from: 395, to: 731}, -{from: 396, to: 417}, -{from: 396, to: 496}, -{from: 396, to: 507}, -{from: 396, to: 534}, -{from: 396, to: 566}, -{from: 396, to: 606}, -{from: 396, to: 613}, -{from: 396, to: 633}, -{from: 396, to: 659}, -{from: 396, to: 673}, -{from: 396, to: 682}, -{from: 396, to: 714}, -{from: 397, to: 569}, -{from: 397, to: 572}, -{from: 397, to: 591}, -{from: 397, to: 629}, -{from: 397, to: 643}, -{from: 397, to: 644}, -{from: 397, to: 677}, -{from: 397, to: 685}, -{from: 397, to: 719}, -{from: 397, to: 720}, -{from: 398, to: 430}, -{from: 398, to: 440}, -{from: 398, to: 474}, -{from: 398, to: 485}, -{from: 398, to: 553}, -{from: 398, to: 583}, -{from: 398, to: 621}, -{from: 398, to: 632}, -{from: 398, to: 638}, -{from: 398, to: 639}, -{from: 398, to: 654}, -{from: 398, to: 657}, -{from: 398, to: 668}, -{from: 398, to: 671}, -{from: 398, to: 702}, -{from: 398, to: 715}, -{from: 398, to: 726}, -{from: 399, to: 405}, -{from: 399, to: 411}, -{from: 399, to: 412}, -{from: 399, to: 413}, -{from: 399, to: 414}, -{from: 399, to: 488}, -{from: 399, to: 559}, -{from: 399, to: 560}, -{from: 399, to: 561}, -{from: 399, to: 647}, -{from: 399, to: 658}, -{from: 399, to: 731}, -{from: 400, to: 401}, -{from: 400, to: 402}, -{from: 400, to: 410}, -{from: 400, to: 423}, -{from: 400, to: 545}, -{from: 400, to: 556}, -{from: 400, to: 557}, -{from: 400, to: 558}, -{from: 400, to: 656}, -{from: 400, to: 660}, -{from: 400, to: 674}, -{from: 400, to: 694}, -{from: 400, to: 696}, -{from: 401, to: 402}, -{from: 401, to: 410}, -{from: 401, to: 423}, -{from: 401, to: 545}, -{from: 401, to: 556}, -{from: 401, to: 557}, -{from: 401, to: 558}, -{from: 401, to: 656}, -{from: 401, to: 660}, -{from: 401, to: 674}, -{from: 401, to: 694}, -{from: 401, to: 696}, -{from: 402, to: 410}, -{from: 402, to: 423}, -{from: 402, to: 545}, -{from: 402, to: 556}, -{from: 402, to: 557}, -{from: 402, to: 558}, -{from: 402, to: 656}, -{from: 402, to: 660}, -{from: 402, to: 674}, -{from: 402, to: 694}, -{from: 402, to: 696}, -{from: 403, to: 500}, -{from: 403, to: 505}, -{from: 403, to: 537}, -{from: 403, to: 574}, -{from: 403, to: 587}, -{from: 403, to: 597}, -{from: 403, to: 649}, -{from: 403, to: 691}, -{from: 403, to: 702}, -{from: 403, to: 706}, -{from: 404, to: 416}, -{from: 404, to: 437}, -{from: 404, to: 461}, -{from: 404, to: 483}, -{from: 404, to: 503}, -{from: 404, to: 520}, -{from: 404, to: 565}, -{from: 404, to: 590}, -{from: 404, to: 628}, -{from: 404, to: 661}, -{from: 404, to: 664}, -{from: 404, to: 670}, -{from: 404, to: 709}, -{from: 405, to: 411}, -{from: 405, to: 412}, -{from: 405, to: 413}, -{from: 405, to: 414}, -{from: 405, to: 559}, -{from: 405, to: 560}, -{from: 405, to: 561}, -{from: 405, to: 658}, -{from: 405, to: 731}, -{from: 406, to: 476}, -{from: 406, to: 502}, -{from: 406, to: 513}, -{from: 406, to: 530}, -{from: 406, to: 544}, -{from: 406, to: 681}, -{from: 406, to: 683}, -{from: 407, to: 420}, -{from: 407, to: 488}, -{from: 407, to: 533}, -{from: 407, to: 579}, -{from: 407, to: 626}, -{from: 407, to: 627}, -{from: 407, to: 662}, -{from: 407, to: 705}, -{from: 408, to: 460}, -{from: 408, to: 522}, -{from: 408, to: 527}, -{from: 408, to: 532}, -{from: 408, to: 612}, -{from: 408, to: 625}, -{from: 408, to: 654}, -{from: 408, to: 667}, -{from: 408, to: 677}, -{from: 408, to: 679}, -{from: 408, to: 685}, -{from: 408, to: 707}, -{from: 409, to: 421}, -{from: 409, to: 425}, -{from: 409, to: 433}, -{from: 409, to: 440}, -{from: 409, to: 472}, -{from: 409, to: 473}, -{from: 409, to: 498}, -{from: 409, to: 508}, -{from: 409, to: 521}, -{from: 409, to: 523}, -{from: 409, to: 543}, -{from: 409, to: 562}, -{from: 409, to: 565}, -{from: 409, to: 573}, -{from: 409, to: 589}, -{from: 409, to: 594}, -{from: 409, to: 604}, -{from: 409, to: 629}, -{from: 409, to: 663}, -{from: 409, to: 679}, -{from: 409, to: 728}, -{from: 410, to: 423}, -{from: 410, to: 545}, -{from: 410, to: 556}, -{from: 410, to: 557}, -{from: 410, to: 558}, -{from: 410, to: 656}, -{from: 410, to: 660}, -{from: 410, to: 674}, -{from: 410, to: 694}, -{from: 410, to: 696}, -{from: 411, to: 412}, -{from: 411, to: 413}, -{from: 411, to: 414}, -{from: 411, to: 559}, -{from: 411, to: 560}, -{from: 411, to: 561}, -{from: 411, to: 658}, -{from: 411, to: 731}, -{from: 412, to: 413}, -{from: 412, to: 414}, -{from: 412, to: 559}, -{from: 412, to: 560}, -{from: 412, to: 561}, -{from: 412, to: 658}, -{from: 412, to: 731}, -{from: 413, to: 414}, -{from: 413, to: 559}, -{from: 413, to: 560}, -{from: 413, to: 561}, -{from: 413, to: 658}, -{from: 413, to: 731}, -{from: 414, to: 559}, -{from: 414, to: 560}, -{from: 414, to: 561}, -{from: 414, to: 658}, -{from: 414, to: 731}, -{from: 415, to: 434}, -{from: 415, to: 575}, -{from: 415, to: 576}, -{from: 415, to: 583}, -{from: 415, to: 603}, -{from: 415, to: 612}, -{from: 415, to: 616}, -{from: 415, to: 668}, -{from: 415, to: 681}, -{from: 415, to: 713}, -{from: 416, to: 422}, -{from: 416, to: 447}, -{from: 416, to: 449}, -{from: 416, to: 452}, -{from: 416, to: 461}, -{from: 416, to: 478}, -{from: 416, to: 481}, -{from: 416, to: 482}, -{from: 416, to: 483}, -{from: 416, to: 565}, -{from: 416, to: 622}, -{from: 416, to: 661}, -{from: 416, to: 675}, -{from: 417, to: 496}, -{from: 417, to: 507}, -{from: 417, to: 534}, -{from: 417, to: 566}, -{from: 417, to: 606}, -{from: 417, to: 613}, -{from: 417, to: 659}, -{from: 417, to: 673}, -{from: 417, to: 682}, -{from: 417, to: 714}, -{from: 418, to: 435}, -{from: 418, to: 493}, -{from: 418, to: 494}, -{from: 418, to: 519}, -{from: 418, to: 525}, -{from: 418, to: 526}, -{from: 418, to: 582}, -{from: 418, to: 585}, -{from: 418, to: 605}, -{from: 418, to: 631}, -{from: 418, to: 655}, -{from: 418, to: 722}, -{from: 419, to: 424}, -{from: 419, to: 429}, -{from: 419, to: 450}, -{from: 419, to: 451}, -{from: 419, to: 462}, -{from: 419, to: 487}, -{from: 419, to: 489}, -{from: 419, to: 529}, -{from: 419, to: 555}, -{from: 419, to: 569}, -{from: 419, to: 600}, -{from: 419, to: 608}, -{from: 419, to: 642}, -{from: 419, to: 643}, -{from: 419, to: 645}, -{from: 419, to: 720}, -{from: 420, to: 488}, -{from: 420, to: 533}, -{from: 420, to: 579}, -{from: 420, to: 626}, -{from: 420, to: 627}, -{from: 420, to: 662}, -{from: 420, to: 705}, -{from: 421, to: 425}, -{from: 421, to: 440}, -{from: 421, to: 472}, -{from: 421, to: 473}, -{from: 421, to: 508}, -{from: 421, to: 521}, -{from: 421, to: 523}, -{from: 421, to: 543}, -{from: 421, to: 562}, -{from: 421, to: 565}, -{from: 421, to: 589}, -{from: 421, to: 594}, -{from: 421, to: 604}, -{from: 421, to: 649}, -{from: 421, to: 663}, -{from: 421, to: 682}, -{from: 421, to: 728}, -{from: 422, to: 447}, -{from: 422, to: 449}, -{from: 422, to: 452}, -{from: 422, to: 478}, -{from: 422, to: 481}, -{from: 422, to: 482}, -{from: 422, to: 572}, -{from: 422, to: 591}, -{from: 422, to: 622}, -{from: 422, to: 675}, -{from: 422, to: 693}, -{from: 423, to: 545}, -{from: 423, to: 556}, -{from: 423, to: 557}, -{from: 423, to: 558}, -{from: 423, to: 656}, -{from: 423, to: 660}, -{from: 423, to: 674}, -{from: 423, to: 694}, -{from: 423, to: 696}, -{from: 424, to: 450}, -{from: 424, to: 451}, -{from: 424, to: 462}, -{from: 424, to: 487}, -{from: 424, to: 517}, -{from: 424, to: 537}, -{from: 424, to: 555}, -{from: 424, to: 600}, -{from: 424, to: 608}, -{from: 424, to: 636}, -{from: 424, to: 642}, -{from: 424, to: 645}, -{from: 425, to: 440}, -{from: 425, to: 449}, -{from: 425, to: 472}, -{from: 425, to: 473}, -{from: 425, to: 490}, -{from: 425, to: 508}, -{from: 425, to: 521}, -{from: 425, to: 523}, -{from: 425, to: 543}, -{from: 425, to: 562}, -{from: 425, to: 565}, -{from: 425, to: 589}, -{from: 425, to: 594}, -{from: 425, to: 604}, -{from: 425, to: 622}, -{from: 425, to: 663}, -{from: 425, to: 675}, -{from: 425, to: 676}, -{from: 425, to: 728}, -{from: 426, to: 427}, -{from: 426, to: 456}, -{from: 426, to: 464}, -{from: 426, to: 492}, -{from: 426, to: 536}, -{from: 426, to: 549}, -{from: 426, to: 551}, -{from: 426, to: 609}, -{from: 426, to: 615}, -{from: 426, to: 700}, -{from: 426, to: 718}, -{from: 427, to: 456}, -{from: 427, to: 464}, -{from: 427, to: 492}, -{from: 427, to: 536}, -{from: 427, to: 549}, -{from: 427, to: 551}, -{from: 427, to: 609}, -{from: 427, to: 615}, -{from: 427, to: 700}, -{from: 427, to: 718}, -{from: 428, to: 511}, -{from: 428, to: 528}, -{from: 428, to: 532}, -{from: 428, to: 540}, -{from: 428, to: 571}, -{from: 428, to: 580}, -{from: 428, to: 593}, -{from: 428, to: 601}, -{from: 428, to: 618}, -{from: 428, to: 619}, -{from: 428, to: 652}, -{from: 428, to: 703}, -{from: 428, to: 704}, -{from: 428, to: 716}, -{from: 428, to: 732}, -{from: 429, to: 489}, -{from: 429, to: 499}, -{from: 429, to: 529}, -{from: 429, to: 548}, -{from: 429, to: 552}, -{from: 429, to: 569}, -{from: 429, to: 595}, -{from: 429, to: 643}, -{from: 429, to: 710}, -{from: 429, to: 720}, -{from: 430, to: 440}, -{from: 430, to: 461}, -{from: 430, to: 463}, -{from: 430, to: 486}, -{from: 430, to: 531}, -{from: 430, to: 583}, -{from: 430, to: 607}, -{from: 430, to: 634}, -{from: 430, to: 654}, -{from: 430, to: 668}, -{from: 430, to: 702}, -{from: 430, to: 711}, -{from: 431, to: 436}, -{from: 431, to: 443}, -{from: 431, to: 490}, -{from: 431, to: 529}, -{from: 431, to: 547}, -{from: 431, to: 567}, -{from: 431, to: 586}, -{from: 431, to: 602}, -{from: 431, to: 676}, -{from: 431, to: 699}, -{from: 431, to: 703}, -{from: 431, to: 717}, -{from: 432, to: 443}, -{from: 432, to: 444}, -{from: 432, to: 455}, -{from: 432, to: 469}, -{from: 432, to: 514}, -{from: 432, to: 535}, -{from: 432, to: 539}, -{from: 432, to: 542}, -{from: 432, to: 571}, -{from: 432, to: 589}, -{from: 432, to: 623}, -{from: 432, to: 624}, -{from: 432, to: 644}, -{from: 432, to: 653}, -{from: 432, to: 669}, -{from: 432, to: 698}, -{from: 432, to: 719}, -{from: 433, to: 442}, -{from: 433, to: 454}, -{from: 433, to: 475}, -{from: 433, to: 480}, -{from: 433, to: 498}, -{from: 433, to: 517}, -{from: 433, to: 543}, -{from: 433, to: 573}, -{from: 433, to: 577}, -{from: 433, to: 611}, -{from: 433, to: 614}, -{from: 433, to: 623}, -{from: 433, to: 629}, -{from: 433, to: 648}, -{from: 433, to: 678}, -{from: 433, to: 679}, -{from: 433, to: 687}, -{from: 434, to: 491}, -{from: 434, to: 521}, -{from: 434, to: 575}, -{from: 434, to: 576}, -{from: 434, to: 583}, -{from: 434, to: 603}, -{from: 434, to: 616}, -{from: 434, to: 668}, -{from: 434, to: 713}, -{from: 435, to: 493}, -{from: 435, to: 494}, -{from: 435, to: 519}, -{from: 435, to: 525}, -{from: 435, to: 526}, -{from: 435, to: 582}, -{from: 435, to: 585}, -{from: 435, to: 605}, -{from: 435, to: 631}, -{from: 435, to: 655}, -{from: 435, to: 722}, -{from: 436, to: 443}, -{from: 436, to: 490}, -{from: 436, to: 516}, -{from: 436, to: 529}, -{from: 436, to: 547}, -{from: 436, to: 567}, -{from: 436, to: 586}, -{from: 436, to: 676}, -{from: 436, to: 696}, -{from: 436, to: 699}, -{from: 436, to: 717}, -{from: 437, to: 503}, -{from: 437, to: 520}, -{from: 437, to: 590}, -{from: 437, to: 628}, -{from: 437, to: 664}, -{from: 437, to: 670}, -{from: 437, to: 709}, -{from: 438, to: 441}, -{from: 438, to: 465}, -{from: 438, to: 466}, -{from: 438, to: 491}, -{from: 438, to: 646}, -{from: 438, to: 647}, -{from: 438, to: 650}, -{from: 438, to: 651}, -{from: 438, to: 683}, -{from: 438, to: 689}, -{from: 438, to: 724}, -{from: 438, to: 725}, -{from: 438, to: 729}, -{from: 438, to: 730}, -{from: 438, to: 734}, -{from: 438, to: 735}, -{from: 439, to: 540}, -{from: 439, to: 568}, -{from: 439, to: 640}, -{from: 439, to: 641}, -{from: 439, to: 695}, -{from: 439, to: 704}, -{from: 439, to: 708}, -{from: 439, to: 709}, -{from: 439, to: 732}, -{from: 439, to: 733}, -{from: 440, to: 472}, -{from: 440, to: 473}, -{from: 440, to: 508}, -{from: 440, to: 521}, -{from: 440, to: 523}, -{from: 440, to: 543}, -{from: 440, to: 562}, -{from: 440, to: 565}, -{from: 440, to: 583}, -{from: 440, to: 589}, -{from: 440, to: 594}, -{from: 440, to: 604}, -{from: 440, to: 654}, -{from: 440, to: 663}, -{from: 440, to: 668}, -{from: 440, to: 702}, -{from: 440, to: 728}, -{from: 441, to: 465}, -{from: 441, to: 466}, -{from: 441, to: 491}, -{from: 441, to: 646}, -{from: 441, to: 647}, -{from: 441, to: 650}, -{from: 441, to: 651}, -{from: 441, to: 689}, -{from: 441, to: 724}, -{from: 441, to: 725}, -{from: 441, to: 729}, -{from: 441, to: 730}, -{from: 441, to: 734}, -{from: 441, to: 735}, -{from: 442, to: 454}, -{from: 442, to: 455}, -{from: 442, to: 475}, -{from: 442, to: 480}, -{from: 442, to: 498}, -{from: 442, to: 517}, -{from: 442, to: 573}, -{from: 442, to: 577}, -{from: 442, to: 611}, -{from: 442, to: 614}, -{from: 442, to: 623}, -{from: 442, to: 648}, -{from: 442, to: 678}, -{from: 442, to: 687}, -{from: 442, to: 721}, -{from: 443, to: 490}, -{from: 443, to: 529}, -{from: 443, to: 547}, -{from: 443, to: 567}, -{from: 443, to: 571}, -{from: 443, to: 586}, -{from: 443, to: 589}, -{from: 443, to: 623}, -{from: 443, to: 644}, -{from: 443, to: 676}, -{from: 443, to: 699}, -{from: 443, to: 717}, -{from: 443, to: 719}, -{from: 444, to: 455}, -{from: 444, to: 469}, -{from: 444, to: 514}, -{from: 444, to: 535}, -{from: 444, to: 539}, -{from: 444, to: 542}, -{from: 444, to: 624}, -{from: 444, to: 653}, -{from: 444, to: 669}, -{from: 444, to: 698}, -{from: 445, to: 483}, -{from: 445, to: 484}, -{from: 445, to: 512}, -{from: 445, to: 638}, -{from: 445, to: 692}, -{from: 445, to: 723}, -{from: 446, to: 509}, -{from: 446, to: 510}, -{from: 446, to: 546}, -{from: 446, to: 564}, -{from: 446, to: 581}, -{from: 446, to: 592}, -{from: 447, to: 449}, -{from: 447, to: 452}, -{from: 447, to: 478}, -{from: 447, to: 481}, -{from: 447, to: 482}, -{from: 447, to: 622}, -{from: 447, to: 675}, -{from: 447, to: 711}, -{from: 448, to: 501}, -{from: 448, to: 506}, -{from: 448, to: 520}, -{from: 448, to: 550}, -{from: 448, to: 563}, -{from: 448, to: 588}, -{from: 448, to: 617}, -{from: 448, to: 712}, -{from: 448, to: 727}, -{from: 449, to: 452}, -{from: 449, to: 478}, -{from: 449, to: 481}, -{from: 449, to: 482}, -{from: 449, to: 490}, -{from: 449, to: 622}, -{from: 449, to: 675}, -{from: 449, to: 676}, -{from: 449, to: 728}, -{from: 450, to: 451}, -{from: 450, to: 462}, -{from: 450, to: 487}, -{from: 450, to: 555}, -{from: 450, to: 600}, -{from: 450, to: 608}, -{from: 450, to: 619}, -{from: 450, to: 642}, -{from: 450, to: 645}, -{from: 450, to: 716}, -{from: 451, to: 462}, -{from: 451, to: 487}, -{from: 451, to: 555}, -{from: 451, to: 600}, -{from: 451, to: 608}, -{from: 451, to: 642}, -{from: 451, to: 645}, -{from: 452, to: 478}, -{from: 452, to: 481}, -{from: 452, to: 482}, -{from: 452, to: 503}, -{from: 452, to: 534}, -{from: 452, to: 622}, -{from: 452, to: 670}, -{from: 452, to: 675}, -{from: 453, to: 504}, -{from: 453, to: 578}, -{from: 453, to: 596}, -{from: 453, to: 602}, -{from: 453, to: 610}, -{from: 453, to: 661}, -{from: 453, to: 665}, -{from: 453, to: 690}, -{from: 453, to: 692}, -{from: 453, to: 693}, -{from: 453, to: 721}, -{from: 453, to: 723}, -{from: 454, to: 455}, -{from: 454, to: 475}, -{from: 454, to: 480}, -{from: 454, to: 498}, -{from: 454, to: 517}, -{from: 454, to: 573}, -{from: 454, to: 577}, -{from: 454, to: 611}, -{from: 454, to: 614}, -{from: 454, to: 623}, -{from: 454, to: 648}, -{from: 454, to: 678}, -{from: 454, to: 687}, -{from: 454, to: 721}, -{from: 455, to: 469}, -{from: 455, to: 514}, -{from: 455, to: 535}, -{from: 455, to: 539}, -{from: 455, to: 542}, -{from: 455, to: 577}, -{from: 455, to: 624}, -{from: 455, to: 653}, -{from: 455, to: 669}, -{from: 455, to: 678}, -{from: 455, to: 687}, -{from: 455, to: 698}, -{from: 455, to: 721}, -{from: 456, to: 464}, -{from: 456, to: 492}, -{from: 456, to: 536}, -{from: 456, to: 549}, -{from: 456, to: 551}, -{from: 456, to: 609}, -{from: 456, to: 615}, -{from: 456, to: 700}, -{from: 456, to: 718}, -{from: 457, to: 554}, -{from: 457, to: 630}, -{from: 457, to: 672}, -{from: 457, to: 701}, -{from: 458, to: 459}, -{from: 458, to: 468}, -{from: 458, to: 470}, -{from: 458, to: 471}, -{from: 458, to: 477}, -{from: 458, to: 479}, -{from: 458, to: 515}, -{from: 458, to: 518}, -{from: 458, to: 541}, -{from: 458, to: 620}, -{from: 458, to: 680}, -{from: 458, to: 686}, -{from: 459, to: 468}, -{from: 459, to: 470}, -{from: 459, to: 471}, -{from: 459, to: 477}, -{from: 459, to: 479}, -{from: 459, to: 515}, -{from: 459, to: 518}, -{from: 459, to: 541}, -{from: 459, to: 620}, -{from: 459, to: 680}, -{from: 459, to: 686}, -{from: 460, to: 522}, -{from: 460, to: 527}, -{from: 460, to: 528}, -{from: 460, to: 532}, -{from: 460, to: 562}, -{from: 460, to: 576}, -{from: 460, to: 606}, -{from: 460, to: 612}, -{from: 460, to: 625}, -{from: 460, to: 646}, -{from: 460, to: 654}, -{from: 460, to: 667}, -{from: 460, to: 677}, -{from: 460, to: 679}, -{from: 460, to: 685}, -{from: 460, to: 707}, -{from: 460, to: 713}, -{from: 461, to: 463}, -{from: 461, to: 483}, -{from: 461, to: 486}, -{from: 461, to: 531}, -{from: 461, to: 565}, -{from: 461, to: 607}, -{from: 461, to: 634}, -{from: 461, to: 661}, -{from: 461, to: 711}, -{from: 462, to: 487}, -{from: 462, to: 555}, -{from: 462, to: 600}, -{from: 462, to: 608}, -{from: 462, to: 642}, -{from: 462, to: 645}, -{from: 462, to: 707}, -{from: 462, to: 726}, -{from: 463, to: 486}, -{from: 463, to: 531}, -{from: 463, to: 607}, -{from: 463, to: 634}, -{from: 463, to: 711}, -{from: 464, to: 492}, -{from: 464, to: 536}, -{from: 464, to: 549}, -{from: 464, to: 551}, -{from: 464, to: 552}, -{from: 464, to: 609}, -{from: 464, to: 615}, -{from: 464, to: 700}, -{from: 464, to: 718}, -{from: 465, to: 466}, -{from: 465, to: 491}, -{from: 465, to: 646}, -{from: 465, to: 647}, -{from: 465, to: 650}, -{from: 465, to: 651}, -{from: 465, to: 689}, -{from: 465, to: 724}, -{from: 465, to: 725}, -{from: 465, to: 729}, -{from: 465, to: 730}, -{from: 465, to: 734}, -{from: 465, to: 735}, -{from: 466, to: 491}, -{from: 466, to: 646}, -{from: 466, to: 647}, -{from: 466, to: 650}, -{from: 466, to: 651}, -{from: 466, to: 689}, -{from: 466, to: 724}, -{from: 466, to: 725}, -{from: 466, to: 729}, -{from: 466, to: 730}, -{from: 466, to: 734}, -{from: 466, to: 735}, -{from: 467, to: 495}, -{from: 467, to: 570}, -{from: 467, to: 584}, -{from: 467, to: 598}, -{from: 467, to: 599}, -{from: 467, to: 666}, -{from: 468, to: 470}, -{from: 468, to: 471}, -{from: 468, to: 477}, -{from: 468, to: 479}, -{from: 468, to: 515}, -{from: 468, to: 518}, -{from: 468, to: 541}, -{from: 468, to: 620}, -{from: 468, to: 680}, -{from: 468, to: 686}, -{from: 469, to: 514}, -{from: 469, to: 535}, -{from: 469, to: 539}, -{from: 469, to: 542}, -{from: 469, to: 600}, -{from: 469, to: 608}, -{from: 469, to: 624}, -{from: 469, to: 631}, -{from: 469, to: 653}, -{from: 469, to: 669}, -{from: 469, to: 698}, -{from: 469, to: 734}, -{from: 470, to: 471}, -{from: 470, to: 477}, -{from: 470, to: 479}, -{from: 470, to: 515}, -{from: 470, to: 518}, -{from: 470, to: 541}, -{from: 470, to: 620}, -{from: 470, to: 680}, -{from: 470, to: 686}, -{from: 471, to: 477}, -{from: 471, to: 479}, -{from: 471, to: 515}, -{from: 471, to: 518}, -{from: 471, to: 541}, -{from: 471, to: 550}, -{from: 471, to: 620}, -{from: 471, to: 680}, -{from: 471, to: 686}, -{from: 472, to: 473}, -{from: 472, to: 508}, -{from: 472, to: 521}, -{from: 472, to: 523}, -{from: 472, to: 543}, -{from: 472, to: 562}, -{from: 472, to: 565}, -{from: 472, to: 589}, -{from: 472, to: 594}, -{from: 472, to: 604}, -{from: 472, to: 649}, -{from: 472, to: 663}, -{from: 472, to: 682}, -{from: 472, to: 728}, -{from: 473, to: 508}, -{from: 473, to: 521}, -{from: 473, to: 523}, -{from: 473, to: 533}, -{from: 473, to: 543}, -{from: 473, to: 562}, -{from: 473, to: 565}, -{from: 473, to: 589}, -{from: 473, to: 594}, -{from: 473, to: 604}, -{from: 473, to: 663}, -{from: 473, to: 728}, -{from: 474, to: 485}, -{from: 474, to: 553}, -{from: 474, to: 621}, -{from: 474, to: 632}, -{from: 474, to: 638}, -{from: 474, to: 639}, -{from: 474, to: 657}, -{from: 474, to: 671}, -{from: 474, to: 715}, -{from: 474, to: 726}, -{from: 475, to: 480}, -{from: 475, to: 498}, -{from: 475, to: 517}, -{from: 475, to: 518}, -{from: 475, to: 573}, -{from: 475, to: 577}, -{from: 475, to: 611}, -{from: 475, to: 614}, -{from: 475, to: 623}, -{from: 475, to: 648}, -{from: 475, to: 656}, -{from: 475, to: 678}, -{from: 475, to: 687}, -{from: 476, to: 502}, -{from: 476, to: 513}, -{from: 476, to: 530}, -{from: 476, to: 544}, -{from: 476, to: 681}, -{from: 476, to: 683}, -{from: 477, to: 479}, -{from: 477, to: 515}, -{from: 477, to: 518}, -{from: 477, to: 541}, -{from: 477, to: 620}, -{from: 477, to: 680}, -{from: 477, to: 686}, -{from: 478, to: 481}, -{from: 478, to: 482}, -{from: 478, to: 558}, -{from: 478, to: 622}, -{from: 478, to: 675}, -{from: 479, to: 515}, -{from: 479, to: 518}, -{from: 479, to: 541}, -{from: 479, to: 620}, -{from: 479, to: 680}, -{from: 479, to: 686}, -{from: 480, to: 497}, -{from: 480, to: 498}, -{from: 480, to: 517}, -{from: 480, to: 573}, -{from: 480, to: 577}, -{from: 480, to: 611}, -{from: 480, to: 614}, -{from: 480, to: 623}, -{from: 480, to: 648}, -{from: 480, to: 678}, -{from: 480, to: 687}, -{from: 481, to: 482}, -{from: 481, to: 503}, -{from: 481, to: 534}, -{from: 481, to: 622}, -{from: 481, to: 670}, -{from: 481, to: 675}, -{from: 482, to: 622}, -{from: 482, to: 653}, -{from: 482, to: 675}, -{from: 483, to: 484}, -{from: 483, to: 512}, -{from: 483, to: 565}, -{from: 483, to: 661}, -{from: 484, to: 512}, -{from: 484, to: 701}, -{from: 485, to: 553}, -{from: 485, to: 621}, -{from: 485, to: 632}, -{from: 485, to: 638}, -{from: 485, to: 639}, -{from: 485, to: 657}, -{from: 485, to: 663}, -{from: 485, to: 671}, -{from: 485, to: 715}, -{from: 485, to: 726}, -{from: 486, to: 531}, -{from: 486, to: 607}, -{from: 486, to: 618}, -{from: 486, to: 634}, -{from: 486, to: 711}, -{from: 487, to: 555}, -{from: 487, to: 600}, -{from: 487, to: 608}, -{from: 487, to: 642}, -{from: 487, to: 645}, -{from: 488, to: 533}, -{from: 488, to: 561}, -{from: 488, to: 579}, -{from: 488, to: 626}, -{from: 488, to: 627}, -{from: 488, to: 647}, -{from: 488, to: 662}, -{from: 488, to: 705}, -{from: 489, to: 499}, -{from: 489, to: 529}, -{from: 489, to: 548}, -{from: 489, to: 552}, -{from: 489, to: 569}, -{from: 489, to: 595}, -{from: 489, to: 643}, -{from: 489, to: 710}, -{from: 489, to: 720}, -{from: 490, to: 529}, -{from: 490, to: 547}, -{from: 490, to: 567}, -{from: 490, to: 586}, -{from: 490, to: 622}, -{from: 490, to: 675}, -{from: 490, to: 676}, -{from: 490, to: 699}, -{from: 490, to: 717}, -{from: 490, to: 728}, -{from: 491, to: 521}, -{from: 491, to: 603}, -{from: 491, to: 646}, -{from: 491, to: 647}, -{from: 491, to: 650}, -{from: 491, to: 651}, -{from: 491, to: 689}, -{from: 491, to: 724}, -{from: 491, to: 725}, -{from: 491, to: 729}, -{from: 491, to: 730}, -{from: 491, to: 734}, -{from: 491, to: 735}, -{from: 492, to: 535}, -{from: 492, to: 536}, -{from: 492, to: 549}, -{from: 492, to: 551}, -{from: 492, to: 609}, -{from: 492, to: 615}, -{from: 492, to: 700}, -{from: 492, to: 718}, -{from: 493, to: 494}, -{from: 493, to: 519}, -{from: 493, to: 525}, -{from: 493, to: 526}, -{from: 493, to: 582}, -{from: 493, to: 585}, -{from: 493, to: 605}, -{from: 493, to: 624}, -{from: 493, to: 631}, -{from: 493, to: 655}, -{from: 493, to: 722}, -{from: 494, to: 519}, -{from: 494, to: 525}, -{from: 494, to: 526}, -{from: 494, to: 582}, -{from: 494, to: 585}, -{from: 494, to: 605}, -{from: 494, to: 631}, -{from: 494, to: 655}, -{from: 494, to: 722}, -{from: 495, to: 570}, -{from: 495, to: 584}, -{from: 495, to: 598}, -{from: 495, to: 599}, -{from: 495, to: 666}, -{from: 496, to: 507}, -{from: 496, to: 534}, -{from: 496, to: 566}, -{from: 496, to: 606}, -{from: 496, to: 613}, -{from: 496, to: 630}, -{from: 496, to: 659}, -{from: 496, to: 673}, -{from: 496, to: 682}, -{from: 496, to: 714}, -{from: 497, to: 516}, -{from: 497, to: 524}, -{from: 497, to: 538}, -{from: 497, to: 633}, -{from: 497, to: 635}, -{from: 497, to: 636}, -{from: 497, to: 637}, -{from: 497, to: 684}, -{from: 497, to: 688}, -{from: 497, to: 697}, -{from: 497, to: 736}, -{from: 498, to: 517}, -{from: 498, to: 543}, -{from: 498, to: 573}, -{from: 498, to: 577}, -{from: 498, to: 611}, -{from: 498, to: 614}, -{from: 498, to: 623}, -{from: 498, to: 629}, -{from: 498, to: 648}, -{from: 498, to: 678}, -{from: 498, to: 679}, -{from: 498, to: 687}, -{from: 499, to: 548}, -{from: 499, to: 552}, -{from: 499, to: 595}, -{from: 499, to: 710}, -{from: 500, to: 505}, -{from: 500, to: 537}, -{from: 500, to: 574}, -{from: 500, to: 587}, -{from: 500, to: 597}, -{from: 500, to: 649}, -{from: 500, to: 691}, -{from: 500, to: 702}, -{from: 500, to: 706}, -{from: 501, to: 506}, -{from: 501, to: 550}, -{from: 501, to: 563}, -{from: 501, to: 588}, -{from: 501, to: 617}, -{from: 501, to: 712}, -{from: 501, to: 727}, -{from: 502, to: 513}, -{from: 502, to: 530}, -{from: 502, to: 544}, -{from: 502, to: 681}, -{from: 502, to: 683}, -{from: 503, to: 520}, -{from: 503, to: 534}, -{from: 503, to: 590}, -{from: 503, to: 628}, -{from: 503, to: 664}, -{from: 503, to: 670}, -{from: 503, to: 709}, -{from: 504, to: 578}, -{from: 504, to: 596}, -{from: 504, to: 602}, -{from: 504, to: 610}, -{from: 504, to: 661}, -{from: 504, to: 665}, -{from: 504, to: 690}, -{from: 504, to: 692}, -{from: 504, to: 693}, -{from: 504, to: 721}, -{from: 504, to: 723}, -{from: 505, to: 537}, -{from: 505, to: 574}, -{from: 505, to: 587}, -{from: 505, to: 597}, -{from: 505, to: 649}, -{from: 505, to: 691}, -{from: 505, to: 702}, -{from: 505, to: 706}, -{from: 506, to: 550}, -{from: 506, to: 563}, -{from: 506, to: 588}, -{from: 506, to: 617}, -{from: 506, to: 712}, -{from: 506, to: 727}, -{from: 507, to: 534}, -{from: 507, to: 566}, -{from: 507, to: 606}, -{from: 507, to: 613}, -{from: 507, to: 659}, -{from: 507, to: 673}, -{from: 507, to: 682}, -{from: 507, to: 714}, -{from: 507, to: 715}, -{from: 508, to: 521}, -{from: 508, to: 523}, -{from: 508, to: 543}, -{from: 508, to: 562}, -{from: 508, to: 565}, -{from: 508, to: 589}, -{from: 508, to: 594}, -{from: 508, to: 604}, -{from: 508, to: 663}, -{from: 508, to: 728}, -{from: 509, to: 510}, -{from: 509, to: 546}, -{from: 509, to: 564}, -{from: 509, to: 581}, -{from: 509, to: 592}, -{from: 510, to: 546}, -{from: 510, to: 564}, -{from: 510, to: 581}, -{from: 510, to: 592}, -{from: 511, to: 528}, -{from: 511, to: 539}, -{from: 511, to: 571}, -{from: 511, to: 580}, -{from: 511, to: 593}, -{from: 511, to: 601}, -{from: 511, to: 618}, -{from: 511, to: 619}, -{from: 511, to: 652}, -{from: 511, to: 703}, -{from: 511, to: 716}, -{from: 513, to: 530}, -{from: 513, to: 544}, -{from: 513, to: 681}, -{from: 513, to: 683}, -{from: 514, to: 535}, -{from: 514, to: 539}, -{from: 514, to: 542}, -{from: 514, to: 624}, -{from: 514, to: 653}, -{from: 514, to: 669}, -{from: 514, to: 698}, -{from: 515, to: 518}, -{from: 515, to: 541}, -{from: 515, to: 620}, -{from: 515, to: 680}, -{from: 515, to: 686}, -{from: 516, to: 524}, -{from: 516, to: 538}, -{from: 516, to: 633}, -{from: 516, to: 635}, -{from: 516, to: 636}, -{from: 516, to: 637}, -{from: 516, to: 684}, -{from: 516, to: 688}, -{from: 516, to: 696}, -{from: 516, to: 697}, -{from: 516, to: 736}, -{from: 517, to: 537}, -{from: 517, to: 573}, -{from: 517, to: 577}, -{from: 517, to: 611}, -{from: 517, to: 614}, -{from: 517, to: 623}, -{from: 517, to: 636}, -{from: 517, to: 648}, -{from: 517, to: 678}, -{from: 517, to: 687}, -{from: 518, to: 541}, -{from: 518, to: 611}, -{from: 518, to: 620}, -{from: 518, to: 656}, -{from: 518, to: 680}, -{from: 518, to: 686}, -{from: 519, to: 525}, -{from: 519, to: 526}, -{from: 519, to: 582}, -{from: 519, to: 585}, -{from: 519, to: 605}, -{from: 519, to: 631}, -{from: 519, to: 655}, -{from: 519, to: 722}, -{from: 520, to: 590}, -{from: 520, to: 628}, -{from: 520, to: 664}, -{from: 520, to: 670}, -{from: 520, to: 709}, -{from: 521, to: 523}, -{from: 521, to: 543}, -{from: 521, to: 562}, -{from: 521, to: 565}, -{from: 521, to: 589}, -{from: 521, to: 594}, -{from: 521, to: 603}, -{from: 521, to: 604}, -{from: 521, to: 663}, -{from: 521, to: 728}, -{from: 522, to: 525}, -{from: 522, to: 527}, -{from: 522, to: 532}, -{from: 522, to: 567}, -{from: 522, to: 612}, -{from: 522, to: 625}, -{from: 522, to: 654}, -{from: 522, to: 667}, -{from: 522, to: 677}, -{from: 522, to: 679}, -{from: 522, to: 685}, -{from: 522, to: 707}, -{from: 523, to: 543}, -{from: 523, to: 562}, -{from: 523, to: 565}, -{from: 523, to: 589}, -{from: 523, to: 594}, -{from: 523, to: 604}, -{from: 523, to: 649}, -{from: 523, to: 663}, -{from: 523, to: 682}, -{from: 523, to: 728}, -{from: 524, to: 538}, -{from: 524, to: 633}, -{from: 524, to: 635}, -{from: 524, to: 636}, -{from: 524, to: 637}, -{from: 524, to: 684}, -{from: 524, to: 688}, -{from: 524, to: 697}, -{from: 524, to: 736}, -{from: 525, to: 526}, -{from: 525, to: 527}, -{from: 525, to: 567}, -{from: 525, to: 582}, -{from: 525, to: 585}, -{from: 525, to: 605}, -{from: 525, to: 631}, -{from: 525, to: 655}, -{from: 525, to: 722}, -{from: 526, to: 582}, -{from: 526, to: 585}, -{from: 526, to: 605}, -{from: 526, to: 631}, -{from: 526, to: 652}, -{from: 526, to: 655}, -{from: 526, to: 667}, -{from: 526, to: 722}, -{from: 527, to: 532}, -{from: 527, to: 567}, -{from: 527, to: 612}, -{from: 527, to: 625}, -{from: 527, to: 654}, -{from: 527, to: 667}, -{from: 527, to: 677}, -{from: 527, to: 679}, -{from: 527, to: 685}, -{from: 527, to: 707}, -{from: 528, to: 562}, -{from: 528, to: 571}, -{from: 528, to: 576}, -{from: 528, to: 580}, -{from: 528, to: 593}, -{from: 528, to: 601}, -{from: 528, to: 606}, -{from: 528, to: 618}, -{from: 528, to: 619}, -{from: 528, to: 646}, -{from: 528, to: 652}, -{from: 528, to: 703}, -{from: 528, to: 713}, -{from: 528, to: 716}, -{from: 529, to: 547}, -{from: 529, to: 567}, -{from: 529, to: 569}, -{from: 529, to: 586}, -{from: 529, to: 643}, -{from: 529, to: 676}, -{from: 529, to: 699}, -{from: 529, to: 717}, -{from: 529, to: 720}, -{from: 530, to: 544}, -{from: 530, to: 681}, -{from: 530, to: 683}, -{from: 531, to: 607}, -{from: 531, to: 634}, -{from: 531, to: 711}, -{from: 532, to: 540}, -{from: 532, to: 612}, -{from: 532, to: 625}, -{from: 532, to: 654}, -{from: 532, to: 667}, -{from: 532, to: 677}, -{from: 532, to: 679}, -{from: 532, to: 685}, -{from: 532, to: 704}, -{from: 532, to: 707}, -{from: 532, to: 732}, -{from: 533, to: 579}, -{from: 533, to: 626}, -{from: 533, to: 627}, -{from: 533, to: 662}, -{from: 533, to: 705}, -{from: 534, to: 566}, -{from: 534, to: 606}, -{from: 534, to: 613}, -{from: 534, to: 659}, -{from: 534, to: 670}, -{from: 534, to: 673}, -{from: 534, to: 682}, -{from: 534, to: 714}, -{from: 535, to: 539}, -{from: 535, to: 542}, -{from: 535, to: 624}, -{from: 535, to: 653}, -{from: 535, to: 669}, -{from: 535, to: 698}, -{from: 536, to: 549}, -{from: 536, to: 551}, -{from: 536, to: 609}, -{from: 536, to: 615}, -{from: 536, to: 700}, -{from: 536, to: 718}, -{from: 537, to: 574}, -{from: 537, to: 587}, -{from: 537, to: 597}, -{from: 537, to: 636}, -{from: 537, to: 649}, -{from: 537, to: 691}, -{from: 537, to: 702}, -{from: 537, to: 706}, -{from: 538, to: 633}, -{from: 538, to: 635}, -{from: 538, to: 636}, -{from: 538, to: 637}, -{from: 538, to: 684}, -{from: 538, to: 688}, -{from: 538, to: 697}, -{from: 538, to: 736}, -{from: 539, to: 542}, -{from: 539, to: 624}, -{from: 539, to: 653}, -{from: 539, to: 669}, -{from: 539, to: 698}, -{from: 540, to: 568}, -{from: 540, to: 640}, -{from: 540, to: 641}, -{from: 540, to: 695}, -{from: 540, to: 704}, -{from: 540, to: 708}, -{from: 540, to: 732}, -{from: 540, to: 733}, -{from: 541, to: 620}, -{from: 541, to: 680}, -{from: 541, to: 686}, -{from: 542, to: 624}, -{from: 542, to: 653}, -{from: 542, to: 669}, -{from: 542, to: 698}, -{from: 543, to: 562}, -{from: 543, to: 565}, -{from: 543, to: 573}, -{from: 543, to: 589}, -{from: 543, to: 594}, -{from: 543, to: 604}, -{from: 543, to: 629}, -{from: 543, to: 663}, -{from: 543, to: 679}, -{from: 543, to: 728}, -{from: 544, to: 681}, -{from: 544, to: 683}, -{from: 545, to: 556}, -{from: 545, to: 557}, -{from: 545, to: 558}, -{from: 545, to: 656}, -{from: 545, to: 660}, -{from: 545, to: 674}, -{from: 545, to: 694}, -{from: 545, to: 696}, -{from: 545, to: 722}, -{from: 546, to: 564}, -{from: 546, to: 581}, -{from: 546, to: 592}, -{from: 547, to: 567}, -{from: 547, to: 586}, -{from: 547, to: 627}, -{from: 547, to: 676}, -{from: 547, to: 699}, -{from: 547, to: 717}, -{from: 548, to: 552}, -{from: 548, to: 595}, -{from: 548, to: 710}, -{from: 549, to: 551}, -{from: 549, to: 609}, -{from: 549, to: 615}, -{from: 549, to: 700}, -{from: 549, to: 718}, -{from: 550, to: 563}, -{from: 550, to: 588}, -{from: 550, to: 617}, -{from: 550, to: 712}, -{from: 550, to: 727}, -{from: 551, to: 609}, -{from: 551, to: 615}, -{from: 551, to: 700}, -{from: 551, to: 718}, -{from: 552, to: 595}, -{from: 552, to: 710}, -{from: 553, to: 621}, -{from: 553, to: 632}, -{from: 553, to: 638}, -{from: 553, to: 639}, -{from: 553, to: 657}, -{from: 553, to: 671}, -{from: 553, to: 715}, -{from: 553, to: 726}, -{from: 554, to: 630}, -{from: 554, to: 672}, -{from: 554, to: 701}, -{from: 555, to: 600}, -{from: 555, to: 608}, -{from: 555, to: 642}, -{from: 555, to: 645}, -{from: 555, to: 707}, -{from: 555, to: 726}, -{from: 556, to: 557}, -{from: 556, to: 558}, -{from: 556, to: 656}, -{from: 556, to: 660}, -{from: 556, to: 674}, -{from: 556, to: 694}, -{from: 556, to: 696}, -{from: 557, to: 558}, -{from: 557, to: 656}, -{from: 557, to: 660}, -{from: 557, to: 674}, -{from: 557, to: 694}, -{from: 557, to: 696}, -{from: 558, to: 656}, -{from: 558, to: 660}, -{from: 558, to: 674}, -{from: 558, to: 694}, -{from: 558, to: 696}, -{from: 559, to: 560}, -{from: 559, to: 561}, -{from: 559, to: 658}, -{from: 559, to: 731}, -{from: 560, to: 561}, -{from: 560, to: 658}, -{from: 560, to: 731}, -{from: 561, to: 647}, -{from: 561, to: 658}, -{from: 561, to: 731}, -{from: 562, to: 565}, -{from: 562, to: 576}, -{from: 562, to: 589}, -{from: 562, to: 594}, -{from: 562, to: 604}, -{from: 562, to: 606}, -{from: 562, to: 646}, -{from: 562, to: 663}, -{from: 562, to: 713}, -{from: 562, to: 728}, -{from: 563, to: 588}, -{from: 563, to: 617}, -{from: 563, to: 712}, -{from: 563, to: 727}, -{from: 564, to: 581}, -{from: 564, to: 592}, -{from: 565, to: 589}, -{from: 565, to: 594}, -{from: 565, to: 604}, -{from: 565, to: 661}, -{from: 565, to: 663}, -{from: 565, to: 728}, -{from: 566, to: 606}, -{from: 566, to: 613}, -{from: 566, to: 659}, -{from: 566, to: 673}, -{from: 566, to: 682}, -{from: 566, to: 714}, -{from: 567, to: 586}, -{from: 567, to: 676}, -{from: 567, to: 699}, -{from: 567, to: 717}, -{from: 568, to: 640}, -{from: 568, to: 641}, -{from: 568, to: 695}, -{from: 568, to: 704}, -{from: 568, to: 708}, -{from: 568, to: 709}, -{from: 568, to: 732}, -{from: 568, to: 733}, -{from: 569, to: 572}, -{from: 569, to: 591}, -{from: 569, to: 629}, -{from: 569, to: 643}, -{from: 569, to: 644}, -{from: 569, to: 719}, -{from: 569, to: 720}, -{from: 570, to: 584}, -{from: 570, to: 598}, -{from: 570, to: 599}, -{from: 570, to: 666}, -{from: 571, to: 580}, -{from: 571, to: 589}, -{from: 571, to: 593}, -{from: 571, to: 601}, -{from: 571, to: 618}, -{from: 571, to: 619}, -{from: 571, to: 623}, -{from: 571, to: 644}, -{from: 571, to: 652}, -{from: 571, to: 703}, -{from: 571, to: 716}, -{from: 571, to: 719}, -{from: 572, to: 591}, -{from: 572, to: 629}, -{from: 572, to: 643}, -{from: 572, to: 644}, -{from: 572, to: 693}, -{from: 572, to: 719}, -{from: 572, to: 720}, -{from: 573, to: 577}, -{from: 573, to: 611}, -{from: 573, to: 614}, -{from: 573, to: 623}, -{from: 573, to: 629}, -{from: 573, to: 648}, -{from: 573, to: 678}, -{from: 573, to: 679}, -{from: 573, to: 687}, -{from: 574, to: 587}, -{from: 574, to: 597}, -{from: 574, to: 649}, -{from: 574, to: 691}, -{from: 574, to: 702}, -{from: 574, to: 706}, -{from: 574, to: 718}, -{from: 575, to: 576}, -{from: 575, to: 583}, -{from: 575, to: 603}, -{from: 575, to: 612}, -{from: 575, to: 616}, -{from: 575, to: 668}, -{from: 575, to: 681}, -{from: 575, to: 713}, -{from: 576, to: 583}, -{from: 576, to: 603}, -{from: 576, to: 606}, -{from: 576, to: 616}, -{from: 576, to: 646}, -{from: 576, to: 668}, -{from: 576, to: 713}, -{from: 577, to: 611}, -{from: 577, to: 614}, -{from: 577, to: 623}, -{from: 577, to: 648}, -{from: 577, to: 678}, -{from: 577, to: 687}, -{from: 577, to: 721}, -{from: 578, to: 596}, -{from: 578, to: 601}, -{from: 578, to: 602}, -{from: 578, to: 610}, -{from: 578, to: 655}, -{from: 578, to: 661}, -{from: 578, to: 665}, -{from: 578, to: 690}, -{from: 578, to: 692}, -{from: 578, to: 693}, -{from: 578, to: 721}, -{from: 578, to: 723}, -{from: 579, to: 593}, -{from: 579, to: 626}, -{from: 579, to: 627}, -{from: 579, to: 662}, -{from: 579, to: 705}, -{from: 580, to: 593}, -{from: 580, to: 601}, -{from: 580, to: 618}, -{from: 580, to: 619}, -{from: 580, to: 652}, -{from: 580, to: 703}, -{from: 580, to: 716}, -{from: 581, to: 592}, -{from: 582, to: 585}, -{from: 582, to: 605}, -{from: 582, to: 631}, -{from: 582, to: 655}, -{from: 582, to: 722}, -{from: 583, to: 603}, -{from: 583, to: 616}, -{from: 583, to: 654}, -{from: 583, to: 668}, -{from: 583, to: 702}, -{from: 583, to: 713}, -{from: 584, to: 598}, -{from: 584, to: 599}, -{from: 584, to: 666}, -{from: 585, to: 605}, -{from: 585, to: 631}, -{from: 585, to: 655}, -{from: 585, to: 722}, -{from: 586, to: 627}, -{from: 586, to: 676}, -{from: 586, to: 699}, -{from: 586, to: 717}, -{from: 587, to: 597}, -{from: 587, to: 649}, -{from: 587, to: 691}, -{from: 587, to: 702}, -{from: 587, to: 706}, -{from: 588, to: 617}, -{from: 588, to: 712}, -{from: 588, to: 727}, -{from: 589, to: 594}, -{from: 589, to: 604}, -{from: 589, to: 623}, -{from: 589, to: 644}, -{from: 589, to: 663}, -{from: 589, to: 719}, -{from: 589, to: 728}, -{from: 590, to: 628}, -{from: 590, to: 664}, -{from: 590, to: 670}, -{from: 590, to: 709}, -{from: 591, to: 629}, -{from: 591, to: 643}, -{from: 591, to: 644}, -{from: 591, to: 693}, -{from: 591, to: 719}, -{from: 591, to: 720}, -{from: 593, to: 601}, -{from: 593, to: 618}, -{from: 593, to: 619}, -{from: 593, to: 652}, -{from: 593, to: 703}, -{from: 593, to: 716}, -{from: 594, to: 604}, -{from: 594, to: 663}, -{from: 594, to: 728}, -{from: 595, to: 710}, -{from: 596, to: 602}, -{from: 596, to: 610}, -{from: 596, to: 661}, -{from: 596, to: 665}, -{from: 596, to: 690}, -{from: 596, to: 692}, -{from: 596, to: 693}, -{from: 596, to: 721}, -{from: 596, to: 723}, -{from: 597, to: 649}, -{from: 597, to: 691}, -{from: 597, to: 702}, -{from: 597, to: 706}, -{from: 598, to: 599}, -{from: 598, to: 666}, -{from: 599, to: 666}, -{from: 600, to: 608}, -{from: 600, to: 631}, -{from: 600, to: 642}, -{from: 600, to: 645}, -{from: 600, to: 734}, -{from: 601, to: 618}, -{from: 601, to: 619}, -{from: 601, to: 652}, -{from: 601, to: 655}, -{from: 601, to: 703}, -{from: 601, to: 716}, -{from: 602, to: 610}, -{from: 602, to: 661}, -{from: 602, to: 665}, -{from: 602, to: 690}, -{from: 602, to: 692}, -{from: 602, to: 693}, -{from: 602, to: 703}, -{from: 602, to: 721}, -{from: 602, to: 723}, -{from: 603, to: 616}, -{from: 603, to: 668}, -{from: 603, to: 713}, -{from: 604, to: 621}, -{from: 604, to: 663}, -{from: 604, to: 706}, -{from: 604, to: 728}, -{from: 605, to: 631}, -{from: 605, to: 655}, -{from: 605, to: 722}, -{from: 606, to: 613}, -{from: 606, to: 646}, -{from: 606, to: 659}, -{from: 606, to: 673}, -{from: 606, to: 682}, -{from: 606, to: 713}, -{from: 606, to: 714}, -{from: 607, to: 634}, -{from: 607, to: 711}, -{from: 608, to: 631}, -{from: 608, to: 642}, -{from: 608, to: 645}, -{from: 608, to: 734}, -{from: 609, to: 615}, -{from: 609, to: 700}, -{from: 609, to: 718}, -{from: 610, to: 661}, -{from: 610, to: 665}, -{from: 610, to: 690}, -{from: 610, to: 692}, -{from: 610, to: 693}, -{from: 610, to: 721}, -{from: 610, to: 723}, -{from: 611, to: 614}, -{from: 611, to: 623}, -{from: 611, to: 648}, -{from: 611, to: 656}, -{from: 611, to: 678}, -{from: 611, to: 687}, -{from: 612, to: 616}, -{from: 612, to: 625}, -{from: 612, to: 654}, -{from: 612, to: 667}, -{from: 612, to: 677}, -{from: 612, to: 679}, -{from: 612, to: 681}, -{from: 612, to: 685}, -{from: 612, to: 707}, -{from: 613, to: 659}, -{from: 613, to: 673}, -{from: 613, to: 682}, -{from: 613, to: 714}, -{from: 614, to: 623}, -{from: 614, to: 648}, -{from: 614, to: 678}, -{from: 614, to: 687}, -{from: 615, to: 700}, -{from: 615, to: 718}, -{from: 616, to: 668}, -{from: 616, to: 681}, -{from: 616, to: 713}, -{from: 617, to: 680}, -{from: 617, to: 712}, -{from: 617, to: 727}, -{from: 618, to: 619}, -{from: 618, to: 652}, -{from: 618, to: 703}, -{from: 618, to: 716}, -{from: 619, to: 652}, -{from: 619, to: 703}, -{from: 619, to: 716}, -{from: 620, to: 680}, -{from: 620, to: 686}, -{from: 621, to: 632}, -{from: 621, to: 638}, -{from: 621, to: 639}, -{from: 621, to: 657}, -{from: 621, to: 671}, -{from: 621, to: 706}, -{from: 621, to: 715}, -{from: 621, to: 726}, -{from: 622, to: 675}, -{from: 622, to: 676}, -{from: 622, to: 728}, -{from: 623, to: 644}, -{from: 623, to: 648}, -{from: 623, to: 678}, -{from: 623, to: 687}, -{from: 623, to: 719}, -{from: 624, to: 653}, -{from: 624, to: 669}, -{from: 624, to: 698}, -{from: 625, to: 654}, -{from: 625, to: 667}, -{from: 625, to: 677}, -{from: 625, to: 679}, -{from: 625, to: 685}, -{from: 625, to: 707}, -{from: 626, to: 627}, -{from: 626, to: 662}, -{from: 626, to: 705}, -{from: 627, to: 662}, -{from: 627, to: 705}, -{from: 627, to: 717}, -{from: 628, to: 664}, -{from: 628, to: 670}, -{from: 628, to: 709}, -{from: 629, to: 643}, -{from: 629, to: 644}, -{from: 629, to: 679}, -{from: 629, to: 719}, -{from: 629, to: 720}, -{from: 630, to: 672}, -{from: 630, to: 701}, -{from: 631, to: 655}, -{from: 631, to: 722}, -{from: 631, to: 734}, -{from: 632, to: 638}, -{from: 632, to: 639}, -{from: 632, to: 657}, -{from: 632, to: 671}, -{from: 632, to: 715}, -{from: 632, to: 726}, -{from: 633, to: 635}, -{from: 633, to: 636}, -{from: 633, to: 637}, -{from: 633, to: 684}, -{from: 633, to: 688}, -{from: 633, to: 697}, -{from: 633, to: 736}, -{from: 634, to: 711}, -{from: 635, to: 636}, -{from: 635, to: 637}, -{from: 635, to: 684}, -{from: 635, to: 688}, -{from: 635, to: 697}, -{from: 635, to: 736}, -{from: 636, to: 637}, -{from: 636, to: 684}, -{from: 636, to: 688}, -{from: 636, to: 697}, -{from: 636, to: 736}, -{from: 637, to: 684}, -{from: 637, to: 688}, -{from: 637, to: 697}, -{from: 637, to: 736}, -{from: 638, to: 639}, -{from: 638, to: 657}, -{from: 638, to: 671}, -{from: 638, to: 692}, -{from: 638, to: 715}, -{from: 638, to: 723}, -{from: 638, to: 726}, -{from: 639, to: 657}, -{from: 639, to: 671}, -{from: 639, to: 715}, -{from: 639, to: 726}, -{from: 640, to: 641}, -{from: 640, to: 695}, -{from: 640, to: 704}, -{from: 640, to: 708}, -{from: 640, to: 732}, -{from: 640, to: 733}, -{from: 641, to: 695}, -{from: 641, to: 704}, -{from: 641, to: 708}, -{from: 641, to: 709}, -{from: 641, to: 732}, -{from: 641, to: 733}, -{from: 642, to: 645}, -{from: 642, to: 707}, -{from: 642, to: 726}, -{from: 643, to: 644}, -{from: 643, to: 719}, -{from: 643, to: 720}, -{from: 644, to: 719}, -{from: 644, to: 720}, -{from: 646, to: 647}, -{from: 646, to: 650}, -{from: 646, to: 651}, -{from: 646, to: 689}, -{from: 646, to: 713}, -{from: 646, to: 724}, -{from: 646, to: 725}, -{from: 646, to: 729}, -{from: 646, to: 730}, -{from: 646, to: 734}, -{from: 646, to: 735}, -{from: 647, to: 650}, -{from: 647, to: 651}, -{from: 647, to: 689}, -{from: 647, to: 724}, -{from: 647, to: 725}, -{from: 647, to: 729}, -{from: 647, to: 730}, -{from: 647, to: 734}, -{from: 647, to: 735}, -{from: 648, to: 678}, -{from: 648, to: 687}, -{from: 649, to: 682}, -{from: 649, to: 691}, -{from: 649, to: 702}, -{from: 649, to: 706}, -{from: 650, to: 651}, -{from: 650, to: 689}, -{from: 650, to: 724}, -{from: 650, to: 725}, -{from: 650, to: 729}, -{from: 650, to: 730}, -{from: 650, to: 734}, -{from: 650, to: 735}, -{from: 651, to: 689}, -{from: 651, to: 724}, -{from: 651, to: 725}, -{from: 651, to: 729}, -{from: 651, to: 730}, -{from: 651, to: 734}, -{from: 651, to: 735}, -{from: 652, to: 667}, -{from: 652, to: 703}, -{from: 652, to: 716}, -{from: 653, to: 669}, -{from: 653, to: 698}, -{from: 654, to: 667}, -{from: 654, to: 668}, -{from: 654, to: 677}, -{from: 654, to: 679}, -{from: 654, to: 685}, -{from: 654, to: 702}, -{from: 654, to: 707}, -{from: 655, to: 722}, -{from: 656, to: 660}, -{from: 656, to: 674}, -{from: 656, to: 694}, -{from: 656, to: 696}, -{from: 657, to: 671}, -{from: 657, to: 715}, -{from: 657, to: 726}, -{from: 658, to: 731}, -{from: 659, to: 673}, -{from: 659, to: 682}, -{from: 659, to: 714}, -{from: 660, to: 674}, -{from: 660, to: 694}, -{from: 660, to: 696}, -{from: 661, to: 665}, -{from: 661, to: 690}, -{from: 661, to: 692}, -{from: 661, to: 693}, -{from: 661, to: 721}, -{from: 661, to: 723}, -{from: 662, to: 705}, -{from: 663, to: 728}, -{from: 664, to: 670}, -{from: 664, to: 709}, -{from: 665, to: 690}, -{from: 665, to: 692}, -{from: 665, to: 693}, -{from: 665, to: 721}, -{from: 665, to: 723}, -{from: 667, to: 677}, -{from: 667, to: 679}, -{from: 667, to: 685}, -{from: 667, to: 707}, -{from: 668, to: 702}, -{from: 668, to: 713}, -{from: 669, to: 698}, -{from: 670, to: 709}, -{from: 671, to: 715}, -{from: 671, to: 726}, -{from: 672, to: 701}, -{from: 673, to: 682}, -{from: 673, to: 714}, -{from: 674, to: 694}, -{from: 674, to: 696}, -{from: 675, to: 676}, -{from: 675, to: 728}, -{from: 676, to: 699}, -{from: 676, to: 717}, -{from: 676, to: 728}, -{from: 677, to: 679}, -{from: 677, to: 685}, -{from: 677, to: 707}, -{from: 678, to: 687}, -{from: 678, to: 721}, -{from: 679, to: 685}, -{from: 679, to: 707}, -{from: 680, to: 686}, -{from: 681, to: 683}, -{from: 682, to: 714}, -{from: 684, to: 688}, -{from: 684, to: 697}, -{from: 684, to: 736}, -{from: 685, to: 707}, -{from: 687, to: 721}, -{from: 688, to: 697}, -{from: 688, to: 736}, -{from: 689, to: 724}, -{from: 689, to: 725}, -{from: 689, to: 729}, -{from: 689, to: 730}, -{from: 689, to: 734}, -{from: 689, to: 735}, -{from: 690, to: 692}, -{from: 690, to: 693}, -{from: 690, to: 721}, -{from: 690, to: 723}, -{from: 691, to: 702}, -{from: 691, to: 706}, -{from: 692, to: 693}, -{from: 692, to: 721}, -{from: 692, to: 723}, -{from: 693, to: 721}, -{from: 693, to: 723}, -{from: 694, to: 696}, -{from: 695, to: 704}, -{from: 695, to: 708}, -{from: 695, to: 732}, -{from: 695, to: 733}, -{from: 697, to: 736}, -{from: 699, to: 717}, -{from: 700, to: 718}, -{from: 702, to: 706}, -{from: 703, to: 716}, -{from: 704, to: 708}, -{from: 704, to: 732}, -{from: 704, to: 733}, -{from: 707, to: 726}, -{from: 708, to: 732}, -{from: 708, to: 733}, -{from: 712, to: 727}, -{from: 715, to: 726}, -{from: 719, to: 720}, -{from: 721, to: 723}, -{from: 724, to: 725}, -{from: 724, to: 729}, -{from: 724, to: 730}, -{from: 724, to: 734}, -{from: 724, to: 735}, -{from: 725, to: 729}, -{from: 725, to: 730}, -{from: 725, to: 734}, -{from: 725, to: 735}, -{from: 729, to: 730}, -{from: 729, to: 734}, -{from: 729, to: 735}, -{from: 730, to: 734}, -{from: 730, to: 735}, -{from: 732, to: 733}, -{from: 734, to: 735} + ]; + // create a network - ]; + var inheritColorVal = inheritColor.value; - // create a network - var container = document.getElementById('mynetwork'); - var data = { - nodes: nodes, - edges: edges - }; - var options = { - nodes: { - shape: 'dot', - radiusMin: 10, - radiusMax: 30, - fontSize: 12, - fontFace: "Tahoma" - }, - edges: { - width: 0.15, - inheritColor: "from" - }, - tooltip: { - delay: 200, - fontSize: 12, - color: { - background: "#fff" - } - }, - smoothCurves: {dynamic:false, type: "continuous"}, - stabilize: false, - physics: {barnesHut: {gravitationalConstant: 0, centralGravity: 0, springConstant: 0}}, - hideEdgesOnDrag: true - }; - var network = new vis.Network(container, data, options); + var container = document.getElementById('mynetwork'); + var data = { + nodes: nodes, + edges: edges + }; + var options = { + nodes: { + shape: 'dot', + radiusMin: 10, + radiusMax: 30, + fontSize: 12, + fontFace: "Tahoma" + }, + edges: { + width: 0.15, + inheritColor: "from" + }, + tooltip: { + delay: 200, + fontSize: 12, + color: { + background: "#fff" + } + }, + smoothCurves: {dynamic:false, type: "continuous"}, + stabilize: false, + physics: {barnesHut: {gravitationalConstant: 0, centralGravity: 0, springConstant: 0}}, + hideEdgesOnDrag: true + }; + + if (inheritColorVal == "false") { + options['edges']['inheritColor'] = false; + } + else { + options['edges']['inheritColor'] = inheritColorVal; + } network = new vis.Network(container, data, options); + } function update() { var type = dropdown.value; @@ -10075,13 +10095,14 @@ function update() { var options = {smoothCurves:{type:type, roundness:roundness}} options['hideEdgesOnDrag'] = hideEdgesOnDrag.checked; options['hideNodesOnDrag'] = hideNodesOnDrag.checked; - console.log(options); network.setOptions(options); } + redrawAll() +