├── README.md └── apis └── 1.0 ├── color.js ├── core.js ├── dagre.js ├── element.js ├── layout.js ├── minified └── 1.0-min.js ├── samples └── atemplate.js ├── surface.js ├── tests └── algo_core_tests.js └── worker_core.js /README.md: -------------------------------------------------------------------------------- 1 | Algomation API Repository. 2 | ========================== 3 | 4 | This repository contains the sources, documentation, tests and minified versions of the public API's available on Algomation.com 5 | 6 | Algorithms developed and running on algomation are developed in conjunction with these apis. 7 | 8 | This repository does not contain the sources for the actual algomation.com website or associated tools. 9 | 10 | All code within this repository is released under the MIT license below: 11 | 12 | The MIT License (MIT) 13 | 14 | Copyright (c) 2014 Duncan Meech / Algomation 15 | 16 | Permission is hereby granted, free of charge, to any person obtaining a copy 17 | of this software and associated documentation files (the "Software"), to deal 18 | in the Software without restriction, including without limitation the rights 19 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 20 | copies of the Software, and to permit persons to whom the Software is 21 | furnished to do so, subject to the following conditions: 22 | 23 | The above copyright notice and this permission notice shall be included in 24 | all copies or substantial portions of the Software. 25 | 26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 27 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 28 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 29 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 30 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 31 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 32 | THE SOFTWARE. 33 | 34 | API 1.0 35 | ======= 36 | 37 | - color.js Is taken directly from the excellent library https://github.com/brehaut/color-js 38 | the default namespace of the library is mapped to algo.Color 39 | - dagre.js The awesome directed graph layout library https://github.com/cpettitt/dagre 40 | - core.js Includes core/graph/heap/array functions etc. 41 | - element.js Includes the rendering / graphics classes e.g. algo.render.Element/Rectangle/Line 42 | - layout.js Include the layout and visualizer classes e.g. algo.layout.GridLayout etc 43 | - surface.js The surface classes in which all algorithms and graphical elements appear 44 | - worker_core.js This is the web worker based loader for algorithm, so you can understand how your algorithm 45 | and associated libraries are loaded. 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /apis/1.0/color.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2008-2013, Andrew Brehaut, Tim Baumann, Matt Wilson, 2 | // Simon Heimler, Michel Vielmetter 3 | // 4 | // All rights reserved. 5 | // 6 | // Redistribution and use in source and binary forms, with or without 7 | // modification, are permitted provided that the following conditions are met: 8 | // 9 | // * Redistributions of source code must retain the above copyright notice, 10 | // this list of conditions and the following disclaimer. 11 | // * Redistributions in binary form must reproduce the above copyright notice, 12 | // this list of conditions and the following disclaimer in the documentation 13 | // and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 18 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 19 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 20 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 21 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 22 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 23 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 24 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 25 | // POSSIBILITY OF SUCH DAMAGE. 26 | 27 | // color.js - version 1.0.1 28 | // 29 | // HSV <-> RGB code based on code from http://www.cs.rit.edu/~ncs/color/t_convert.html 30 | // object function created by Douglas Crockford. 31 | // Color scheme degrees taken from the colorjack.com colorpicker 32 | // 33 | // HSL support kindly provided by Tim Baumann - http://github.com/timjb 34 | 35 | // create namespaces 36 | /*global net */ 37 | if ("undefined" == typeof net) { 38 | var net = {}; 39 | } 40 | if (!net.brehaut) { 41 | net.brehaut = {}; 42 | } 43 | 44 | // so we can import the color library into Algomation (algo) namespace 45 | var algo = algo || {}; 46 | 47 | // this module function is called with net.brehaut as 'this' 48 | (function () { 49 | "use strict"; 50 | // Constants 51 | 52 | // css_colors maps color names onto their hex values 53 | // these names are defined by W3C 54 | var css_colors = {aliceblue: '#F0F8FF', antiquewhite: '#FAEBD7', aqua: '#00FFFF', aquamarine: '#7FFFD4', azure: '#F0FFFF', beige: '#F5F5DC', bisque: '#FFE4C4', black: '#000000', blanchedalmond: '#FFEBCD', blue: '#0000FF', blueviolet: '#8A2BE2', brown: '#A52A2A', burlywood: '#DEB887', cadetblue: '#5F9EA0', chartreuse: '#7FFF00', chocolate: '#D2691E', coral: '#FF7F50', cornflowerblue: '#6495ED', cornsilk: '#FFF8DC', crimson: '#DC143C', cyan: '#00FFFF', darkblue: '#00008B', darkcyan: '#008B8B', darkgoldenrod: '#B8860B', darkgray: '#A9A9A9', darkgrey: '#A9A9A9', darkgreen: '#006400', darkkhaki: '#BDB76B', darkmagenta: '#8B008B', darkolivegreen: '#556B2F', darkorange: '#FF8C00', darkorchid: '#9932CC', darkred: '#8B0000', darksalmon: '#E9967A', darkseagreen: '#8FBC8F', darkslateblue: '#483D8B', darkslategray: '#2F4F4F', darkslategrey: '#2F4F4F', darkturquoise: '#00CED1', darkviolet: '#9400D3', deeppink: '#FF1493', deepskyblue: '#00BFFF', dimgray: '#696969', dimgrey: '#696969', dodgerblue: '#1E90FF', firebrick: '#B22222', floralwhite: '#FFFAF0', forestgreen: '#228B22', fuchsia: '#FF00FF', gainsboro: '#DCDCDC', ghostwhite: '#F8F8FF', gold: '#FFD700', goldenrod: '#DAA520', gray: '#808080', grey: '#808080', green: '#008000', greenyellow: '#ADFF2F', honeydew: '#F0FFF0', hotpink: '#FF69B4', indianred: '#CD5C5C', indigo: '#4B0082', ivory: '#FFFFF0', khaki: '#F0E68C', lavender: '#E6E6FA', lavenderblush: '#FFF0F5', lawngreen: '#7CFC00', lemonchiffon: '#FFFACD', lightblue: '#ADD8E6', lightcoral: '#F08080', lightcyan: '#E0FFFF', lightgoldenrodyellow: '#FAFAD2', lightgray: '#D3D3D3', lightgrey: '#D3D3D3', lightgreen: '#90EE90', lightpink: '#FFB6C1', lightsalmon: '#FFA07A', lightseagreen: '#20B2AA', lightskyblue: '#87CEFA', lightslategray: '#778899', lightslategrey: '#778899', lightsteelblue: '#B0C4DE', lightyellow: '#FFFFE0', lime: '#00FF00', limegreen: '#32CD32', linen: '#FAF0E6', magenta: '#FF00FF', maroon: '#800000', mediumaquamarine: '#66CDAA', mediumblue: '#0000CD', mediumorchid: '#BA55D3', mediumpurple: '#9370D8', mediumseagreen: '#3CB371', mediumslateblue: '#7B68EE', mediumspringgreen: '#00FA9A', mediumturquoise: '#48D1CC', mediumvioletred: '#C71585', midnightblue: '#191970', mintcream: '#F5FFFA', mistyrose: '#FFE4E1', moccasin: '#FFE4B5', navajowhite: '#FFDEAD', navy: '#000080', oldlace: '#FDF5E6', olive: '#808000', olivedrab: '#6B8E23', orange: '#FFA500', orangered: '#FF4500', orchid: '#DA70D6', palegoldenrod: '#EEE8AA', palegreen: '#98FB98', paleturquoise: '#AFEEEE', palevioletred: '#D87093', papayawhip: '#FFEFD5', peachpuff: '#FFDAB9', peru: '#CD853F', pink: '#FFC0CB', plum: '#DDA0DD', powderblue: '#B0E0E6', purple: '#800080', rebeccapurple: '#663399', red: '#FF0000', rosybrown: '#BC8F8F', royalblue: '#4169E1', saddlebrown: '#8B4513', salmon: '#FA8072', sandybrown: '#F4A460', seagreen: '#2E8B57', seashell: '#FFF5EE', sienna: '#A0522D', silver: '#C0C0C0', skyblue: '#87CEEB', slateblue: '#6A5ACD', slategray: '#708090', slategrey: '#708090', snow: '#FFFAFA', springgreen: '#00FF7F', steelblue: '#4682B4', tan: '#D2B48C', teal: '#008080', thistle: '#D8BFD8', tomato: '#FF6347', turquoise: '#40E0D0', violet: '#EE82EE', wheat: '#F5DEB3', white: '#FFFFFF', whitesmoke: '#F5F5F5', yellow: '#FFFF00', yellowgreen: '#9ACD32'}; 55 | 56 | // CSS value regexes, according to http://www.w3.org/TR/css3-values/ 57 | var css_integer = '(?:\\+|-)?\\d+'; 58 | var css_float = '(?:\\+|-)?\\d*\\.\\d+'; 59 | var css_number = '(?:' + css_integer + ')|(?:' + css_float + ')'; 60 | css_integer = '(' + css_integer + ')'; 61 | css_float = '(' + css_float + ')'; 62 | css_number = '(' + css_number + ')'; 63 | var css_percentage = css_number + '%'; 64 | var css_whitespace = '\\s*?'; 65 | 66 | // http://www.w3.org/TR/2003/CR-css3-color-20030514/ 67 | var hsl_hsla_regex = new RegExp([ 68 | '^hsl(a?)\\(', css_number, ',', css_percentage, ',', css_percentage, '(,(', css_number, '))?\\)$' 69 | ].join(css_whitespace)); 70 | var rgb_rgba_integer_regex = new RegExp([ 71 | '^rgb(a?)\\(', css_integer, ',', css_integer, ',', css_integer, '(,(', css_number, '))?\\)$' 72 | ].join(css_whitespace)); 73 | var rgb_rgba_percentage_regex = new RegExp([ 74 | '^rgb(a?)\\(', css_percentage, ',', css_percentage, ',', css_percentage, '(,(', css_number, '))?\\)$' 75 | ].join(css_whitespace)); 76 | 77 | // Package wide variables 78 | 79 | // becomes the top level prototype object 80 | var color; 81 | 82 | /* registered_models contains the template objects for all the 83 | * models that have been registered for the color class. 84 | */ 85 | var registered_models = []; 86 | 87 | /* factories contains methods to create new instance of 88 | * different color models that have been registered. 89 | */ 90 | var factories = {}; 91 | 92 | // Utility functions 93 | 94 | /* object is Douglas Crockfords object function for prototypal 95 | * inheritance. 96 | */ 97 | if (!this.object) { 98 | this.object = function (o) { 99 | function F() { 100 | } 101 | 102 | F.prototype = o; 103 | return new F(); 104 | }; 105 | } 106 | var object = this.object; 107 | 108 | /* takes a value, converts to string if need be, then pads it 109 | * to a minimum length. 110 | */ 111 | function pad(val, len) { 112 | val = val.toString(); 113 | var padded = []; 114 | 115 | for (var i = 0, j = Math.max(len - val.length, 0); i < j; i++) { 116 | padded.push('0'); 117 | } 118 | 119 | padded.push(val); 120 | return padded.join(''); 121 | } 122 | 123 | /* takes a string and returns a new string with the first letter 124 | * capitalised 125 | */ 126 | function capitalise(s) { 127 | return s.slice(0, 1).toUpperCase() + s.slice(1); 128 | } 129 | 130 | /* removes leading and trailing whitespace 131 | */ 132 | function trim(str) { 133 | return str.replace(/^\s+|\s+$/g, ''); 134 | } 135 | 136 | /* used to apply a method to object non-destructively by 137 | * cloning the object and then apply the method to that 138 | * new object 139 | */ 140 | function cloneOnApply(meth) { 141 | return function () { 142 | var cloned = this.clone(); 143 | meth.apply(cloned, arguments); 144 | return cloned; 145 | }; 146 | } 147 | 148 | /* registerModel is used to add additional representations 149 | * to the color code, and extend the color API with the new 150 | * operatiosn that model provides. see before for examples 151 | */ 152 | function registerModel(name, model) { 153 | var proto = object(color); 154 | var fields = []; // used for cloning and generating accessors 155 | 156 | var to_meth = 'to' + capitalise(name); 157 | 158 | function convertAndApply(meth) { 159 | return function () { 160 | return meth.apply(this[to_meth](), arguments); 161 | }; 162 | } 163 | 164 | for (var key in model) if (model.hasOwnProperty(key)) { 165 | proto[key] = model[key]; 166 | var prop = proto[key]; 167 | 168 | if (key.slice(0, 1) == '_') { 169 | continue; 170 | } 171 | if (!(key in color) && "function" == typeof prop) { 172 | // the method found on this object is a) public and b) not 173 | // currently supported by the color object. Create an impl that 174 | // calls the toModel function and passes that new object 175 | // onto the correct method with the args. 176 | color[key] = convertAndApply(prop); 177 | } 178 | else if ("function" != typeof prop) { 179 | // we have found a public property. create accessor methods 180 | // and bind them up correctly 181 | fields.push(key); 182 | var getter = 'get' + capitalise(key); 183 | var setter = 'set' + capitalise(key); 184 | 185 | color[getter] = convertAndApply( 186 | proto[getter] = (function (key) { 187 | return function () { 188 | return this[key]; 189 | }; 190 | })(key) 191 | ); 192 | 193 | color[setter] = convertAndApply( 194 | proto[setter] = (function (key) { 195 | return function (val) { 196 | var cloned = this.clone(); 197 | cloned[key] = val; 198 | return cloned; 199 | }; 200 | })(key) 201 | ); 202 | } 203 | } // end of for over model 204 | 205 | // a method to create a new object - largely so prototype chains dont 206 | // get insane. This uses an unrolled 'object' so that F is cached 207 | // for later use. this is approx a 25% speed improvement 208 | function F() { 209 | } 210 | 211 | F.prototype = proto; 212 | function factory() { 213 | return new F(); 214 | } 215 | 216 | factories[name] = factory; 217 | 218 | proto.clone = function () { 219 | var cloned = factory(); 220 | for (var i = 0, j = fields.length; i < j; i++) { 221 | var key = fields[i]; 222 | cloned[key] = this[key]; 223 | } 224 | return cloned; 225 | }; 226 | 227 | color[to_meth] = function () { 228 | return factory(); 229 | }; 230 | 231 | registered_models.push(proto); 232 | 233 | return proto; 234 | }// end of registerModel 235 | 236 | // Template Objects 237 | 238 | /* color is the root object in the color hierarchy. It starts 239 | * life as a very simple object, but as color models are 240 | * registered it has methods programmatically added to manage 241 | * conversions as needed. 242 | */ 243 | color = { 244 | /* fromObject takes an argument and delegates to the internal 245 | * color models to try to create a new instance. 246 | */ 247 | fromObject: function (o) { 248 | if (!o) { 249 | return object(color); 250 | } 251 | 252 | for (var i = 0, j = registered_models.length; i < j; i++) { 253 | var nu = registered_models[i].fromObject(o); 254 | if (nu) { 255 | return nu; 256 | } 257 | } 258 | 259 | return object(color); 260 | }, 261 | 262 | toString: function () { 263 | return this.toCSS(); 264 | } 265 | }; 266 | 267 | var transparent = null; // defined with an RGB later. 268 | 269 | /* RGB is the red green blue model. This definition is converted 270 | * to a template object by registerModel. 271 | */ 272 | registerModel('RGB', { 273 | red : 0, 274 | green : 0, 275 | blue : 0, 276 | alpha : 0, 277 | 278 | /* getLuminance returns a value between 0 and 1, this is the 279 | * luminance calcuated according to 280 | * http://www.poynton.com/notes/colour_and_gamma/ColorFAQ.html#RTFToC9 281 | */ 282 | getLuminance: function () { 283 | return (this.red * 0.2126) + (this.green * 0.7152) + (this.blue * 0.0722); 284 | }, 285 | 286 | /* does an alpha based blend of color onto this. alpha is the 287 | * amount of 'color' to use. (0 to 1) 288 | */ 289 | blend : function (color, alpha) { 290 | color = color.toRGB(); 291 | alpha = Math.min(Math.max(alpha, 0), 1); 292 | var rgb = this.clone(); 293 | 294 | rgb.red = (rgb.red * (1 - alpha)) + (color.red * alpha); 295 | rgb.green = (rgb.green * (1 - alpha)) + (color.green * alpha); 296 | rgb.blue = (rgb.blue * (1 - alpha)) + (color.blue * alpha); 297 | rgb.alpha = (rgb.alpha * (1 - alpha)) + (color.alpha * alpha); 298 | 299 | return rgb; 300 | }, 301 | 302 | /* fromObject attempts to convert an object o to and RGB 303 | * instance. This accepts an object with red, green and blue 304 | * members or a string. If the string is a known CSS color name 305 | * or a hexdecimal string it will accept it. 306 | */ 307 | fromObject : function (o) { 308 | if (o instanceof Array) { 309 | return this._fromRGBArray(o); 310 | } 311 | if ("string" == typeof o) { 312 | return this._fromCSS(trim(o)); 313 | } 314 | if (o.hasOwnProperty('red') && 315 | o.hasOwnProperty('green') && 316 | o.hasOwnProperty('blue')) { 317 | return this._fromRGB(o); 318 | } 319 | // nothing matchs, not an RGB object 320 | }, 321 | 322 | _stringParsers: [ 323 | // CSS RGB(A) literal: 324 | function (css) { 325 | css = trim(css); 326 | 327 | var withInteger = match(rgb_rgba_integer_regex, 255); 328 | if (withInteger) { 329 | return withInteger; 330 | } 331 | return match(rgb_rgba_percentage_regex, 100); 332 | 333 | function match(regex, max_value) { 334 | var colorGroups = css.match(regex); 335 | 336 | // If there is an "a" after "rgb", there must be a fourth parameter and the other way round 337 | if (!colorGroups || (!!colorGroups[1] + !!colorGroups[5] === 1)) { 338 | return null; 339 | } 340 | 341 | var rgb = factories.RGB(); 342 | rgb.red = Math.min(1, Math.max(0, colorGroups[2] / max_value)); 343 | rgb.green = Math.min(1, Math.max(0, colorGroups[3] / max_value)); 344 | rgb.blue = Math.min(1, Math.max(0, colorGroups[4] / max_value)); 345 | rgb.alpha = !!colorGroups[5] ? Math.min(Math.max(parseFloat(colorGroups[6]), 0), 1) : 1; 346 | 347 | return rgb; 348 | } 349 | }, 350 | 351 | function (css) { 352 | var lower = css.toLowerCase(); 353 | if (lower in css_colors) { 354 | css = css_colors[lower]; 355 | } 356 | 357 | if (!css.match(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/)) { 358 | return; 359 | } 360 | 361 | css = css.replace(/^#/, ''); 362 | 363 | var bytes = css.length / 3; 364 | 365 | var max = Math.pow(16, bytes) - 1; 366 | 367 | var rgb = factories.RGB(); 368 | rgb.red = parseInt(css.slice(0, bytes), 16) / max; 369 | rgb.green = parseInt(css.slice(bytes * 1, bytes * 2), 16) / max; 370 | rgb.blue = parseInt(css.slice(bytes * 2), 16) / max; 371 | rgb.alpha = 1; 372 | return rgb; 373 | }, 374 | 375 | function (css) { 376 | if (css.toLowerCase() !== 'transparent') return; 377 | 378 | return transparent; 379 | } 380 | ], 381 | 382 | _fromCSS: function (css) { 383 | var color = null; 384 | for (var i = 0, j = this._stringParsers.length; i < j; i++) { 385 | color = this._stringParsers[i](css); 386 | if (color) return color; 387 | } 388 | }, 389 | 390 | _fromRGB: function (RGB) { 391 | var newRGB = factories.RGB(); 392 | 393 | newRGB.red = RGB.red; 394 | newRGB.green = RGB.green; 395 | newRGB.blue = RGB.blue; 396 | newRGB.alpha = RGB.hasOwnProperty('alpha') ? RGB.alpha : 1; 397 | 398 | return newRGB; 399 | }, 400 | 401 | _fromRGBArray: function (RGB) { 402 | var newRGB = factories.RGB(); 403 | 404 | newRGB.red = Math.max(0, Math.min(1, RGB[0] / 255)); 405 | newRGB.green = Math.max(0, Math.min(1, RGB[1] / 255)); 406 | newRGB.blue = Math.max(0, Math.min(1, RGB[2] / 255)); 407 | newRGB.alpha = RGB[3] !== undefined ? Math.max(0, Math.min(1, RGB[3])) : 1; 408 | 409 | return newRGB; 410 | }, 411 | 412 | // convert to a CSS string. defaults to two bytes a value 413 | toCSSHex : function (bytes) { 414 | bytes = bytes || 2; 415 | 416 | var max = Math.pow(16, bytes) - 1; 417 | var css = [ 418 | "#", 419 | pad(Math.round(this.red * max).toString(16).toUpperCase(), bytes), 420 | pad(Math.round(this.green * max).toString(16).toUpperCase(), bytes), 421 | pad(Math.round(this.blue * max).toString(16).toUpperCase(), bytes) 422 | ]; 423 | 424 | return css.join(''); 425 | }, 426 | 427 | toCSS: function (bytes) { 428 | if (this.alpha === 1) return this.toCSSHex(bytes); 429 | 430 | var max = 255; 431 | 432 | var components = [ 433 | 'rgba(', 434 | Math.max(0, Math.min(max, Math.round(this.red * max))), ',', 435 | Math.max(0, Math.min(max, Math.round(this.green * max))), ',', 436 | Math.max(0, Math.min(max, Math.round(this.blue * max))), ',', 437 | Math.max(0, Math.min(1, this.alpha)), 438 | ')' 439 | ]; 440 | 441 | return components.join(''); 442 | }, 443 | 444 | toHSV: function () { 445 | var hsv = factories.HSV(); 446 | var min, max, delta; 447 | 448 | min = Math.min(this.red, this.green, this.blue); 449 | max = Math.max(this.red, this.green, this.blue); 450 | hsv.value = max; // v 451 | 452 | delta = max - min; 453 | 454 | if (delta == 0) { // white, grey, black 455 | hsv.hue = hsv.saturation = 0; 456 | } 457 | else { // chroma 458 | hsv.saturation = delta / max; 459 | 460 | if (this.red == max) { 461 | hsv.hue = ( this.green - this.blue ) / delta; // between yellow & magenta 462 | } 463 | else if (this.green == max) { 464 | hsv.hue = 2 + ( this.blue - this.red ) / delta; // between cyan & yellow 465 | } 466 | else { 467 | hsv.hue = 4 + ( this.red - this.green ) / delta; // between magenta & cyan 468 | } 469 | 470 | hsv.hue = ((hsv.hue * 60) + 360) % 360; // degrees 471 | } 472 | 473 | hsv.alpha = this.alpha; 474 | 475 | return hsv; 476 | }, 477 | toHSL: function () { 478 | return this.toHSV().toHSL(); 479 | }, 480 | 481 | toRGB: function () { 482 | return this.clone(); 483 | } 484 | }); 485 | 486 | transparent = color.fromObject({red: 0, blue: 0, green: 0, alpha: 0}); 487 | 488 | /* Like RGB above, this object describes what will become the HSV 489 | * template object. This model handles hue, saturation and value. 490 | * hue is the number of degrees around the color wheel, saturation 491 | * describes how much color their is and value is the brightness. 492 | */ 493 | registerModel('HSV', { 494 | hue : 0, 495 | saturation: 0, 496 | value : 1, 497 | alpha : 1, 498 | 499 | shiftHue: cloneOnApply(function (degrees) { 500 | var hue = (this.hue + degrees) % 360; 501 | if (hue < 0) { 502 | hue = (360 + hue) % 360; 503 | } 504 | 505 | this.hue = hue; 506 | }), 507 | 508 | devalueByAmount: cloneOnApply(function (val) { 509 | this.value = Math.min(1, Math.max(this.value - val, 0)); 510 | }), 511 | 512 | devalueByRatio: cloneOnApply(function (val) { 513 | this.value = Math.min(1, Math.max(this.value * (1 - val), 0)); 514 | }), 515 | 516 | valueByAmount: cloneOnApply(function (val) { 517 | this.value = Math.min(1, Math.max(this.value + val, 0)); 518 | }), 519 | 520 | valueByRatio: cloneOnApply(function (val) { 521 | this.value = Math.min(1, Math.max(this.value * (1 + val), 0)); 522 | }), 523 | 524 | desaturateByAmount: cloneOnApply(function (val) { 525 | this.saturation = Math.min(1, Math.max(this.saturation - val, 0)); 526 | }), 527 | 528 | desaturateByRatio: cloneOnApply(function (val) { 529 | this.saturation = Math.min(1, Math.max(this.saturation * (1 - val), 0)); 530 | }), 531 | 532 | saturateByAmount: cloneOnApply(function (val) { 533 | this.saturation = Math.min(1, Math.max(this.saturation + val, 0)); 534 | }), 535 | 536 | saturateByRatio: cloneOnApply(function (val) { 537 | this.saturation = Math.min(1, Math.max(this.saturation * (1 + val), 0)); 538 | }), 539 | 540 | schemeFromDegrees: function (degrees) { 541 | var newColors = []; 542 | for (var i = 0, j = degrees.length; i < j; i++) { 543 | var col = this.clone(); 544 | col.hue = (this.hue + degrees[i]) % 360; 545 | newColors.push(col); 546 | } 547 | return newColors; 548 | }, 549 | 550 | /* Algomation extension, returns a palette of n colors using this as the starting color*/ 551 | circularPalette : function (n) { 552 | var degrees = []; 553 | for (var i = 0; i < n; i += 1) { 554 | degrees[i] = this.hue + (360 / n * i); 555 | } 556 | return this.schemeFromDegrees(degrees); 557 | }, 558 | /* Algomation extension, return s a palette of n colors that are a gradient between this and the other color */ 559 | gradientPalette : function (other, n) { 560 | var colors = []; 561 | for (var i = 0; i < n; i += 1) { 562 | var c = this.clone(); 563 | c = c.setHue(this.getHue() + (other.getHue() - this.getHue()) / n * i); 564 | c = c.setSaturation(this.getSaturation() + (other.getSaturation() - this.getSaturation()) / n * i); 565 | c = c.setValue(this.getValue() + (other.getValue() - this.getValue()) / n * i); 566 | colors.push(c); 567 | } 568 | return colors; 569 | }, 570 | 571 | complementaryScheme: function () { 572 | return this.schemeFromDegrees([0, 180]); 573 | }, 574 | 575 | splitComplementaryScheme: function () { 576 | return this.schemeFromDegrees([0, 150, 320]); 577 | }, 578 | 579 | splitComplementaryCWScheme: function () { 580 | return this.schemeFromDegrees([0, 150, 300]); 581 | }, 582 | 583 | splitComplementaryCCWScheme: function () { 584 | return this.schemeFromDegrees([0, 60, 210]); 585 | }, 586 | 587 | triadicScheme: function () { 588 | return this.schemeFromDegrees([0, 120, 240]); 589 | }, 590 | 591 | clashScheme: function () { 592 | return this.schemeFromDegrees([0, 90, 270]); 593 | }, 594 | 595 | tetradicScheme: function () { 596 | return this.schemeFromDegrees([0, 90, 180, 270]); 597 | }, 598 | 599 | fourToneCWScheme: function () { 600 | return this.schemeFromDegrees([0, 60, 180, 240]); 601 | }, 602 | 603 | fourToneCCWScheme: function () { 604 | return this.schemeFromDegrees([0, 120, 180, 300]); 605 | }, 606 | 607 | fiveToneAScheme: function () { 608 | return this.schemeFromDegrees([0, 115, 155, 205, 245]); 609 | }, 610 | 611 | fiveToneBScheme: function () { 612 | return this.schemeFromDegrees([0, 40, 90, 130, 245]); 613 | }, 614 | 615 | fiveToneCScheme: function () { 616 | return this.schemeFromDegrees([0, 50, 90, 205, 320]); 617 | }, 618 | 619 | fiveToneDScheme: function () { 620 | return this.schemeFromDegrees([0, 40, 155, 270, 310]); 621 | }, 622 | 623 | fiveToneEScheme: function () { 624 | return this.schemeFromDegrees([0, 115, 230, 270, 320]); 625 | }, 626 | 627 | sixToneCWScheme: function () { 628 | return this.schemeFromDegrees([0, 30, 120, 150, 240, 270]); 629 | }, 630 | 631 | sixToneCCWScheme: function () { 632 | return this.schemeFromDegrees([0, 90, 120, 210, 240, 330]); 633 | }, 634 | 635 | neutralScheme: function () { 636 | return this.schemeFromDegrees([0, 15, 30, 45, 60, 75]); 637 | }, 638 | 639 | analogousScheme: function () { 640 | return this.schemeFromDegrees([0, 30, 60, 90, 120, 150]); 641 | }, 642 | 643 | fromObject: function (o) { 644 | if (o.hasOwnProperty('hue') && 645 | o.hasOwnProperty('saturation') && 646 | o.hasOwnProperty('value')) { 647 | var hsv = factories.HSV(); 648 | 649 | hsv.hue = o.hue; 650 | hsv.saturation = o.saturation; 651 | hsv.value = o.value; 652 | hsv.alpha = o.hasOwnProperty('alpha') ? o.alpha : 1; 653 | 654 | return hsv; 655 | } 656 | // nothing matches, not an HSV object 657 | return null; 658 | }, 659 | 660 | _normalise: function () { 661 | this.hue %= 360; 662 | this.saturation = Math.min(Math.max(0, this.saturation), 1); 663 | this.value = Math.min(Math.max(0, this.value)); 664 | this.alpha = Math.min(1, Math.max(0, this.alpha)); 665 | }, 666 | 667 | toRGB: function () { 668 | this._normalise(); 669 | 670 | var rgb = factories.RGB(); 671 | var i; 672 | var f, p, q, t; 673 | 674 | if (this.saturation === 0) { 675 | // achromatic (grey) 676 | rgb.red = this.value; 677 | rgb.green = this.value; 678 | rgb.blue = this.value; 679 | rgb.alpha = this.alpha; 680 | return rgb; 681 | } 682 | 683 | var h = this.hue / 60; // sector 0 to 5 684 | i = Math.floor(h); 685 | f = h - i; // factorial part of h 686 | p = this.value * ( 1 - this.saturation ); 687 | q = this.value * ( 1 - this.saturation * f ); 688 | t = this.value * ( 1 - this.saturation * ( 1 - f ) ); 689 | 690 | switch (i) { 691 | case 0: 692 | rgb.red = this.value; 693 | rgb.green = t; 694 | rgb.blue = p; 695 | break; 696 | case 1: 697 | rgb.red = q; 698 | rgb.green = this.value; 699 | rgb.blue = p; 700 | break; 701 | case 2: 702 | rgb.red = p; 703 | rgb.green = this.value; 704 | rgb.blue = t; 705 | break; 706 | case 3: 707 | rgb.red = p; 708 | rgb.green = q; 709 | rgb.blue = this.value; 710 | break; 711 | case 4: 712 | rgb.red = t; 713 | rgb.green = p; 714 | rgb.blue = this.value; 715 | break; 716 | default: // case 5: 717 | rgb.red = this.value; 718 | rgb.green = p; 719 | rgb.blue = q; 720 | break; 721 | } 722 | 723 | rgb.alpha = this.alpha; 724 | 725 | return rgb; 726 | }, 727 | toHSL: function () { 728 | this._normalise(); 729 | 730 | var hsl = factories.HSL(); 731 | 732 | hsl.hue = this.hue; 733 | var l = (2 - this.saturation) * this.value, 734 | s = this.saturation * this.value; 735 | if (l && 2 - l) { 736 | s /= (l <= 1) ? l : 2 - l; 737 | } 738 | l /= 2; 739 | hsl.saturation = s; 740 | hsl.lightness = l; 741 | hsl.alpha = this.alpha; 742 | 743 | return hsl; 744 | }, 745 | 746 | toHSV: function () { 747 | return this.clone(); 748 | } 749 | }); 750 | 751 | registerModel('HSL', { 752 | hue : 0, 753 | saturation: 0, 754 | lightness : 0, 755 | alpha : 1, 756 | 757 | darkenByAmount: cloneOnApply(function (val) { 758 | this.lightness = Math.min(1, Math.max(this.lightness - val, 0)); 759 | }), 760 | 761 | darkenByRatio: cloneOnApply(function (val) { 762 | this.lightness = Math.min(1, Math.max(this.lightness * (1 - val), 0)); 763 | }), 764 | 765 | lightenByAmount: cloneOnApply(function (val) { 766 | this.lightness = Math.min(1, Math.max(this.lightness + val, 0)); 767 | }), 768 | 769 | lightenByRatio: cloneOnApply(function (val) { 770 | this.lightness = Math.min(1, Math.max(this.lightness * (1 + val), 0)); 771 | }), 772 | 773 | fromObject: function (o) { 774 | if ("string" == typeof o) { 775 | return this._fromCSS(o); 776 | } 777 | if (o.hasOwnProperty('hue') && 778 | o.hasOwnProperty('saturation') && 779 | o.hasOwnProperty('lightness')) { 780 | return this._fromHSL(o); 781 | } 782 | // nothing matchs, not an RGB object 783 | }, 784 | 785 | _fromCSS: function (css) { 786 | var colorGroups = trim(css).match(hsl_hsla_regex); 787 | 788 | // if there is an "a" after "hsl", there must be a fourth parameter and the other way round 789 | if (!colorGroups || (!!colorGroups[1] + !!colorGroups[5] === 1)) { 790 | return null; 791 | } 792 | 793 | var hsl = factories.HSL(); 794 | hsl.hue = (colorGroups[2] % 360 + 360) % 360; 795 | hsl.saturation = Math.max(0, Math.min(parseInt(colorGroups[3], 10) / 100, 1)); 796 | hsl.lightness = Math.max(0, Math.min(parseInt(colorGroups[4], 10) / 100, 1)); 797 | hsl.alpha = !!colorGroups[5] ? Math.max(0, Math.min(1, parseFloat(colorGroups[6]))) : 1; 798 | 799 | return hsl; 800 | }, 801 | 802 | _fromHSL: function (HSL) { 803 | var newHSL = factories.HSL(); 804 | 805 | newHSL.hue = HSL.hue; 806 | newHSL.saturation = HSL.saturation; 807 | newHSL.lightness = HSL.lightness; 808 | 809 | newHSL.alpha = HSL.hasOwnProperty('alpha') ? HSL.alpha : 1; 810 | 811 | return newHSL; 812 | }, 813 | 814 | _normalise: function () { 815 | this.hue = (this.hue % 360 + 360) % 360; 816 | this.saturation = Math.min(Math.max(0, this.saturation), 1); 817 | this.lightness = Math.min(Math.max(0, this.lightness)); 818 | this.alpha = Math.min(1, Math.max(0, this.alpha)); 819 | }, 820 | 821 | toHSL: function () { 822 | return this.clone(); 823 | }, 824 | toHSV: function () { 825 | this._normalise(); 826 | 827 | var hsv = factories.HSV(); 828 | 829 | // http://ariya.blogspot.com/2008/07/converting-between-hsl-and-hsv.html 830 | hsv.hue = this.hue; // H 831 | var l = 2 * this.lightness, 832 | s = this.saturation * ((l <= 1) ? l : 2 - l); 833 | hsv.value = (l + s) / 2; // V 834 | hsv.saturation = ((2 * s) / (l + s)) || 0; // S 835 | hsv.alpha = this.alpha; 836 | 837 | return hsv; 838 | }, 839 | toRGB: function () { 840 | return this.toHSV().toRGB(); 841 | } 842 | }); 843 | 844 | // Package specific exports 845 | 846 | /* the Color function is a factory for new color objects. 847 | */ 848 | function Color(o) { 849 | return color.fromObject(o); 850 | } 851 | 852 | Color.isValid = function (str) { 853 | var key, c = Color(str); 854 | 855 | var length = 0; 856 | for (key in c) { 857 | if (c.hasOwnProperty(key)) { 858 | length++; 859 | } 860 | } 861 | 862 | return length > 0; 863 | }; 864 | net.brehaut.Color = Color; 865 | 866 | algo.Color = net.brehaut.Color; 867 | 868 | /** 869 | * some color objects that match the interface colors 870 | */ 871 | algo.Color.iBLUE = new algo.Color('#3266B5'); 872 | algo.Color.iGREEN = new algo.Color('#89B632'); 873 | algo.Color.iCYAN = new algo.Color('#30ADAD'); 874 | algo.Color.iORANGE = new algo.Color('#E89200'); 875 | algo.Color.iRED = new algo.Color('#B23A31'); 876 | algo.Color.iWHITE = new algo.Color('#FFFFFF'); 877 | algo.Color.iBLACK = new algo.Color('#000000'); 878 | algo.Color.iGRAY = new algo.Color('#CCCCCC'); 879 | algo.Color.iTRANSPARENT = new algo.Color('transparent'); 880 | 881 | // expose the css_colors object 882 | algo.Color.NamedColors = css_colors; 883 | 884 | }).call(net.brehaut); 885 | 886 | /* Export to CommonJS 887 | */ 888 | var module; 889 | if (module) { 890 | module.exports.Color = net.brehaut.Color; 891 | } -------------------------------------------------------------------------------- /apis/1.0/element.js: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2014 Duncan Meech / Algomation 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | */ 24 | 25 | /*globals _, document, window, $*/ 26 | "use strict"; 27 | 28 | /** 29 | * namespaces for the render library 30 | * @namespace algo 31 | */ 32 | var algo = algo || {}; 33 | 34 | /** 35 | * @namespace 36 | */ 37 | algo.render = algo.render || {}; 38 | 39 | /** 40 | * the base class of renderable elements e.g. algo.render.Rectangle etc. 41 | * @class 42 | * @param _options 43 | * @constructor 44 | * @abstract 45 | */ 46 | algo.render.Element = function (_options) { 47 | 48 | // clone options and extend the options with defaults 49 | 50 | var options = _.defaults(_.clone(_options || {}), { 51 | type : 'Element', 52 | strokeWidth: 1, 53 | fontSize : '40px', 54 | rotation : 0 55 | }); 56 | 57 | // All elements will use the state:algo.render.kS_NORMAL ** UNLESS they specify another state or any of the 58 | // properties fill, stroke, pen in their constructor options. 59 | 60 | if (!algo.core.hasAny(options, 'state', 'stroke', 'fill', 'pen')) { 61 | options.state = algo.render.kS_NORMAL; 62 | } 63 | 64 | // default x/y to zero UNLESS they are already set ( including via a shape ) 65 | if (!options.x && !options.shape) { 66 | options.x = 0; 67 | } 68 | if (!options.y && !options.shape) { 69 | options.y = 0; 70 | } 71 | 72 | // create our ID and add to static map of elements 73 | 74 | this.id = 'algoid-' + algo.render.Element.nextID++; 75 | algo.render.Element.map[this.id] = this; 76 | 77 | // create empty child collection 78 | this.children = new algo.render.ElementGroup(); 79 | 80 | // add default states, this must be done before applying caller initial configuration since that may reference 81 | // these states 82 | this.addStates([ 83 | { 84 | name : algo.render.kS_NORMAL, 85 | properties: {fill: algo.Color.iWHITE, stroke: algo.Color.iBLUE, pen: algo.Color.iBLUE} 86 | }, 87 | { 88 | name : algo.render.kS_FADED, 89 | properties: {fill: algo.Color.iWHITE, stroke: algo.Color.iGRAY, pen: algo.Color.iGRAY} 90 | }, 91 | { 92 | name : algo.render.kS_BLUE, 93 | properties: {fill: algo.Color.iBLUE, stroke: algo.Color.iBLUE, pen: algo.Color.iWHITE} 94 | }, 95 | { 96 | name : algo.render.kS_GRAY, 97 | properties: {fill: algo.Color.iGRAY, stroke: algo.Color.iGRAY, pen: algo.Color.iWHITE} 98 | }, 99 | { 100 | name : algo.render.kS_ORANGE, 101 | properties: {fill: algo.Color.iORANGE, stroke: algo.Color.iORANGE, pen: algo.Color.iWHITE} 102 | }, 103 | { 104 | name : algo.render.kS_RED, 105 | properties: {fill: algo.Color.iRED, stroke: algo.Color.iRED, pen: algo.Color.iWHITE} 106 | }, 107 | { 108 | name : algo.render.kS_GREEN, 109 | properties: {fill: algo.Color.iGREEN, stroke: algo.Color.iGREEN, pen: algo.Color.iWHITE} 110 | }, 111 | { 112 | name : algo.render.kS_CYAN, 113 | properties: {fill: algo.Color.iCYAN, stroke: algo.Color.iCYAN, pen: algo.Color.iWHITE} 114 | } 115 | ]); 116 | 117 | // apply options to elements 118 | this.set(options); 119 | }; 120 | 121 | /* The following are constants for use with the 'state' property of elements */ 122 | /** 123 | * @const The normal display state 124 | */ 125 | algo.render.kS_NORMAL = 'ks_normal'; 126 | 127 | /** 128 | * @const The faded / disabled display state 129 | */ 130 | algo.render.kS_FADED = 'ks_faded'; 131 | 132 | /** 133 | * @const The blue display state 134 | */ 135 | algo.render.kS_BLUE = 'ks_blue'; 136 | 137 | /** 138 | * @const The deselected display state 139 | */ 140 | algo.render.kS_GRAY = 'ks_gray'; 141 | 142 | /** 143 | * @const The orange display state 144 | */ 145 | algo.render.kS_ORANGE = 'ks_orange'; 146 | 147 | /** 148 | * @const The red display state 149 | */ 150 | algo.render.kS_RED = 'ks_red'; 151 | 152 | /** 153 | * @const The green display state 154 | */ 155 | algo.render.kS_GREEN = 'ks_green'; 156 | 157 | /** 158 | * @const The cyan display state 159 | */ 160 | algo.render.kS_CYAN = 'ks_cyan'; 161 | 162 | /** 163 | * return the given CSS property name appropriately prefixed for the browser we are on. 164 | * Since method will only work correctly when inside the DOM, and therefore is only 165 | * available to the element through methods like updateDOM. 166 | * NOTE. It is also dumb, its simple prefixes the given string with the browser prefix. 167 | * 168 | * @param {string} propertyName 169 | */ 170 | algo.render.Element.prefixed = function (propertyName) { 171 | 172 | // generate browser prefix if this is the first call 173 | 174 | if (!algo.render.Element.browserPrefix) { 175 | 176 | var styles = window.getComputedStyle(document.documentElement, ''), 177 | pre = (Array.prototype.slice 178 | .call(styles) 179 | .join('') 180 | .match(/-(moz|webkit|ms)-/) || (styles.OLink === '' && ['', 'o']) 181 | )[1], 182 | dom = ('WebKit|Moz|MS|O').match(new RegExp('(' + pre + ')', 'i'))[1]; 183 | 184 | algo.render.Element.browserPrefix = { 185 | dom : dom, 186 | lowercase: pre, 187 | css : '-' + pre + '-', 188 | js : pre[0].toUpperCase() + pre.substr(1) 189 | }; 190 | } 191 | 192 | return algo.render.Element.browserPrefix.css + propertyName; 193 | }; 194 | 195 | /** 196 | * apply the properties of the state specified by name e.g. algo.render.kS_BLUE, but could be any name including custom 197 | * states supplied by user 198 | * @param {string} stateName 199 | */ 200 | algo.render.Element.prototype.setState = function (stateName) { 201 | 202 | this.states = this.states || {}; 203 | var state; 204 | 205 | // element properties can be values or arrays of values, so handle both 206 | 207 | if (_.isArray(stateName)) { 208 | 209 | // we have an array of state names e.g. [ "S1", "S2", "S3" ], representing potentially different sets of properties. 210 | // We need to translate these state names into arrays of properties to set e.g. fill: [1,2,3], stroke: [a,b,c] 211 | 212 | var properties = {}; 213 | 214 | // iterate each state named in the array 215 | 216 | _.each(stateName, _.bind(function (name) { 217 | 218 | state = this.states[name]; 219 | if (!state) { 220 | throw new Error("Attempt to apply unregistered state ( " + stateName + " ) to element:" + this.id); 221 | } 222 | 223 | // take the properties of the state and add the master list 224 | _.each(state.properties, _.bind(function (value, key) { 225 | 226 | // create properties value array as needed and add the value 227 | properties[key] = properties[key] || []; 228 | properties[key].push(value); 229 | 230 | }, this)); 231 | 232 | // so here an array of states if now an object containing property name keys and values, which 233 | // we can set directly 234 | 235 | this.set(properties); 236 | 237 | }, this)); 238 | 239 | } else { 240 | 241 | state = this.states[stateName]; 242 | if (state) { 243 | this.set(state.properties); 244 | } else { 245 | throw new Error("Attempt to apply unregistered state ( " + stateName + " ) to element:" + this.id); 246 | } 247 | } 248 | }; 249 | 250 | /** 251 | * when you want to reset the element class i.e. when restarting the animation. 252 | */ 253 | algo.render.Element.resetClass = function () { 254 | 255 | // reset ID and map of elements ID's to elements 256 | 257 | algo.render.Element.nextID = 0; 258 | 259 | algo.render.Element.map = {}; 260 | }; 261 | 262 | /** 263 | * start history mode which basically means remember currently allocated elements and reset 264 | */ 265 | algo.render.Element.enterHistoryMode = function () { 266 | 267 | if (algo.render.Element.history) { 268 | throw new Error("Element class is already in history mode"); 269 | } 270 | 271 | algo.render.Element.history = { 272 | nextID: algo.render.Element.nextID, 273 | map : algo.render.Element.map 274 | }; 275 | 276 | algo.render.Element.resetClass(); 277 | }; 278 | 279 | /** 280 | * exit history mode 281 | */ 282 | algo.render.Element.exitHistoryMode = function () { 283 | 284 | if (!algo.render.Element.history) { 285 | throw new Error("Element class was not in history mode"); 286 | } 287 | 288 | algo.render.Element.nextID = algo.render.Element.history.nextID; 289 | algo.render.Element.map = algo.render.Element.history.map; 290 | 291 | delete algo.render.Element.history; 292 | }; 293 | 294 | /** 295 | * find an element by its id 296 | * @param id 297 | */ 298 | algo.render.Element.findElement = function (id) { 299 | 300 | return algo.render.Element.map[id]; 301 | }; 302 | 303 | /** 304 | * get the value for the given property from the object or the nearest parent where it is set. 305 | * if the property is not found in the chain then return the default value 306 | * @param key 307 | */ 308 | algo.render.Element.prototype.getInheritedValue = function (key, defaultValue) { 309 | 310 | var obj = this; 311 | 312 | while (obj) { 313 | 314 | if (obj.hasOwnProperty(key)) { 315 | return obj[key]; 316 | } 317 | 318 | obj = obj.parent; 319 | } 320 | 321 | // if here nobody has the property so return the default 322 | 323 | return defaultValue; 324 | }; 325 | 326 | /** 327 | * get the value for the given property from the object if set otherwise return the default value 328 | * @param key 329 | */ 330 | algo.render.Element.prototype.getOwnValue = function (key, defaultValue) { 331 | 332 | if (this.hasOwnProperty(key)) { 333 | return this[key]; 334 | } 335 | 336 | return defaultValue; 337 | }; 338 | 339 | /** 340 | * shallow clone the properties from the object, using inheritance to fill values if not present on the object. 341 | * Values that are not present will be left untouched and therefore can be pre-filled with defaults. 342 | * @param properties 343 | * @return {object} a new properties objects with requested values. 344 | */ 345 | algo.render.Element.prototype.get = function (properties) { 346 | 347 | var p = _.clone(properties); 348 | 349 | _.each(p, function (defaultValue, key) { 350 | 351 | p[key] = this.getInheritedValue(key, defaultValue); 352 | 353 | }, this); 354 | 355 | return p; 356 | 357 | }; 358 | 359 | 360 | /** 361 | * apply all the properties in options. child class should overload by but call the base class to ensure 362 | * base class properties are set as well. 363 | * @param options 364 | * @depth {number} depth - is the how deep in the child tree should be apply the options. Its defaults to zero 365 | * which means it will apply only to the element. If called with 1, the options will apply 366 | * to the element and its immediately children and so on. 367 | * Since depth is only tested for truthiness at each level and decremented on recursive calls 368 | * omitting (undefined) or setting to NaN, -1 etc will cause the options to be applied at all levels of the child tree. 369 | */ 370 | algo.render.Element.prototype.set = function (options, _depth) { 371 | 372 | // count changes to the element, if it is zero after applying the options we don't need to update it 373 | 374 | var changes = 0; 375 | 376 | var depth = _depth || 0; 377 | 378 | if (options) { 379 | 380 | // set other properties 381 | 382 | _.each(options, function (value, key) { 383 | 384 | // set all options 385 | 386 | switch (key) { 387 | 388 | case 'parent': 389 | { 390 | // if already parent then remove from existing parent 391 | 392 | if (this.parent) { 393 | this.parent.removeChild(this); 394 | } 395 | 396 | // add ourselves as a child of the given value, count this is a change 397 | 398 | value.addChild(this); 399 | 400 | changes += 1; 401 | } 402 | break; 403 | 404 | case 'states': 405 | { 406 | this.addStates(value); 407 | 408 | // this doesn't count as a change ! since defining states doesn't produce any difference 409 | // until the state is changed 410 | 411 | } 412 | break; 413 | 414 | case 'state': 415 | { 416 | 417 | // apply a set of properties from the named state 418 | 419 | this.setState(value); 420 | 421 | changes += 1; 422 | 423 | } 424 | break; 425 | 426 | case 'shape': 427 | { 428 | 429 | // shape is a generic property that is meaningful to the particular class only e.g. 430 | // an algo.render.Line instance can have its x1/y1/x2/y2 properties set by any shape with 431 | // the same properties e.g. algo.render.Line, algo.layout.Line, {x1:0, y1: 0 x2: 0, y2: 0} etc 432 | 433 | this.fromShape(value); 434 | 435 | // fromShape will just write to the appropriate properties, so we must mark as a change 436 | 437 | changes += 1; 438 | 439 | } 440 | break; 441 | 442 | // all other options simply set properties on the element of the same name 443 | 444 | default: 445 | { 446 | if (this[key] !== value) { 447 | this[key] = value; 448 | changes += 1; 449 | } 450 | } 451 | break; 452 | } 453 | 454 | }, this); 455 | 456 | // create update command, but apply only if there were changes 457 | 458 | if (changes) { 459 | algo.SURFACE.elementUpdated(this, options); 460 | } 461 | 462 | // if we have no parent AND no parent was specified AND this is not the root element then append to the root 463 | 464 | if (!this.parent && !options.parent && !options.root && this.id !== 'algoid-0') { 465 | algo.SURFACE.root.addChild(this); 466 | } 467 | 468 | } 469 | 470 | // apply properties to children if depth if truthy 471 | 472 | if (depth) { 473 | _.each(this.children.elements, function (child) { 474 | 475 | // decrease depth when applying to children. 476 | 477 | child.set(options, depth - 1); 478 | 479 | }, this); 480 | } 481 | }; 482 | 483 | /** 484 | * add new states in the given object 485 | * @param states 486 | */ 487 | algo.render.Element.prototype.addStates = function (states) { 488 | 489 | this.states = this.states || {}; 490 | _.each(states, function (state) { 491 | this.states[state.name] = {properties: _.clone(state.properties)}; 492 | }, this); 493 | }; 494 | 495 | /** 496 | * this is static version of the algo.render.Element.prototype.set 497 | * Since it does not operate on a specific instance it accepts a variable number of arguments with an 498 | * optional final depth parameter which defaults to zero. The optional arguments must be individual element instances 499 | * or arrays or elements or objects with properties that are elements. You can supply objects or arrays which do not exclusively hold 500 | * element, objects of other types will be ignored. 501 | * The .set method of each element will be called e.g. 502 | * 503 | * algo.render.Element.set({x:0}, element1, "abc", element2, [element3, element4, { element: element5}, 3.1415; 504 | * 505 | * would invoke set on element1, element2, element3, element4, element5 506 | * 507 | * @param options 508 | * @param {...elements} var_args - zero or more element instances or arrays of element instances 509 | * @param [depth] - if the last argument is a number it is passed as the depth parameter to the instances set method 510 | * @static 511 | */ 512 | algo.render.Element.set = function (options) { 513 | 514 | // set depth to zero or the supplied value 515 | 516 | var args = _.toArray(arguments); 517 | var depth = _.isNumber(_.last(args)) ? args.pop() : 0; 518 | 519 | // iterate elements following required options argument 520 | for (var i = 1; i < args.length; i += 1) { 521 | 522 | // check the type, might be an array, and object or an instance of an element 523 | var x = args[i]; 524 | 525 | if (x instanceof algo.render.Element) { 526 | // for an instance of Element we call directly. 527 | x.set(options, depth); 528 | } else { 529 | // for arrays of object or objects with properties that are elements we need to iterate 530 | _.each(x, function (y) { 531 | if (y instanceof algo.render.Element) { 532 | y.set(options, depth); 533 | } 534 | }, this); 535 | } 536 | } 537 | }; 538 | 539 | /** 540 | * get our geometry from some other shape. This is meaningfully implemented in progeny objects 541 | * @param {Object} shape - any object that shares the geometrical properties of the 542 | * @abstract 543 | */ 544 | algo.render.Element.prototype.fromShape = function (shape) { 545 | 546 | }; 547 | 548 | /** 549 | * return a CSS color specification from any object / string that can be used to construct an algo.Color 550 | * @param {String|algo.Color} obj 551 | * @returns {String} - for colors with alpha === 1.0 it will be '#xxxxxx', otherwise 'rgba(x,x,x,x)' 552 | * NOTE: This should have been static but its too late now...just use algo.render.Element.prototype.getCSSColor.call(null, obj) 553 | */ 554 | algo.render.Element.prototype.getCSSColor = function (obj) { 555 | 556 | var color = obj; 557 | 558 | if (!(obj instanceof algo.Color)) { 559 | color = new algo.Color(obj); 560 | } 561 | 562 | return color.toCSS(); 563 | }; 564 | 565 | /** 566 | * each element is assigned a unique ID upon construction. This ID is used as an attribute within the element 567 | * to make finding the element in the DOM easier. 568 | */ 569 | algo.render.Element.nextID = 0; 570 | 571 | /** 572 | * if true then new elements get the fade in keyframe animation. During history update the fade in effect is 573 | * unattractive since you are skipping between frames quickly 574 | * @type {boolean} 575 | */ 576 | algo.render.Element.fadeIn = true; 577 | 578 | /** 579 | * create the DOM element for this instance. The base class is simply an absolutely positioned div 580 | */ 581 | algo.render.Element.prototype.createDOM = function () { 582 | 583 | var s = _.sprintf('
', 584 | algo.render.Element.fadeIn ? "algo-element-fadein" : ""); 585 | 586 | this.dom = $(s); 587 | 588 | // add our ID as an attribute 589 | 590 | this.dom.attr('id', this.id); 591 | 592 | this.textSpan = $('.algo-element-text', this.dom); 593 | 594 | // append to parent if we have one 595 | 596 | if (this.parent) { 597 | 598 | this.dom.appendTo(this.parent.dom); 599 | } 600 | }; 601 | 602 | /** 603 | * get an object with our CSS properties. These are combined with those of inheriting classes 604 | * to construct the full set of CSS properties for this element 605 | */ 606 | algo.render.Element.prototype.updateDOM = function () { 607 | 608 | // holds properties for the primary DOM element 609 | 610 | var prop = {}; 611 | 612 | // holds properties for the text span 613 | 614 | var tprop = {}; 615 | 616 | // fill property is applied to the background 617 | 618 | prop['background-color'] = this.getCSSColor(this.getInheritedValue('fill', 'transparent')); 619 | 620 | // gradients are set via the css3 background-image property 621 | 622 | prop['background-image'] = this.getInheritedValue('gradient', 'none'); 623 | 624 | // stroke width is applied to the border width 625 | 626 | var sw = this.getInheritedValue('strokeWidth', 0); 627 | 628 | prop.border = sw + 'px solid ' + this.getCSSColor(this.getInheritedValue('stroke', 'transparent')); 629 | 630 | // opacity 631 | 632 | prop.opacity = this.getInheritedValue('opacity', 1); 633 | 634 | // visibility 635 | 636 | prop.visibility = this.getInheritedValue('visible', 'visible'); 637 | 638 | // z is converted to z-index 639 | 640 | prop['z-index'] = this.getOwnValue('z', 0); 641 | 642 | // pen color is applied to text 643 | 644 | tprop.color = this.getCSSColor(this.getInheritedValue('pen', 'black')); 645 | 646 | // font-size is applied to text 647 | 648 | tprop['font-size'] = this.getInheritedValue('fontSize', '12px'); 649 | 650 | // text align is also applied to text element 651 | 652 | tprop['text-align'] = this.getInheritedValue('textAlign', 'center'); 653 | 654 | // basic position, scaling and rotation use inherited values except for x/y which must be set on each 655 | // element that uses them. 656 | 657 | var x = this.x, 658 | y = this.y, 659 | sx = this.getOwnValue('scaleX', 1), 660 | sy = this.getOwnValue('scaleY', 1), 661 | r = this.getOwnValue('rotation', 0); 662 | 663 | // property name must be prefixed 664 | 665 | var transform = algo.render.Element.prefixed('transform'); 666 | 667 | prop[transform] = _.sprintf('translate3d(%.0fpx, %.0fpx, 0) rotate(%.2fdeg) scale(%.2f, %.2f)', x, y, r, sx, sy); 668 | 669 | // apply to element and text element 670 | 671 | this.dom.css(prop); 672 | 673 | this.textSpan.css(tprop); 674 | 675 | // set text, which is never inherited 676 | 677 | this.textSpan.text(this.text); 678 | }; 679 | 680 | /** 681 | * ensure the DOM is created and updated and apply the same procedure to our children 682 | */ 683 | algo.render.Element.prototype.update = function () { 684 | 685 | if (!this.dom) { 686 | 687 | this.createDOM(); 688 | } 689 | 690 | this.updateDOM(); 691 | 692 | _.each(this.children.elements, function (c) { 693 | 694 | c.update(); 695 | 696 | }, this); 697 | 698 | }; 699 | 700 | /** 701 | * add a child element 702 | * @param e 703 | */ 704 | algo.render.Element.prototype.addChild = function (e) { 705 | 706 | // add the new child using its id as the key 707 | this.children.add(e); 708 | 709 | // set the elements parent to ourselves, record the ID since the parent object is not transmitted between worker and DOM 710 | 711 | e.parent = this; 712 | e.parentID = this.id; 713 | 714 | }; 715 | 716 | /** 717 | * remove the child from this elements child collection 718 | * @param e 719 | */ 720 | algo.render.Element.prototype.removeChild = function (e) { 721 | 722 | this.children.remove(e); 723 | 724 | delete e.parent; 725 | 726 | delete e.parentID; 727 | }; 728 | 729 | /** 730 | * destroyy the element, remove from parent. Destroy all children first, then ourselves. 731 | */ 732 | algo.render.Element.prototype.destroy = function () { 733 | 734 | // can only be destroyed once 735 | 736 | if (this.destroyed) { 737 | throw new Error("Destroy already called in Element::destroy"); 738 | } 739 | 740 | // destroy children first 741 | 742 | this.children.destroy(); 743 | 744 | // now, ourselves, remove from parent 745 | 746 | if (this.parent) { 747 | 748 | this.parent.removeChild(this); 749 | } 750 | 751 | // remove DOM if present 752 | 753 | if (this.dom) { 754 | 755 | this.dom.remove(); 756 | 757 | delete this.dom; 758 | } 759 | 760 | // tell surface we have been removed 761 | 762 | algo.SURFACE.elementDestroyed(this); 763 | 764 | // remove from element map 765 | 766 | if (!algo.render.Element.map[this.id]) { 767 | throw new Error("Missing Element"); 768 | } 769 | 770 | delete algo.render.Element.map[this.id]; 771 | 772 | // flag as destroyed 773 | 774 | this.destroyed = true; 775 | }; 776 | 777 | /** 778 | * position / size the element within the given algo.render.Box object. The meaning is deferred to inheriting classes 779 | * @param layout 780 | */ 781 | algo.render.Element.prototype.layout = function (box) { 782 | 783 | // base class just centers itself in the box 784 | 785 | this.set({ 786 | x: box.cx, 787 | y: box.cy 788 | }); 789 | }; 790 | 791 | /** 792 | * position and size to fill the given box 793 | * @param layout 794 | */ 795 | algo.render.Element.prototype.fillBox = function (box) { 796 | 797 | // base class just centers itself in the box 798 | 799 | this.set({ 800 | x: box.x, 801 | y: box.y, 802 | w: box.w, 803 | h: box.h 804 | }); 805 | }; 806 | 807 | /** 808 | * a box object representing our bounds 809 | */ 810 | algo.render.Element.prototype.getBounds = function () { 811 | 812 | // base class returns an empty box centered on our position 813 | 814 | return new algo.layout.Box(this.x, this.y, 0, 0); 815 | }; 816 | 817 | /* ---------------------------------------------------- RECTANGLE ----------------------------------------------------*/ 818 | 819 | // **Rectangle** object constructor 820 | /** 821 | * @class algo.render.Rectangle 822 | * @augments algo.render.Element 823 | * @param _options 824 | * @constructor 825 | */ 826 | algo.render.Rectangle = function (_options) { 827 | 828 | // options object gets modified so pass along a clone so we don't change the users object 829 | 830 | var options = _.clone(_options || {}); 831 | 832 | // base class constructor with defaults 833 | 834 | algo.render.Element.call(this, _.defaults(options, { 835 | 836 | type : 'Rectangle', 837 | cornerRadius: 0 838 | 839 | })); 840 | 841 | }; 842 | /** 843 | * Rectangle extends Element 844 | */ 845 | algo.core.extends(algo.render.Element, algo.render.Rectangle); 846 | 847 | /** 848 | * create a rectangle with the most basic options. Syntactic sugar 849 | * @param box 850 | * @param stroke 851 | * @param fill 852 | * @param strokeWidth 853 | */ 854 | algo.render.Rectangle.create = function (box, stroke, fill, strokeWidth) { 855 | 856 | return new algo.render.Rectangle({ 857 | shape : box, 858 | stroke : stroke, 859 | fill : fill, 860 | strokeWidth: strokeWidth 861 | }); 862 | }; 863 | 864 | /** 865 | * get the DOM element for this rectangle. 866 | */ 867 | algo.render.Rectangle.prototype.createDOM = function () { 868 | 869 | // our dom is identical the base element DOM 870 | 871 | algo.render.Element.prototype.createDOM.call(this); 872 | }; 873 | 874 | algo.render.Rectangle.prototype.updateDOM = function () { 875 | 876 | // super class first 877 | 878 | algo.render.Element.prototype.updateDOM.call(this); 879 | 880 | // then this instance ( using integer widths seems to improve rendering on FireFox ) 881 | 882 | var w = this.getInheritedValue('w', 0) >> 0, 883 | h = this.getInheritedValue('h', 0) >> 0, 884 | cr = this.getInheritedValue('cornerRadius', 0); 885 | 886 | 887 | // then this instance 888 | 889 | var prop = { 890 | width : w + 'px', 891 | height : h + 'px', 892 | 'border-radius': cr + 'px' 893 | }; 894 | 895 | // NOTE: There is a problem with scaled rectangles with a 1px stroke. One or more borders may randomly 896 | // disappear as the element is scaled since border widths are scaled as well. 897 | // There are no good fixes for this problem ( how do you draw a line that is 0.5 of a pixel thick? ) but 898 | // you can fake it by setting a corner radius of at least 1px! This forces the browser to anti-alias the border 899 | // as a curve so helping to make the border re-appear 900 | 901 | if (this.strokeWidth === 1 && cr === 0) { 902 | prop['border-radius'] = '1px' 903 | } 904 | 905 | this.dom.css(prop); 906 | 907 | }; 908 | 909 | /** 910 | * position / size the element within the given algo.render.Box object. The meaning is deferred to inheriting classes 911 | * @param {algo.layout.Box} 912 | */ 913 | algo.render.Rectangle.prototype.layout = function (box) { 914 | 915 | // for now just center ourselves in the box 916 | 917 | this.set({ 918 | x: box.cx - this.w / 2, 919 | y: box.cy - this.h / 2 920 | }); 921 | }; 922 | 923 | /** 924 | * get our geometry from some other rectangle or circle like object. 925 | * @param {Object} shape - some line like object 926 | */ 927 | algo.render.Rectangle.prototype.fromShape = function (shape) { 928 | 929 | if (algo.core.isCircleLike(shape)) { 930 | this.set({x: shape.x - shape.radius, y: shape.y - shape.radius, w: shape.radius * 2, h: shape.radius * 2}); 931 | } else if (algo.core.isRectLike(shape)) { 932 | this.set({x: shape.x, y: shape.y, w: shape.w, h: shape.h}); 933 | } else if (algo.core.isPointLike(shape)) { 934 | this.set({x: shape.x - this.w / 2, y: shape.y - this.h / 2}); 935 | } else { 936 | throw new Error("algo.render.Rectangle.fromShape called with unrecognized shape"); 937 | } 938 | }; 939 | 940 | /** 941 | * center on the given x/y location 942 | * @param layout 943 | */ 944 | algo.render.Rectangle.prototype.center = function (x, y) { 945 | 946 | // for now just center ourselves in the box 947 | 948 | this.set({ 949 | x: x - this.w / 2, 950 | y: y - this.h / 2 951 | }); 952 | }; 953 | 954 | /** 955 | * a box object representing our bounds 956 | */ 957 | algo.render.Rectangle.prototype.getBounds = function () { 958 | 959 | // return our bounds 960 | 961 | return new algo.layout.Box(this.x, this.y, this.w, this.h); 962 | }; 963 | 964 | /* ------------------------------------------------------ LETTER TILE -------------------------------------------------*/ 965 | 966 | /** 967 | * A rectangle with center text and optional text at top right. It is useful for displaying strings for example where 968 | * you want to display both the character and the index at each location. 969 | * @constructor 970 | */ 971 | algo.render.LetterTile = function (_options) { 972 | 973 | // options object gets modified so pass along a clone so we don't change the users object 974 | 975 | var options = _.defaults(_.clone(_options || {}, { 976 | type : 'LetterTile', 977 | cornerRadius: 0 978 | })); 979 | 980 | // if not shape or w/h was set then make them 50...for backwards compatibility 981 | 982 | if (!options.shape && !options.w) { 983 | options.w = 50; 984 | } 985 | 986 | if (!options.shape && !options.h) { 987 | options.h = 50; 988 | } 989 | 990 | // base class constructor with defaults 991 | 992 | algo.render.Rectangle.call(this, options); 993 | 994 | }; 995 | 996 | algo.core.extends(algo.render.Rectangle, algo.render.LetterTile); 997 | 998 | /** 999 | * set is overloaded so we can keep the value element sized correctly and displaying the correct text 1000 | * @param options 1001 | * @param _depth 1002 | */ 1003 | algo.render.LetterTile.prototype.set = function (options, _depth) { 1004 | 1005 | // base class first 1006 | algo.render.Rectangle.prototype.set.call(this, options, _depth); 1007 | 1008 | // create the text label that is positioned at the top of us. 1009 | 1010 | // NOTE: For the DOM side zombie we don't need to create the child in the ctor 1011 | // since the calls to children's ctor are captured as commands and passed to the DOM side 1012 | 1013 | if (algo.SURFACE.isWorker && !this.valueElement) { 1014 | 1015 | this.valueElement = new algo.render.Rectangle({ 1016 | 1017 | x : 0, 1018 | y : 0, 1019 | w : 0, 1020 | h : 0, 1021 | parent : this, 1022 | fill : algo.Color.iTRANSPARENT, 1023 | strokeWidth: 0, 1024 | fontSize : 16, 1025 | textAlign : 'right', 1026 | text : '' 1027 | 1028 | }); 1029 | } 1030 | 1031 | // now update the value element 1032 | if (this.valueElement) { 1033 | 1034 | var fontSize = 12; 1035 | var inset = 4; 1036 | 1037 | this.valueElement.set({ 1038 | y : inset, 1039 | w : this.w - inset, 1040 | h : fontSize, 1041 | pen : this.pen, 1042 | fontSize: fontSize + 'px', 1043 | text : this.value 1044 | }); 1045 | } 1046 | }; 1047 | 1048 | /** 1049 | * circle class, positioned via center 1050 | * @param _options 1051 | * @constructor 1052 | */ 1053 | algo.render.Circle = function (_options) { 1054 | 1055 | // options object gets modified so pass along a clone so we don't change the users object 1056 | 1057 | var options = _.clone(_options || {}); 1058 | 1059 | // add our type into the options 1060 | 1061 | algo.render.Element.call(this, _.defaults(options, { 1062 | type: 'Circle' 1063 | })); 1064 | 1065 | }; 1066 | /** 1067 | * Circle extends Element 1068 | */ 1069 | algo.core.extends(algo.render.Element, algo.render.Circle); 1070 | 1071 | /** 1072 | * get our geometry from some other circle or rectangle like object. 1073 | * @param {Object} shape - some line like object 1074 | */ 1075 | algo.render.Circle.prototype.fromShape = function (shape) { 1076 | 1077 | if (algo.core.isCircleLike(shape)) { 1078 | this.set({ 1079 | x: shape.x, 1080 | y: shape.y, 1081 | radius: shape.radius 1082 | }); 1083 | } else if (algo.core.isRectLike(shape)) { 1084 | this.set({ 1085 | x: shape.x + shape.w / 2, 1086 | y: shape.y + shape.h / 2, 1087 | radius: Math.min(shape.w, shape.h) / 2 1088 | }); 1089 | } else if (algo.core.isPointLike(shape)) { 1090 | this.set({ 1091 | x: shape.x, 1092 | y: shape.y 1093 | }); 1094 | } else { 1095 | throw new Error("algo.render.Circle.fromShape called with unrecognized shape"); 1096 | } 1097 | 1098 | }; 1099 | /** 1100 | * get the DOM element for this circle. 1101 | */ 1102 | algo.render.Circle.prototype.createDOM = function () { 1103 | 1104 | // our dom is identical the base element DOM 1105 | 1106 | algo.render.Element.prototype.createDOM.call(this); 1107 | 1108 | }; 1109 | 1110 | /** 1111 | * update to current properties 1112 | */ 1113 | algo.render.Circle.prototype.updateDOM = function () { 1114 | 1115 | // super class first 1116 | 1117 | algo.render.Element.prototype.updateDOM.call(this); 1118 | 1119 | // then this instance 1120 | 1121 | var x = this.x, 1122 | y = this.y, 1123 | sx = this.getOwnValue('scaleX', 1), 1124 | sy = this.getOwnValue('scaleY', 1), 1125 | r = this.getOwnValue('rotation', 0), 1126 | R = this.getInheritedValue('radius', 10), 1127 | sw = this.getInheritedValue('strokeWidth', 0); 1128 | 1129 | // circles are positioned via center, calculate the negative offset required 1130 | 1131 | var o = -(R + sw); 1132 | 1133 | // width and height 1134 | var size = _.sprintf('%.0fpx', R << 1); 1135 | 1136 | var prop = { 1137 | width : size, 1138 | height : size, 1139 | 'border-radius': '100%' 1140 | }; 1141 | 1142 | //var transform = 'translate3d(' + (x + o) + 'px,' + (y + o) + 'px' + ',0) ' + 1143 | // 'rotate(' + r + 'deg) ' + 1144 | // 'scale(' + sx + ',' + sy + ')'; 1145 | 1146 | var transform = _.sprintf('translate3d(%.0fpx, %.0fpx, 0px) rotate(%.2fdeg) scale(%.2f, %.2f)', x + o, y + o, r, sx, sy); 1147 | 1148 | // apply with and without browser prefix 1149 | 1150 | prop[algo.render.Element.prefixed('transform')] = transform; 1151 | prop.transform = transform; 1152 | 1153 | this.dom.css(prop); 1154 | }; 1155 | 1156 | /** 1157 | * layout bounds 1158 | * @returns {algo.layout.Box} 1159 | */ 1160 | algo.render.Circle.prototype.getBounds = function () { 1161 | 1162 | return new algo.layout.Box(this.x, this.y, this.R * 2, this.R * 2); 1163 | }; 1164 | 1165 | /** 1166 | * stroked and filled line, defined by properties x/y/x2/y2 1167 | * NOTE: Due to the way CSS transforms are applied the stroke, if any, will be included as part of the line. 1168 | * For now I recommend not using strokes on lines. 1169 | */ 1170 | algo.render.Line = function (_options) { 1171 | 1172 | // lines use their fill color as their primary color so set adjust their states accordingly 1173 | var lineStates = [ 1174 | { 1175 | name : algo.render.kS_NORMAL, 1176 | properties: {fill: algo.Color.iBLUE} 1177 | }, 1178 | { 1179 | name : algo.render.kS_BLUE, 1180 | properties: {fill: algo.Color.iBLUE} 1181 | }, 1182 | { 1183 | name : algo.render.kS_GRAY, 1184 | properties: {fill: algo.Color.iGRAY} 1185 | }, 1186 | { 1187 | name : algo.render.kS_FADED, 1188 | properties: {fill: algo.Color.iGRAY} 1189 | }, 1190 | { 1191 | name : algo.render.kS_ORANGE, 1192 | properties: {fill: algo.Color.iORANGE} 1193 | }, 1194 | { 1195 | name : algo.render.kS_RED, 1196 | properties: {fill: algo.Color.iRED} 1197 | }, 1198 | { 1199 | name : algo.render.kS_CYAN, 1200 | properties: {fill: algo.Color.iCYAN} 1201 | } 1202 | ]; 1203 | 1204 | // lines required a modification to the default display states since they are rendered using their fill color 1205 | 1206 | var options = _.clone(_options || {}); 1207 | 1208 | // add the default states to the given states if there are any, allowing the user states to overwrite the defaults 1209 | // if provided 1210 | 1211 | if (options.states) { 1212 | options.states = lineStates.concat(options.states); 1213 | } else { 1214 | options.states = lineStates; 1215 | } 1216 | 1217 | // if no display properties were set in the constructor apply the default state 1218 | 1219 | if (!algo.core.hasAny(options, 'state', 'fill')) { 1220 | options.state = algo.render.kS_NORMAL; 1221 | } 1222 | 1223 | // progenitor constructor first 1224 | algo.render.Element.call(this, _.defaults(options, { 1225 | type : 'Line', 1226 | thickness : 1, 1227 | strokeWidth: 0 1228 | })); 1229 | 1230 | }; 1231 | 1232 | // **Line** extends the object **Element** 1233 | algo.core.extends(algo.render.Element, algo.render.Line); 1234 | 1235 | /** 1236 | * get the DOM element for this line. 1237 | */ 1238 | algo.render.Line.prototype.createDOM = function () { 1239 | 1240 | // our dom is identical the base element DOM 1241 | 1242 | algo.render.Element.prototype.createDOM.call(this); 1243 | 1244 | }; 1245 | 1246 | /** 1247 | * update to current properties 1248 | */ 1249 | algo.render.Line.prototype.updateDOM = function () { 1250 | 1251 | // super class first 1252 | 1253 | algo.render.Element.prototype.updateDOM.call(this); 1254 | 1255 | // then this instance 1256 | 1257 | var p = this.get({ 1258 | x1 : 0, 1259 | y1 : 0, 1260 | x2 : 0, 1261 | y2 : 0, 1262 | thickness : 1, 1263 | strokeWidth: 0, 1264 | inset : 0 1265 | }); 1266 | 1267 | // now calculate the length of the line which becomes the width of the div 1268 | 1269 | var len = Math.sqrt(((p.x2 - p.x1) * (p.x2 - p.x1)) + ((p.y2 - p.y1) * (p.y2 - p.y1))); 1270 | 1271 | // if the line is inset then adjust start/end points 1272 | 1273 | if (p.inset) { 1274 | 1275 | // clamp inset to 1/2 length, less 2 pixels so that the line never 1276 | // collapses to a point 1277 | 1278 | p.inset = Math.min((len - 4) / 2, p.inset); 1279 | 1280 | // get delta x/y 1281 | 1282 | var dx = p.x2 - p.x1, dy = p.y2 - p.y1; 1283 | 1284 | // normalize, while avoiding / 0 errors 1285 | 1286 | var nx = (dx / len) || 0, ny = (dy / len) || 0; 1287 | 1288 | // get x inset 1289 | 1290 | var xi = nx * p.inset; 1291 | 1292 | var yi = ny * p.inset; 1293 | 1294 | // adjust end points 1295 | 1296 | p.x1 += xi; 1297 | 1298 | p.y1 += yi; 1299 | 1300 | p.x2 -= xi; 1301 | 1302 | p.y2 -= yi; 1303 | 1304 | } 1305 | 1306 | // first calculate the angle from this.x/this.y to this.p.x2/this.p.y2 1307 | 1308 | var rads = Math.atan2(p.y2 - p.y1, p.x2 - p.x1); 1309 | 1310 | // atan2 return negative PI radians for the 180-360 degrees ( 9 o'clock to 3 o'clock ) 1311 | 1312 | if (rads < 0) { 1313 | 1314 | rads = 2 * Math.PI + rads; 1315 | } 1316 | 1317 | // now calculate the length of the line which becomes the width of the div 1318 | 1319 | len = Math.sqrt(((p.x2 - p.x1) * (p.x2 - p.x1)) + ((p.y2 - p.y1) * (p.y2 - p.y1))); 1320 | 1321 | // get total thickness or line 1322 | 1323 | var t = p.thickness + p.strokeWidth * 2; 1324 | 1325 | // set DOM with our transform, thickness and width (len) 1326 | 1327 | var prop = { 1328 | width : _.sprintf('%.0fpx', len), 1329 | height : _.sprintf('%.0fpx', p.thickness), 1330 | 'border-radius': _.sprintf('%.0fpx', t / 2) 1331 | }; 1332 | 1333 | var origin = _.sprintf('0px %.0fpx', t / 2); 1334 | 1335 | var transform = _.sprintf('translate(%.0fpx, %.0fpx) rotate(%.2frad)', p.x1, p.y1 - t / 2, rads); 1336 | 1337 | prop[algo.render.Element.prefixed('transform-origin')] = origin; 1338 | 1339 | prop[algo.render.Element.prefixed('transform')] = transform; 1340 | 1341 | prop['transform-origin'] = origin; 1342 | 1343 | prop.transform = transform; 1344 | 1345 | this.dom.css(prop); 1346 | 1347 | }; 1348 | 1349 | /** 1350 | * line instances are isomorphic with Line so we can 1351 | * @param other 1352 | * @returns {point|min.point|algo.layout.Intersection.point|Function|algo.point} 1353 | */ 1354 | algo.render.Line.prototype.intersectWithLine = function (other) { 1355 | 1356 | return algo.layout.Line.prototype.intersectWithLine.call(this, other).point; 1357 | }; 1358 | 1359 | algo.render.Line.prototype.intersectWithBox = function (other) { 1360 | 1361 | return algo.layout.Line.prototype.intersectWithBox.call(this, other).points; 1362 | }; 1363 | 1364 | /** 1365 | * get our geometry from some other line like object. 1366 | * @param {Object} shape - some line like object 1367 | */ 1368 | algo.render.Line.prototype.fromShape = function (shape) { 1369 | 1370 | if (!algo.core.isLineLike(shape)) { 1371 | throw new Error("Not line like shape in algo.render.Line::fromShape"); 1372 | } 1373 | 1374 | this.set({ 1375 | x1: shape.x1, 1376 | y1: shape.y1, 1377 | x2: shape.x2, 1378 | y2: shape.y2 1379 | }); 1380 | 1381 | }; 1382 | 1383 | /** 1384 | * Due to limitations of CSS arrows are constrained to 1px thickness. Arrows are a fixed size also. 1385 | * use startArrow: [true,false] and endArrow: [true, false] to control the visibility of the arrow heads. 1386 | * @class algo.render.Arrow 1387 | * @constructor 1388 | * @inherits algo.render.Element 1389 | */ 1390 | algo.render.Arrow = function (options) { 1391 | 1392 | // base class constructor 1393 | 1394 | algo.render.Line.call(this, _.defaults(options, { 1395 | type : 'Arrow', 1396 | startArrow: true, 1397 | endArrow : true 1398 | })); 1399 | 1400 | }; 1401 | 1402 | algo.core.extends(algo.render.Line, algo.render.Arrow); 1403 | 1404 | /** 1405 | * get the DOM element for this line. 1406 | */ 1407 | algo.render.Arrow.prototype.createDOM = function () { 1408 | 1409 | // our dom is identical the base element DOM 1410 | 1411 | algo.render.Element.prototype.createDOM.call(this); 1412 | 1413 | // but then... we add two additional divs with the appropriate CSS classes for the arrows 1414 | 1415 | this.startElement = $('
'); 1416 | this.startElement.appendTo(this.dom); 1417 | 1418 | this.endElement = $('
'); 1419 | this.endElement.appendTo(this.dom); 1420 | 1421 | }; 1422 | 1423 | /** 1424 | * update to current properties 1425 | */ 1426 | algo.render.Arrow.prototype.updateDOM = function () { 1427 | 1428 | // all the line css will be set by the base class 1429 | 1430 | algo.render.Line.prototype.updateDOM.call(this); 1431 | 1432 | // .. we just need to control the visibility and color of the arrows and set arrow color correctly 1433 | 1434 | var startCSS = {display: 'none'}, endCSS = {display: 'none'}; 1435 | 1436 | if (this.startArrow) { 1437 | 1438 | // the color of the arrow is set on the object is ancestor or as a last resort is the ancestors fill color 1439 | 1440 | startCSS = { 1441 | display : 'block', 1442 | 'border-right-color': this.getCSSColor(this.getInheritedValue('startArrowColor', this.getInheritedValue('fill', algo.Color.iBLUE))) 1443 | }; 1444 | } 1445 | 1446 | if (this.endArrow) { 1447 | 1448 | // the color of the arrow is set on the object is ancestor or as a last resort is the ancestors fill color 1449 | 1450 | endCSS = { 1451 | display : 'block', 1452 | 'border-left-color': this.getCSSColor(this.getInheritedValue('endArrowColor', this.getInheritedValue('fill', algo.Color.iBLUE))) 1453 | }; 1454 | } 1455 | 1456 | this.startElement.css(startCSS); 1457 | 1458 | this.endElement.css(endCSS); 1459 | }; 1460 | 1461 | // --------------------------------------------------------------------------------------------------------------------- 1462 | 1463 | /** 1464 | * an element group is any related or unrelated set of elements to which you want to apply properties in unison. 1465 | * The constructor accepts arrays or individual elements or a map or elements e.g. the children properties of an element 1466 | * 1467 | * new algo.render.ElementGroup(element1, element2) 1468 | * 1469 | * or 1470 | * 1471 | * new algo.render.ElementGroup(elementArray, element1, anotherElementArray, element2) 1472 | * 1473 | * or 1474 | * 1475 | * new algo.render.ElementGroup(element.children) 1476 | */ 1477 | algo.render.ElementGroup = function () { 1478 | 1479 | this.elements = []; 1480 | 1481 | // process the arguments, processing according to type 1482 | 1483 | _.each(_.toArray(arguments), function (arg) { 1484 | 1485 | if (arg instanceof algo.render.Element) { 1486 | 1487 | this.elements.push(arg); 1488 | 1489 | } else if (_.isArray(arg)) { 1490 | 1491 | this.elements = _.union(this.elements, arg); 1492 | } 1493 | else if (_.isObject(arg)) { 1494 | 1495 | this.elements = _.union(this.elements, _.values(arg)); 1496 | 1497 | } 1498 | 1499 | }, this 1500 | ); 1501 | }; 1502 | 1503 | /** 1504 | * apply the properties to all the members of the group 1505 | * @param options 1506 | */ 1507 | algo.render.ElementGroup.prototype.set = function (options) { 1508 | 1509 | _.each(this.elements, function (e) { 1510 | 1511 | e.set(options); 1512 | 1513 | }, this); 1514 | }; 1515 | 1516 | /** 1517 | * add an element to the group 1518 | * @param {algo.render.Element} e - the element to add to the group 1519 | */ 1520 | algo.render.ElementGroup.prototype.add = function (e) { 1521 | 1522 | if (this.elements.indexOf(e) < 0) { 1523 | this.elements.push(e); 1524 | } 1525 | }; 1526 | 1527 | /** 1528 | * remove an element from the group 1529 | * @param {algo.render.Element} e - the element to remove from the group 1530 | */ 1531 | algo.render.ElementGroup.prototype.remove = function (e) { 1532 | 1533 | var index = this.elements.indexOf(e); 1534 | 1535 | if (index >= 0) { 1536 | this.elements.splice(index, 1); 1537 | } 1538 | }; 1539 | 1540 | /** 1541 | * remove all elements from the group 1542 | */ 1543 | algo.render.ElementGroup.prototype.clear = function () { 1544 | this.elements.length = 0; 1545 | }; 1546 | 1547 | /** 1548 | * call destroy on all the elements of the group 1549 | * @param options 1550 | */ 1551 | algo.render.ElementGroup.prototype.destroy = function () { 1552 | 1553 | // work from a cloned list since element groups are used to hold the children of elements and might therefore 1554 | // be modified as part of the destroy function call graph 1555 | 1556 | _.each(_.toArray(this.elements), function (e) { 1557 | 1558 | e.destroy(); 1559 | 1560 | }, this); 1561 | 1562 | this.elements.length = 0; 1563 | }; 1564 | 1565 | /** 1566 | * a linear gradient is the angle of the gradient and the start and end color 1567 | * @param angle 1568 | * @param start 1569 | * @param end 1570 | * @constructor 1571 | */ 1572 | algo.render.LinearGradient = function (angle, start, end) { 1573 | 1574 | this.angle = angle; 1575 | this.start = start; 1576 | this.end = end; 1577 | }; 1578 | 1579 | // TODO, include in 2.0 and get grid of gradient property. 1580 | ///** 1581 | // * return a CSS representation of ourselves 1582 | // */ 1583 | //algo.render.LinearGradient.prototype.toCSS = function() { 1584 | // 1585 | // // get a string representation of our start/end colors, however they are represented 1586 | // var c1 = algo.render.Element.prototype.getCSSColor.call(this, this.start); 1587 | // var c2 = algo.render.Element.prototype.getCSSColor.call(this, this.end); 1588 | // 1589 | // return _.sprintf('linear-gradient(%sdeg, %s, %s)', this.angle, c1, c2 ); 1590 | // 1591 | //}; 1592 | 1593 | 1594 | 1595 | 1596 | 1597 | 1598 | -------------------------------------------------------------------------------- /apis/1.0/layout.js: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2014 Duncan Meech / Algomation 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | */ 24 | 25 | /*globals _, dagre*/ 26 | "use strict"; 27 | /** 28 | * namespaces for the layout algorithms 29 | * @namespace algo 30 | */ 31 | var algo = algo || {}; 32 | 33 | /** 34 | * @namespace 35 | */ 36 | algo.layout = algo.layout || {}; 37 | 38 | /** 39 | * the progenitor object for all visual layout strategies. It is constructed with the DataStructure it operates 40 | * on. Each time its .update method is called it will invoke methods on the dataStructure as appropriate. 41 | * @param {algo.core.DataStructure} dataStructure 42 | * @constructor 43 | */ 44 | algo.layout.Strategy = function (dataStructure) { 45 | 46 | this.dataStructure = dataStructure; 47 | }; 48 | 49 | /** 50 | * call after making changes to the data structure. This will invoke various methods on the data structure as 51 | * appropriate. 52 | */ 53 | algo.layout.Strategy.prototype.update = function () { 54 | 55 | }; 56 | 57 | /** 58 | * the simplest of the graph layout strategies. Arranges graph vertices around the circumference of an ellipse. 59 | * @param {algo.core.Graph} dataStructure 60 | * @constructor 61 | */ 62 | algo.layout.GraphCircular = function (dataStructure) { 63 | 64 | algo.layout.Strategy.call(this, dataStructure); 65 | 66 | }; 67 | 68 | algo.core.extends(algo.layout.Strategy, algo.layout.GraphCircular); 69 | 70 | /** 71 | * arrange the vertices on the circumference of the given box 72 | * @param {algo.layout.Box} box 73 | */ 74 | algo.layout.GraphCircular.prototype.update = function (box) { 75 | 76 | // x/y radius is derived from our bounding box 77 | var rX = box.w >> 1, rY = box.h >> 1; 78 | 79 | // keep a temporary map of the vertex positions, since we supply those to the edge update function 80 | var vertexPositions = {}; 81 | 82 | // iterate over an array of the graph vertices, which provides us with an index in the callback 83 | var vertices = _.values(this.dataStructure.vertices); 84 | 85 | _.each(vertices, function (v, index) { 86 | 87 | // degrees around the circle is simple index * ( 360 / number of vertices ) 88 | var degrees = index * (360 / vertices.length); 89 | 90 | // return an empty box centered on the correct location 91 | var p = algo.core.pointOnEllipse(box.cx, box.cy, rX, rY, degrees); 92 | 93 | // save vertex position for edge update 94 | vertexPositions[v.id] = p; 95 | 96 | // update vertex 97 | this.dataStructure.invoke('updateVertex', v, v.element, p, this.dataStructure); 98 | 99 | }, this); 100 | 101 | // process all edges 102 | _.each(this.dataStructure.edges, function (edge) { 103 | 104 | var p1 = vertexPositions[edge.source.id]; 105 | var p2 = vertexPositions[edge.target.id]; 106 | this.dataStructure.invoke('updateEdge', edge, edge.element, p1, p2, this.dataStructure); 107 | 108 | }, this); 109 | }; 110 | 111 | /** 112 | * The binary tree layout strategy. s 113 | * @param {algo.core.BinaryTree} dataStructure 114 | * @constructor 115 | */ 116 | algo.layout.BinaryTree = function (dataStructure) { 117 | 118 | algo.layout.Strategy.call(this, dataStructure); 119 | }; 120 | 121 | algo.core.extends(algo.layout.Strategy, algo.layout.BinaryTree); 122 | 123 | /** 124 | * perform Knuth layout on binary tree graph 125 | * @param {algo.layout.Box} box 126 | */ 127 | algo.layout.BinaryTree.prototype.update = function (box) { 128 | 129 | // uses Knuth's simple binary tree layout algorithm. This results 130 | // in a simple x/y (column/row) position for each vertex. We then 131 | // use a algo.layout.GridLayout object to position each vertex 132 | // within the given bounding box 133 | 134 | // x position of nodes is a global while we perform the traversal, 135 | // maxDepth tracks how deep the tree is. vertexMap is used to store 136 | // the x/y position for each vertex for later updates 137 | 138 | var x = 0, maxDepth = 0, vertexMap = {}; 139 | 140 | // the recursive function that traverses the tree and updates x, maxDepth 141 | // and vertexMap 142 | 143 | function traverseLayout(vertex, depth) { 144 | 145 | if (vertex) { 146 | 147 | // keep track of max depth 148 | maxDepth = Math.max(maxDepth, depth); 149 | 150 | // go left first 151 | traverseLayout(vertex.left, depth + 1); 152 | 153 | // save position for this vertex 154 | vertexMap[vertex.id] = { 155 | x: x++, 156 | y: depth 157 | }; 158 | 159 | // go right 160 | traverseLayout(vertex.right, depth + 1); 161 | } 162 | } 163 | 164 | // traverse from root, depth 1 165 | traverseLayout(this.dataStructure.root, 0); 166 | 167 | // create a grid layout using the calculate rows and columns required 168 | var grid = new algo.layout.GridLayout(box, maxDepth + 1, x); 169 | 170 | // now update the vertex position...and then the edges since edge positions depend on vertices 171 | var p, b; 172 | 173 | _.each(this.dataStructure.vertices, function (v) { 174 | 175 | // get position 176 | p = vertexMap[v.id]; 177 | 178 | // get corresponding box from grid layout 179 | b = grid.getBox(p.y, p.x); 180 | 181 | // invoke the update method of the tree 182 | this.dataStructure.invoke('updateVertex', v, v.element, {x: b.cx, y: b.cy}); 183 | 184 | }, this); 185 | 186 | _.each(this.dataStructure.vertices, function (v) { 187 | 188 | p = vertexMap[v.id]; 189 | b = grid.getBox(p.y, p.x); 190 | 191 | // update its left and right edges if they exist 192 | if (v.left) { 193 | this.dataStructure.invoke('updateEdge', v.leftEdge, v.leftEdge.element, v, v.left); 194 | } 195 | if (v.right) { 196 | this.dataStructure.invoke('updateEdge', v.rightEdge, v.rightEdge.element, v, v.right); 197 | } 198 | 199 | }, this); 200 | 201 | }; 202 | 203 | /** 204 | * The force directed graph layout strategy. 205 | * @param {algo.core.Graph} dataStructure 206 | * @constructor 207 | */ 208 | algo.layout.GraphForceDirected = function (dataStructure, options) { 209 | 210 | algo.layout.Strategy.call(this, dataStructure); 211 | 212 | // syntactic sugar 213 | this.graph = dataStructure; 214 | 215 | // clone and save options and suppy defaults 216 | this.options = _.defaults(_.clone(options || {}), { 217 | stiffness: 400, 218 | repulsion: 400, 219 | damping : 0.5 220 | }); 221 | 222 | // extend ourselves with the options so we don't have to write this.options.stiffness etc 223 | _.extend(this, this.options); 224 | 225 | this.vertexPoints = {}; // keep track of points associated with vertices 226 | this.edgeSprings = {}; // keep track of springs associated with edges 227 | }; 228 | 229 | algo.core.extends(algo.layout.Strategy, algo.layout.GraphForceDirected); 230 | 231 | /** 232 | * Start the layout algorithm and run for the specified number of ms OR until the total energy 233 | * in the system goes below a threshold 234 | * @param {box} box - the bounding box for the layout 235 | * @param {number} ms - the maximum number of milliseconds to run the simulation for 236 | */ 237 | algo.layout.GraphForceDirected.prototype.update = function (box, ms) { 238 | 239 | // set bounding box for graph 240 | this.box = box; 241 | // calculate time to stop at, or default to 100ms from now 242 | var stop = Date.now() + (ms || 100); 243 | // iteratively improve the layout until time limit reached or the total energy in the system has decayed below 244 | // a certain threshold 245 | while (true) { 246 | 247 | this.applyCoulombsLaw(); 248 | this.applyHookesLaw(); 249 | this.attractToCentre(); 250 | this.updateVelocity(0.03); 251 | this.updatePosition(0.03); 252 | 253 | if (this.totalEnergy() < 0.01 || Date.now() >= stop) { 254 | break; 255 | } 256 | } 257 | 258 | // update the simulations bounding box and size after the layout 259 | this.simBounds = this.getBoundingBox(); 260 | this.simSize = this.simBounds.topright.subtract(this.simBounds.bottomleft); 261 | 262 | // update all vertices first 263 | _.each(this.graph.vertices, function (vertex) { 264 | if (vertex.element) { 265 | this.graph.invoke('updateVertex', vertex, vertex.element, this.getVertexPosition(vertex)); 266 | } 267 | }, this); 268 | 269 | // now edges 270 | _.each(this.graph.edges, function (edge) { 271 | 272 | // get screen positions of end points of edge springs 273 | var spring = this.spring(edge); 274 | var p1 = this.simToScreen(spring.point1.p); 275 | var p2 = this.simToScreen(spring.point2.p); 276 | if (edge.element) { 277 | this.graph.invoke('updateEdge', edge, edge.element, p1, p2); 278 | } 279 | }, this); 280 | }; 281 | 282 | /** 283 | * get the location within our bounds for the given vertex. 284 | * @param vertex 285 | * @return {algo.layout.Vector} 286 | */ 287 | algo.layout.GraphForceDirected.prototype.getVertexPosition = function (vertex) { 288 | 289 | return this.simToScreen(this.vertexPoints[vertex.id].p); 290 | }; 291 | 292 | /** 293 | * convert the simulation point to a point within our current bounding box. 294 | * Assumes that this.simBounds is up to date. 295 | * @param p 296 | */ 297 | algo.layout.GraphForceDirected.prototype.simToScreen = function (p) { 298 | 299 | var sx = p.subtract(this.simBounds.bottomleft).divide(this.simSize.x).x * this.box.w; 300 | 301 | var sy = p.subtract(this.simBounds.bottomleft).divide(this.simSize.y).y * this.box.h; 302 | 303 | // allow for non zero position of bounding box 304 | 305 | sx += this.box.x; 306 | 307 | sy += this.box.y; 308 | 309 | // return vector representing location 310 | 311 | return new algo.layout.Vector(sx, sy); 312 | 313 | }; 314 | 315 | /** 316 | * return a point representing a vertex, the point is added to the vertexPoints set on creation 317 | * @param vertex 318 | * @returns {algo.layout.GraphForceDirected.Point} 319 | */ 320 | algo.layout.GraphForceDirected.prototype.point = function (vertex) { 321 | if (!(vertex.id in this.vertexPoints)) { 322 | var mass = vertex.mass || 1.0; 323 | this.vertexPoints[vertex.id] = new algo.layout.GraphForceDirected.Point(algo.layout.Vector.random(), mass); 324 | } 325 | 326 | return this.vertexPoints[vertex.id]; 327 | }; 328 | 329 | /** 330 | * create a sprint from a graph edge 331 | * @param edge 332 | * @returns {algo.layout.GraphForceDirected.Spring} 333 | */ 334 | algo.layout.GraphForceDirected.prototype.spring = function (edge) { 335 | if (!(edge.id in this.edgeSprings)) { 336 | var length = edge.length || 1.0; 337 | 338 | var existingSpring = false; 339 | 340 | var from = this.graph.getEdges(edge.source, edge.target); 341 | from.forEach(function (e) { 342 | if (existingSpring === false && e.id in this.edgeSprings) { 343 | existingSpring = this.edgeSprings[e.id]; 344 | } 345 | }, this); 346 | 347 | if (existingSpring !== false) { 348 | return new algo.layout.GraphForceDirected.Spring(existingSpring.point1, existingSpring.point2, 0.0, 0.0); 349 | } 350 | 351 | var to = this.graph.getEdges(edge.target, edge.source); 352 | from.forEach(function (e) { 353 | if (existingSpring === false && e.id in this.edgeSprings) { 354 | existingSpring = this.edgeSprings[e.id]; 355 | } 356 | }, this); 357 | 358 | if (existingSpring !== false) { 359 | return new algo.layout.GraphForceDirected.Spring(existingSpring.point2, existingSpring.point1, 0.0, 0.0); 360 | } 361 | 362 | this.edgeSprings[edge.id] = new algo.layout.GraphForceDirected.Spring( 363 | this.point(edge.source), this.point(edge.target), length, this.stiffness 364 | ); 365 | } 366 | 367 | return this.edgeSprings[edge.id]; 368 | }; 369 | 370 | /** 371 | * callback for each vertex in the graph. The callback is invoked with (vertex, point) 372 | * @param callback 373 | */ 374 | algo.layout.GraphForceDirected.prototype.eachVertex = function (callback) { 375 | 376 | var t = this; 377 | _.values(this.graph.vertices).forEach(function (n) { 378 | callback.call(t, n, t.point(n)); 379 | }); 380 | }; 381 | 382 | /** 383 | * callback for each edge in the graph. Callback is invoked with (edge, spring) 384 | * @param callback 385 | */ 386 | algo.layout.GraphForceDirected.prototype.eachEdge = function (callback) { 387 | var t = this; 388 | _.values(this.graph.edges).forEach(function (e) { 389 | callback.call(t, e, t.spring(e)); 390 | }); 391 | }; 392 | 393 | /** 394 | * callback for each spring in the visualizer. Callback is invoked with (spring) 395 | * @param callback 396 | */ 397 | algo.layout.GraphForceDirected.prototype.eachSpring = function (callback) { 398 | var t = this; 399 | _.values(this.graph.edges).forEach(function (e) { 400 | callback.call(t, t.spring(e)); 401 | }); 402 | }; 403 | 404 | /** 405 | * apply the repulsive force to each vertex against each other vertex 406 | */ 407 | algo.layout.GraphForceDirected.prototype.applyCoulombsLaw = function () { 408 | this.eachVertex(function (n1, point1) { 409 | this.eachVertex(function (n2, point2) { 410 | if (point1 !== point2) { 411 | var d = point1.p.subtract(point2.p); 412 | var distance = d.magnitude() + 0.1; // avoid massive forces at small distances (and divide by zero) 413 | var direction = d.normalise(); 414 | 415 | // apply force to each end point 416 | point1.applyForce(direction.multiply(this.repulsion).divide(distance * distance * 0.5)); 417 | point2.applyForce(direction.multiply(this.repulsion).divide(distance * distance * -0.5)); 418 | } 419 | }); 420 | }); 421 | }; 422 | 423 | /** 424 | * apply Hookes spring law ( attractive force ) on each spring in the visualizer 425 | */ 426 | algo.layout.GraphForceDirected.prototype.applyHookesLaw = function () { 427 | this.eachSpring(function (spring) { 428 | var d = spring.point2.p.subtract(spring.point1.p); // the direction of the spring 429 | var displacement = spring.length - d.magnitude(); 430 | var direction = d.normalise(); 431 | 432 | // apply force to each end point 433 | spring.point1.applyForce(direction.multiply(spring.k * displacement * -0.5)); 434 | spring.point2.applyForce(direction.multiply(spring.k * displacement * 0.5)); 435 | }); 436 | }; 437 | 438 | /** 439 | * all vertices/points are generally attracted to the center 440 | */ 441 | algo.layout.GraphForceDirected.prototype.attractToCentre = function () { 442 | this.eachVertex(function (vertex, point) { 443 | var direction = point.p.multiply(-1.0); 444 | point.applyForce(direction.multiply(this.repulsion / 50.0)); 445 | }); 446 | }; 447 | 448 | /** 449 | * update the velocity of each vertex 450 | * @param timestep 451 | */ 452 | algo.layout.GraphForceDirected.prototype.updateVelocity = function (timestep) { 453 | this.eachVertex(function (vertex, point) { 454 | // Is this, along with updatePosition below, the only places that your 455 | // integration code exist? 456 | point.v = point.v.add(point.a.multiply(timestep)).multiply(this.damping); 457 | point.a = new algo.layout.Vector(0, 0); 458 | }); 459 | }; 460 | 461 | /** 462 | * update the position of each point 463 | * @param timestep 464 | */ 465 | algo.layout.GraphForceDirected.prototype.updatePosition = function (timestep) { 466 | this.eachVertex(function (vertex, point) { 467 | // Same question as above; along with updateVelocity, is this all of 468 | // your integration code? 469 | point.p = point.p.add(point.v.multiply(timestep)); 470 | }); 471 | }; 472 | 473 | /** 474 | * return the total energy in the system. This is used to short circuit the update 475 | * when a near stable arrangement is obtained 476 | * @returns {number} 477 | */ 478 | algo.layout.GraphForceDirected.prototype.totalEnergy = function () { 479 | var energy = 0.0; 480 | this.eachVertex(function (vertex, point) { 481 | var speed = point.v.magnitude(); 482 | energy += 0.5 * point.m * speed * speed; 483 | }); 484 | 485 | return energy; 486 | }; 487 | 488 | /** 489 | * get the bounding box of the visualizer layout. This is within the visualizers internal 490 | * coordinate system. Not screen or surface space 491 | * 492 | * @returns {{bottomleft: algo.layout.Vector, topright: algo.layout.Vector}} 493 | */ 494 | algo.layout.GraphForceDirected.prototype.getBoundingBox = function () { 495 | var bottomleft = new algo.layout.Vector(-2, -2); 496 | var topright = new algo.layout.Vector(2, 2); 497 | 498 | this.eachVertex(function (n, point) { 499 | if (point.p.x < bottomleft.x) { 500 | bottomleft.x = point.p.x; 501 | } 502 | if (point.p.y < bottomleft.y) { 503 | bottomleft.y = point.p.y; 504 | } 505 | if (point.p.x > topright.x) { 506 | topright.x = point.p.x; 507 | } 508 | if (point.p.y > topright.y) { 509 | topright.y = point.p.y; 510 | } 511 | }); 512 | 513 | return {bottomleft: bottomleft, topright: topright}; 514 | }; 515 | 516 | /** 517 | * point instances are used to represent vertices in the graph along with their physical properties 518 | * @param position 519 | * @param mass 520 | * @constructor 521 | */ 522 | algo.layout.GraphForceDirected.Point = function (position, mass) { 523 | this.p = position; // position 524 | this.m = mass; // mass 525 | this.v = new algo.layout.Vector(0, 0); // velocity 526 | this.a = new algo.layout.Vector(0, 0); // acceleration 527 | }; 528 | 529 | /** 530 | * apply a force to a point 531 | * @param force 532 | */ 533 | algo.layout.GraphForceDirected.Point.prototype.applyForce = function (force) { 534 | this.a = this.a.add(force.divide(this.m)); 535 | }; 536 | 537 | /** 538 | * springs represent edges in the force directed layout visualizer 539 | * @param point1 540 | * @param point2 541 | * @param length 542 | * @param k 543 | * @constructor 544 | */ 545 | algo.layout.GraphForceDirected.Spring = function (point1, point2, length, k) { 546 | this.point1 = point1; 547 | this.point2 = point2; 548 | this.length = length; // spring length at rest 549 | this.k = k; // spring constant (See Hooke's law) .. how stiff the spring is 550 | }; 551 | 552 | /** 553 | * the generic results of various types of intersection test. 554 | * For valid intersections the points property is an array of 555 | * algo.layout.Vector objects. There is also a point property that returns 556 | * the first point in the points array. The status property is a string that indicates why the intersection test 557 | * failed if any 558 | * @constructor 559 | * @param {object} arg - can be a vector or a status string or nothing 560 | */ 561 | algo.layout.Intersection = function (arg) { 562 | 563 | if (arg instanceof algo.layout.Vector) { 564 | this.points = [arg]; 565 | } else { 566 | if (_.isString(arg)) { 567 | this.status = arg; 568 | } 569 | this.points = []; 570 | } 571 | 572 | /** 573 | * return the first point of our results, or null if no points 574 | */ 575 | Object.defineProperty(this, 'point', { 576 | enumerable: true, 577 | get : function () { 578 | if (this.points && this.points.length > 0) { 579 | return this.points[0]; 580 | } 581 | 582 | return null; 583 | } 584 | }); 585 | 586 | // status of intersection 587 | 588 | Object.defineProperty(this, 'status', { 589 | enumerable: true, 590 | get : function () { 591 | return this._status; 592 | }, 593 | set : function (s) { 594 | this._status = s; 595 | return this; 596 | } 597 | }); 598 | }; 599 | 600 | /** 601 | * add an object with x/y values to the results 602 | * @param p 603 | */ 604 | algo.layout.Intersection.prototype.addPoint = function (p) { 605 | 606 | if (p) { 607 | 608 | this.points = this.points || []; 609 | 610 | this.points.push(new algo.layout.Vector(p.x, p.y)); 611 | } 612 | 613 | return this; 614 | }; 615 | 616 | /** 617 | * basic vector class, not to be confused with a line which is not 618 | * necessarily anchored at the origin 619 | * @param x 620 | * @param y 621 | * @constructor 622 | */ 623 | algo.layout.Vector = function (x, y) { 624 | 625 | this.x = x; 626 | this.y = y; 627 | }; 628 | 629 | /** 630 | * return a new vector that is lerped toward that by the parametric value t 631 | * @param {algo.layout.Vector} that 632 | * @param {number} t 633 | * @returns {algo.layout.Vector} 634 | */ 635 | algo.layout.Vector.prototype.lerp = function (that, t) { 636 | return new algo.layout.Vector( 637 | this.x + (that.x - this.x) * t, 638 | this.y + (that.y - this.y) * t 639 | ); 640 | }; 641 | 642 | /** 643 | * make a random vector between -0.5 and + 0.5 644 | * @return {algo.layout.Vector} 645 | */ 646 | algo.layout.Vector.random = function () { 647 | 648 | return new algo.layout.Vector(Math.random() - 0.5, Math.random() - 0.5); 649 | }; 650 | 651 | /** 652 | * add v2 and return a new vector 653 | * @param v2 654 | * @returns {algo.layout.Vector} 655 | */ 656 | algo.layout.Vector.prototype.add = function (v2) { 657 | return new algo.layout.Vector(this.x + v2.x, this.y + v2.y); 658 | }; 659 | 660 | /** 661 | * subtract v2 and return a new vector 662 | * @param v2 663 | * @returns {algo.layout.Vector} 664 | */ 665 | algo.layout.Vector.prototype.subtract = function (v2) { 666 | return new algo.layout.Vector(this.x - v2.x, this.y - v2.y); 667 | }; 668 | 669 | /** 670 | * multiple by n and return a new vector 671 | * @param n 672 | * @returns {algo.layout.Vector} 673 | */ 674 | algo.layout.Vector.prototype.multiply = function (n) { 675 | return new algo.layout.Vector(this.x * n, this.y * n); 676 | }; 677 | 678 | /** 679 | * divide self by n and return a new vector 680 | * @param n 681 | * @returns {algo.layout.Vector} 682 | */ 683 | algo.layout.Vector.prototype.divide = function (n) { 684 | return new algo.layout.Vector((this.x / n) || 0, (this.y / n) || 0); // Avoid divide by zero errors.. 685 | }; 686 | 687 | /** 688 | * magnitude of the vector 689 | * @returns {number} 690 | */ 691 | algo.layout.Vector.prototype.magnitude = function () { 692 | return Math.sqrt(this.x * this.x + this.y * this.y); 693 | }; 694 | 695 | /** 696 | * normal of the vector 697 | * @returns {algo.layout.Vector} 698 | */ 699 | algo.layout.Vector.prototype.normal = function () { 700 | return new algo.layout.Vector(-this.y, this.x); 701 | }; 702 | 703 | /** 704 | * normalize the vector, returns a new vector 705 | * @returns {*} 706 | */ 707 | algo.layout.Vector.prototype.normalise = function () { 708 | return this.divide(this.magnitude()); 709 | }; 710 | 711 | /** 712 | * clone into new Vector 713 | * @returns {algo.layout.Vector} 714 | */ 715 | algo.layout.Vector.prototype.clone = function () { 716 | 717 | return new algo.layout.Vector(this.x, this.y); 718 | }; 719 | 720 | /** 721 | * get the angle between this Vector and another in degrees 722 | * @param {algo.layout.Vector} other 723 | * @returns {number} polar angle between two points in degrees 724 | */ 725 | algo.layout.Vector.prototype.angle = function(other) { 726 | 727 | // first calculate the angle from this.x/this.y to this.p.x2/this.p.y2 728 | 729 | var rads = Math.atan2(other.y - this.y, other.x - this.x); 730 | 731 | // atan2 return negative PI radians for the 180-360 degrees ( 9 o'clock to 3 o'clock ) 732 | 733 | if (rads < 0) { 734 | 735 | rads = 2 * Math.PI + rads; 736 | } 737 | 738 | return algo.core.radiansToDegrees(rads); 739 | 740 | }; 741 | 742 | /** 743 | * a line object. Has vector like properties as well an intersection testing. 744 | * Most usefully, it has a static getConnector method that returns a line attached to the boundary of 745 | * any pair of objects with vector/line/circle/box type properties 746 | * @param x1 747 | * @param y1 748 | * @param x2 749 | * @param y2 750 | * @constructor 751 | */ 752 | algo.layout.Line = function (x1, y1, x2, y2) { 753 | 754 | this.x1 = x1; 755 | 756 | this.y1 = y1; 757 | 758 | this.x2 = x2; 759 | 760 | this.y2 = y2; 761 | 762 | // x extent of line 763 | 764 | Object.defineProperty(this, 'dx', { 765 | enumerable: true, 766 | get : function () { 767 | return this.x2 - this.x1; 768 | } 769 | }); 770 | 771 | // y extent of line 772 | 773 | Object.defineProperty(this, 'dy', { 774 | enumerable: true, 775 | get : function () { 776 | return this.y2 - this.y1; 777 | } 778 | }); 779 | 780 | // length of line ( magnitude ) 781 | 782 | Object.defineProperty(this, 'length', { 783 | enumerable: true, 784 | get : function () { 785 | return Math.sqrt(this.dx * this.dx + this.dy * this.dy); 786 | } 787 | }); 788 | }; 789 | 790 | /** 791 | * return a line representing the top edge of any box/rectangle like object 792 | * @param {*} r - a rectangle like object 793 | * @returns {algo.layout.Line} 794 | */ 795 | algo.layout.Line.topEdge = function (r) { 796 | return new algo.layout.Line(r.x, r.y, r.x + r.w, r.y); 797 | }; 798 | /** 799 | * return a line representing the bottom edge of any box/rectangle like object 800 | * @param {*} r - a rectangle like object 801 | * @returns {algo.layout.Line} 802 | */ 803 | algo.layout.Line.bottomEdge = function (r) { 804 | return new algo.layout.Line(r.x, r.y + r.h, r.x + r.w, r.y + r.h); 805 | }; 806 | /** 807 | * return a line representing the left edge of any box/rectangle like object 808 | * @param {*} r - a rectangle like object 809 | * @returns {algo.layout.Line} 810 | */ 811 | algo.layout.Line.leftEdge = function (r) { 812 | return new algo.layout.Line(r.x, r.y, r.x, r.y + r.h); 813 | }; 814 | /** 815 | * return a line representing the right edge of any box/rectangle like object 816 | * @param {*} r - a rectangle like object 817 | * @returns {algo.layout.Line} 818 | */ 819 | algo.layout.Line.rightEdge = function (r) { 820 | return new algo.layout.Line(r.x + r.w, r.y, r.x + r.w, r.y + r.h); 821 | }; 822 | 823 | /** 824 | * return a line object that connects the boundary of start to the boundary of end. 825 | * degenerates cases (overlapping objects etc) will return a line connecting the center of the objects. 826 | * start and end can be any combination of point like, rectangle like or circle like objects e.g. 827 | * algo.layout.Vector or {x:0, y:0} or algo.layout.Rect or algo.render.Rect etc. 828 | * 829 | 830 | * 831 | * @param start - see above 832 | * @param end - see above 833 | */ 834 | algo.layout.Line.getConnector = function (start, end) { 835 | 836 | // these will become the start and end of the line, initially we set them to the center of the objects 837 | // so we intersect the results vector with the shape boundaries 838 | var p1, p2, temp; 839 | 840 | if (algo.core.isRectLike(start)) { 841 | 842 | p1 = new algo.layout.Vector(start.x + start.w / 2, start.y + start.h / 2); 843 | 844 | } else if (algo.core.isCircleLike(start)) { 845 | 846 | p1 = new algo.layout.Vector(start.x, start.y); 847 | 848 | } else if (algo.core.isPointLike(start)) { 849 | 850 | p1 = new algo.layout.Vector(start.x, start.y); 851 | 852 | } else { 853 | throw new Error("Unrecognized object passed to Line::getConnector"); 854 | } 855 | 856 | if (algo.core.isRectLike(end)) { 857 | 858 | p2 = new algo.layout.Vector(end.x + end.w / 2, end.y + end.h / 2); 859 | 860 | } else if (algo.core.isCircleLike(end)) { 861 | 862 | p2 = new algo.layout.Vector(end.x, end.y); 863 | 864 | } else if (algo.core.isPointLike(end)) { 865 | 866 | p2 = new algo.layout.Vector(end.x, end.y); 867 | 868 | } else { 869 | throw new Error("Unrecognized object passed to Line::getConnector"); 870 | } 871 | 872 | // now p1->p2 is a line from the center of each shape, adjust to boundary for circles and rectangle 873 | 874 | var line = new algo.layout.Line(p1.x, p1.y, p2.x, p2.y); 875 | 876 | // start adjustment 877 | 878 | if (algo.core.isRectLike(start)) { 879 | 880 | temp = line.intersectWithBox(start); 881 | if (temp.point) { 882 | line.x1 = temp.point.x; 883 | line.y1 = temp.point.y; 884 | } 885 | } else { 886 | if (algo.core.isCircleLike(start)) { 887 | 888 | temp = line.intersectWithCircle(start); 889 | if (temp.point) { 890 | line.x1 = temp.point.x; 891 | line.y1 = temp.point.y; 892 | } 893 | } 894 | } 895 | 896 | // end adjustment 897 | 898 | if (algo.core.isRectLike(end)) { 899 | 900 | temp = line.intersectWithBox(end); 901 | if (temp.point) { 902 | line.x2 = temp.point.x; 903 | line.y2 = temp.point.y; 904 | } 905 | } else { 906 | if (algo.core.isCircleLike(end)) { 907 | 908 | temp = line.intersectWithCircle(end); 909 | if (temp.point) { 910 | line.x2 = temp.point.x; 911 | line.y2 = temp.point.y; 912 | } 913 | } 914 | } 915 | 916 | return line; 917 | 918 | }; 919 | 920 | /** 921 | * intersection of this line with another line. 922 | * @param {algo.layout.Line} other - other line segment to intersect with 923 | * @returns {algo.layout.Vector} 924 | */ 925 | algo.layout.Line.prototype.intersectWithLine = function (other) { 926 | 927 | var result; 928 | 929 | var ua_t = (other.x2 - other.x1) * (this.y1 - other.y1) - (other.y2 - other.y1) * (this.x1 - other.x1); 930 | var ub_t = (this.x2 - this.x1) * (this.y1 - other.y1) - (this.y2 - this.y1) * (this.x1 - other.x1); 931 | var u_b = (other.y2 - other.y1) * (this.x2 - this.x1) - (other.x2 - other.x1) * (this.y2 - this.y1); 932 | 933 | if (u_b !== 0) { 934 | var ua = ua_t / u_b; 935 | var ub = ub_t / u_b; 936 | 937 | if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) { 938 | 939 | result = new algo.layout.Intersection(new algo.layout.Vector( 940 | this.x1 + ua * (this.x2 - this.x1), 941 | this.y1 + ua * (this.y2 - this.y1) 942 | )); 943 | 944 | result.status = "Intersection"; 945 | 946 | } else { 947 | result = new algo.layout.Intersection("No Intersection"); 948 | } 949 | } else { 950 | if (ua_t === 0 || ub_t === 0) { 951 | result = new algo.layout.Intersection("Coincident"); 952 | } else { 953 | result = new algo.layout.Intersection("Parallel"); 954 | } 955 | } 956 | 957 | return result; 958 | }; 959 | 960 | /** 961 | * intersect the line with a Box. This can result in 0,1,2 points of intersection. 962 | * @param box - any rectangle like object 963 | * @returns {algo.layout.Intersection} 964 | */ 965 | algo.layout.Line.prototype.intersectWithBox = function (box) { 966 | 967 | var result = new algo.layout.Intersection(); 968 | 969 | result.addPoint(this.intersectWithLine(algo.layout.Line.topEdge(box)).point); 970 | result.addPoint(this.intersectWithLine(algo.layout.Line.rightEdge(box)).point); 971 | result.addPoint(this.intersectWithLine(algo.layout.Line.bottomEdge(box)).point); 972 | result.addPoint(this.intersectWithLine(algo.layout.Line.leftEdge(box)).point); 973 | 974 | result.status = result.points ? "Intersection" : "No Intersection"; 975 | 976 | return result; 977 | }; 978 | 979 | /** 980 | * line with circle intersection from 981 | * @param circle - circle like object (x/y/radius) 982 | * @returns {algo.layout.Intersection} - containing 0 or 1 or 2 points 983 | */ 984 | algo.layout.Line.prototype.intersectWithCircle = function (c) { 985 | 986 | var a1 = new algo.layout.Vector(this.x1, this.y1), 987 | a2 = new algo.layout.Vector(this.x2, this.y2), 988 | r = c.radius; 989 | 990 | var result; 991 | 992 | var a = (a2.x - a1.x) * (a2.x - a1.x) + 993 | (a2.y - a1.y) * (a2.y - a1.y); 994 | var b = 2 * ( (a2.x - a1.x) * (a1.x - c.x) + 995 | (a2.y - a1.y) * (a1.y - c.y) ); 996 | var cc = c.x * c.x + c.y * c.y + a1.x * a1.x + a1.y * a1.y - 997 | 2 * (c.x * a1.x + c.y * a1.y) - r * r; 998 | var deter = b * b - 4 * a * cc; 999 | 1000 | if (deter < 0) { 1001 | result = new algo.layout.Intersection("Outside"); 1002 | } else if (deter === 0) { 1003 | result = new algo.layout.Intersection("Tangent"); 1004 | // NOTE: should calculate this point 1005 | } else { 1006 | var e = Math.sqrt(deter); 1007 | var u1 = ( -b + e ) / ( 2 * a ); 1008 | var u2 = ( -b - e ) / ( 2 * a ); 1009 | 1010 | if ((u1 < 0 || u1 > 1) && (u2 < 0 || u2 > 1)) { 1011 | if ((u1 < 0 && u2 < 0) || (u1 > 1 && u2 > 1)) { 1012 | result = new algo.layout.Intersection("Outside"); 1013 | } else { 1014 | result = new algo.layout.Intersection("Inside"); 1015 | } 1016 | } else { 1017 | result = new algo.layout.Intersection("Intersection"); 1018 | 1019 | if (0 <= u1 && u1 <= 1) 1020 | result.points.push(a1.lerp(a2, u1)); 1021 | 1022 | if (0 <= u2 && u2 <= 1) 1023 | result.points.push(a1.lerp(a2, u2)); 1024 | } 1025 | } 1026 | 1027 | return result; 1028 | }; 1029 | 1030 | /** 1031 | * get the angle in radians between the start and the end of the line 1032 | * @return {number} angle in radians between start and end of line 1033 | */ 1034 | algo.layout.Line.prototype.angleStartEnd = function () { 1035 | 1036 | // first calculate the angle from this.x/this.y to this.p.x2/this.p.y2 1037 | 1038 | var rads = Math.atan2(this.y2 - this.y1, this.x2 - this.x1); 1039 | 1040 | // atan2 return negative PI radians for the 180-360 degrees ( 9 o'clock to 3 o'clock ) 1041 | 1042 | if (rads < 0) { 1043 | 1044 | rads = 2 * Math.PI + rads; 1045 | } 1046 | 1047 | return rads; 1048 | }; 1049 | 1050 | /** 1051 | * a basic circle class. 1052 | * @param cx 1053 | * @param cy 1054 | * @param radius 1055 | * @constructor 1056 | */ 1057 | algo.layout.Circle = function (x, y, radius) { 1058 | 1059 | this.x = x; 1060 | this.y = y; 1061 | this.radius = radius; 1062 | }; 1063 | 1064 | /** 1065 | * return a cloned copy of this 1066 | */ 1067 | algo.layout.Circle.prototype.clone = function () { 1068 | 1069 | return new algo.layout.Circle(this.x, this.y, this.radius); 1070 | }; 1071 | 1072 | /** 1073 | * return a new Cicle inflated by the given signed delta 1074 | * @param inflateX 1075 | * @param inflateY 1076 | */ 1077 | algo.layout.Circle.prototype.inflate = function (delta) { 1078 | 1079 | return new algo.layout.Circle(this.x, this.y, this.radius + delta); 1080 | }; 1081 | 1082 | /** 1083 | * axis aligned box 1084 | * @param x 1085 | * @param y 1086 | * @param w 1087 | * @param h 1088 | * @constructor 1089 | */ 1090 | algo.layout.Box = function (x, y, w, h) { 1091 | 1092 | // initialize 1093 | 1094 | this.x = x || 0; 1095 | this.y = y || 0; 1096 | this.w = w || 0; 1097 | this.h = h || 0; 1098 | 1099 | Object.defineProperty(this, 'r', { 1100 | enumerable: true, 1101 | get : function () { 1102 | 1103 | return this.x + this.w; 1104 | }, 1105 | set : function (_r) { 1106 | 1107 | this.w = _r - this.x; 1108 | } 1109 | }); 1110 | 1111 | Object.defineProperty(this, 'b', { 1112 | enumerable: true, 1113 | get : function () { 1114 | 1115 | return this.y + this.h; 1116 | }, 1117 | set : function (_b) { 1118 | 1119 | this.h = _b - this.y; 1120 | } 1121 | }); 1122 | 1123 | Object.defineProperty(this, 'cx', { 1124 | enumerable: true, 1125 | get : function () { 1126 | 1127 | return this.x + this.w / 2; 1128 | }, 1129 | set : function (cx) { 1130 | 1131 | this.x = cx - this.w / 2; 1132 | } 1133 | }); 1134 | 1135 | Object.defineProperty(this, 'cy', { 1136 | enumerable: true, 1137 | get : function () { 1138 | 1139 | return this.y + this.h / 2; 1140 | }, 1141 | set : function (cy) { 1142 | 1143 | this.y = cy - this.h / 2; 1144 | } 1145 | }); 1146 | 1147 | /** 1148 | * get/set center as point/vector 1149 | */ 1150 | Object.defineProperty(this, 'center', { 1151 | enumerable: true, 1152 | get : function () { 1153 | return new algo.layout.Vector(this.cx, this.cy); 1154 | }, 1155 | set : function (v) { 1156 | 1157 | this.cx = v.x; 1158 | this.cy = v.y; 1159 | } 1160 | }); 1161 | 1162 | }; 1163 | 1164 | /** 1165 | * return a new box that is this box multiplied by the given vector. This is useful for scaling boxes 1166 | * @param {algo.layout.Vector} v 1167 | * @return {algo.layout.Box} 1168 | */ 1169 | algo.layout.Box.prototype.mul = function (v) { 1170 | 1171 | return new algo.layout.Box(this.x * v.x, this.y * v.y, this.w * v.x, this.h * v.y); 1172 | }; 1173 | 1174 | /** 1175 | * return a new box that is offset by the given vector 1176 | * @param {algo.layout.Vector} v 1177 | * @return {algo.layout.Box} 1178 | */ 1179 | algo.layout.Box.prototype.add = function (v) { 1180 | 1181 | return new algo.layout.Box(this.x + v.x, this.y + v.y, this.w, this.h); 1182 | }; 1183 | 1184 | /** 1185 | * return a cloned copy of this 1186 | */ 1187 | algo.layout.Box.prototype.clone = function () { 1188 | 1189 | return new algo.layout.Box(this.x, this.y, this.w, this.h); 1190 | }; 1191 | 1192 | /** 1193 | * return a new Box inflated by the given signed amount 1194 | * @param inflateX 1195 | * @param inflateY 1196 | */ 1197 | algo.layout.Box.prototype.inflate = function (inflateX, inflateY) { 1198 | 1199 | var b = new algo.layout.Box(this.x, this.y, this.w + inflateX * 2, this.h + inflateY * 2); 1200 | b.cx = this.cx; 1201 | b.cy = this.cy; 1202 | return b; 1203 | }; 1204 | 1205 | /** 1206 | * return true if the box have zero or negative extents in either axis 1207 | */ 1208 | algo.layout.Box.prototype.isEmpty = function () { 1209 | 1210 | return this.w <= 0 || this.h <= 0; 1211 | }; 1212 | 1213 | /** 1214 | * horizontally align this box within another box using the given alignment [left, center, right] 1215 | * @param {algo.layout.Box} other - the box which we are to be aligned in 1216 | * @param {string} alignment - one of left,center,right 1217 | */ 1218 | algo.layout.Box.prototype.halign = function (other, alignment) { 1219 | 1220 | switch (alignment) { 1221 | 1222 | case 'center': 1223 | { 1224 | this.x = other.x + (other.w - this.w) / 2; 1225 | } 1226 | break; 1227 | 1228 | case 'right': 1229 | { 1230 | this.x = other.r - this.w; 1231 | } 1232 | break; 1233 | 1234 | default: 1235 | { 1236 | this.x = other.x; 1237 | } 1238 | break; 1239 | } 1240 | }; 1241 | 1242 | /** 1243 | * vertically align this box within another box using the given alignment [top, center, bottom] 1244 | * @param {algo.layout.Box} other - the box which we are to be aligned in 1245 | * @param {string} alignment - one of top, center, bottom 1246 | */ 1247 | algo.layout.Box.prototype.valign = function (other, alignment) { 1248 | 1249 | switch (alignment) { 1250 | 1251 | case 'center': 1252 | { 1253 | this.y = other.y + (other.h - this.h) / 2; 1254 | } 1255 | break; 1256 | 1257 | case 'bottom': 1258 | { 1259 | this.y = other.b - this.h; 1260 | } 1261 | break; 1262 | 1263 | default: 1264 | { 1265 | 1266 | this.y = other.y; 1267 | } 1268 | 1269 | } 1270 | }; 1271 | 1272 | /** 1273 | * center ourselves in the given box 1274 | * @param other 1275 | */ 1276 | algo.layout.Box.prototype.center = function (other) { 1277 | 1278 | this.halign(other, 'center'); 1279 | this.valign(other, 'center'); 1280 | }; 1281 | 1282 | /** 1283 | * return a new box that is the union of this box and some other box/rect like object 1284 | * @param {algo.layout.Box|algo.render.Rectangle|*} box - anything with x,y,w,h properties 1285 | * @returns algo.layout.Box - the union of this and box 1286 | */ 1287 | algo.layout.Box.prototype.union = function (box) { 1288 | 1289 | var u = new algo.layout.Box( 1290 | Math.min(this.x, box.x), 1291 | Math.min(this.y, box.y), 1292 | 0, 0 1293 | ); 1294 | 1295 | u.r = Math.max(this.r, box.x + box.w); 1296 | u.b = Math.max(this.b, box.y + box.h); 1297 | 1298 | return u; 1299 | }; 1300 | 1301 | /** 1302 | * return the union of the given boxes or an empty box if the list is empty 1303 | * @static 1304 | */ 1305 | algo.layout.Box.union = function (boxes) { 1306 | 1307 | var u = new algo.layout.Box(0, 0, 0, 0); 1308 | 1309 | if (boxes && boxes.length) { 1310 | 1311 | u.x = _.min(boxes, function (box) { 1312 | return box.x; 1313 | }).x; 1314 | 1315 | u.y = _.min(boxes, function (box) { 1316 | return box.y; 1317 | }).y; 1318 | 1319 | u.r = _.max(boxes, function (box) { 1320 | return box.r; 1321 | }).r; 1322 | 1323 | u.b = _.max(boxes, function (box) { 1324 | return box.b; 1325 | }).b; 1326 | } 1327 | 1328 | return u; 1329 | }; 1330 | 1331 | /** 1332 | * return the intersection of this box with the other box 1333 | * @param box 1334 | */ 1335 | algo.layout.Box.intersectWithBox = function (box) { 1336 | 1337 | // minimum of right edges 1338 | 1339 | var minx = Math.min(this.r, box.r); 1340 | 1341 | // maximum of left edges 1342 | 1343 | var maxx = Math.max(this.x, box.x); 1344 | 1345 | // minimum of bottom edges 1346 | 1347 | var miny = Math.min(this.b, box.b); 1348 | 1349 | // maximum of top edges 1350 | 1351 | var maxy = Math.max(this.y, box.y); 1352 | 1353 | // if area is greater than zero there is an intersection 1354 | 1355 | if (maxx < minx && maxy < miny) { 1356 | 1357 | var x = Math.min(minx, maxx); 1358 | 1359 | var y = Math.min(miny, maxy); 1360 | 1361 | var w = Math.max(minx, maxx) - x; 1362 | 1363 | var h = Math.max(miny, maxy) - y; 1364 | 1365 | return new algo.layout.Box(x, y, w, h); 1366 | 1367 | } 1368 | 1369 | return null; 1370 | }; 1371 | 1372 | /** 1373 | * return an array of points or objects within this box. If a callback is provided then the object returns is 1374 | * created by the callback which is invoked with the x/y position. If no callback is provided the resulting array 1375 | * elements are of type algo.layout.Vector 1376 | * @param {number} n - the number of objects to create 1377 | * @param {Function} [callback] - optional callback for creating the object 1378 | */ 1379 | algo.layout.Box.prototype.pointSet = function (n, callback) { 1380 | 1381 | var results = []; 1382 | var xgen = algo.core.randomFloat(this.x, this.r), 1383 | ygen = algo.core.randomFloat(this.y, this.b); 1384 | 1385 | for (var i = 0; i < n; i += 1) { 1386 | var p = new algo.layout.Vector(xgen(), ygen()); 1387 | results.push(callback ? callback(p.x, p.y) : p); 1388 | } 1389 | return results; 1390 | }; 1391 | 1392 | /** 1393 | * create a grid layout within the given box with the given numbers of rows and columns. 1394 | * We keep a reference to the original box object. You can change the bounds, rows and columns layer with 1395 | * setBox and setRowsAndColumns 1396 | */ 1397 | algo.layout.GridLayout = function (box, rows, columns, options) { 1398 | 1399 | this.setBox(box); 1400 | 1401 | this.setRowsAndColumns(rows, columns); 1402 | 1403 | // clone and extend options. 1404 | // inflateX/Y are values by which the returned boxes are inflated. 1405 | 1406 | this.options = _.defaults(_.clone(options || {}), { 1407 | inflateX: 0, 1408 | inflateY: 0 1409 | }); 1410 | }; 1411 | 1412 | /** 1413 | * set the bounding box for the grid 1414 | * @param box 1415 | */ 1416 | algo.layout.GridLayout.prototype.setBox = function (box) { 1417 | 1418 | this.box = box.clone(); 1419 | }; 1420 | 1421 | /** 1422 | * set the number of rows and columns 1423 | * @param rows 1424 | * @param columns 1425 | */ 1426 | algo.layout.GridLayout.prototype.setRowsAndColumns = function (rows, columns) { 1427 | 1428 | this.rows = rows; 1429 | 1430 | this.columns = columns; 1431 | }; 1432 | 1433 | /** 1434 | * a box representing the bounds of the given cell. 1435 | * @param {number} rowOrIndex - the row of the grid 0..this.rows-1 OR the index of the box 0..(this.rows * this.columns-1) 1436 | * @param {number} [column] - the column of the grid 0..this.columns-1 1437 | * @returns {algo.layout.Box} 1438 | */ 1439 | algo.layout.GridLayout.prototype.getBox = function (rowOrIndex, column) { 1440 | 1441 | // get row and column 1442 | var r = arguments.length === 1 ? Math.floor(rowOrIndex / this.columns) : rowOrIndex; 1443 | var c = arguments.length === 1 ? rowOrIndex % this.columns : column; 1444 | 1445 | // dimensions of boxes 1446 | var cx = this.box.w / this.columns, cy = this.box.h / this.rows; 1447 | 1448 | // return box allowing for the optional inflation ( defaults to zero ) 1449 | return new algo.layout.Box(this.box.x + c * cx, this.box.y + r * cy, cx, cy).inflate(this.options.inflateX, this.options.inflateY); 1450 | }; 1451 | 1452 | /** 1453 | * return a rectangle for the given row 1454 | * @param {number} row 1455 | * @returns {algo.layout.Box} 1456 | */ 1457 | algo.layout.GridLayout.prototype.getRowBox = function(row) { 1458 | 1459 | return this.getBox(row, 0).union(this.getBox(row, this.columns-1)); 1460 | }; 1461 | 1462 | /** 1463 | * return a rectangle for the given column 1464 | * @param {number} row 1465 | * @returns {algo.layout.Box} 1466 | */ 1467 | algo.layout.GridLayout.prototype.getColumnBox = function(col) { 1468 | 1469 | return this.getBox(0, col).union(this.getBox(this.rows-1, col)); 1470 | }; 1471 | 1472 | /** 1473 | * for debugging, create a visual representation of the layout, remove any previous 1474 | * visualization of the layout 1475 | */ 1476 | algo.layout.GridLayout.prototype.debugShow = function () { 1477 | 1478 | if (this.debugElements) { 1479 | this.debugElements.destroy(); 1480 | } 1481 | 1482 | this.debugElements = this.debugElements || new algo.render.ElementGroup(); 1483 | 1484 | for (var y = 0; y < this.rows; y += 1) { 1485 | for (var x = 0; x < this.columns; x += 1) { 1486 | 1487 | var b = this.getBox(y, x); 1488 | 1489 | var e = new algo.render.Rectangle({ 1490 | 1491 | x : b.x, 1492 | y : b.y, 1493 | w : b.w, 1494 | h : b.h, 1495 | strokeWidth: 1, 1496 | stroke : 'lightgray', 1497 | fill : 'transparent' 1498 | 1499 | }); 1500 | 1501 | this.debugElements.add(e); 1502 | } 1503 | } 1504 | }; 1505 | 1506 | /** 1507 | * The binary tree layout strategy for heaps. s 1508 | * @param {algo.core.BinaryTree} dataStructure 1509 | * @constructor 1510 | */ 1511 | algo.layout.HeapTree = function (dataStructure) { 1512 | 1513 | algo.layout.Strategy.call(this, dataStructure); 1514 | 1515 | // make this.heap syntactic sugar for this.dataStructure 1516 | 1517 | this.heap = this.dataStructure; 1518 | 1519 | // we create and destroy edges as need. This is a hash of the edges 1520 | // currently in use...The key is simple the index of the child vertex 1521 | 1522 | this.edgeMap = {}; 1523 | }; 1524 | 1525 | algo.core.extends(algo.layout.Strategy, algo.layout.HeapTree); 1526 | 1527 | /** 1528 | * perform Knuth binary tree layout on a heap 1529 | * @param {algo.layout.Box} box 1530 | */ 1531 | algo.layout.HeapTree.prototype.update = function (box) { 1532 | 1533 | // uses Knuth's simple binary tree layout algorithm. This results 1534 | // in a simple x/y (column/row) position for each vertex. We then 1535 | // use a algo.layout.GridLayout object to position each vertex 1536 | // within the given bounding box 1537 | 1538 | // x position of nodes is a global while we perform the traversal, 1539 | // maxDepth tracks how deep the tree is. vertexMap is used to store 1540 | // the x/y position for each vertex for later updates 1541 | 1542 | var x = 0, maxDepth = 0, vertexMap = []; 1543 | 1544 | // the recursive function that traverses the tree and updates x, maxDepth 1545 | // and vertexMap 1546 | 1547 | function traverseLayout(vertex, depth) { 1548 | 1549 | if (!this.heap.isNull(vertex)) { 1550 | 1551 | // keep track of max depth 1552 | maxDepth = Math.max(maxDepth, depth); 1553 | 1554 | // go left first 1555 | traverseLayout.call(this, this.heap.leftChild(vertex), depth + 1); 1556 | 1557 | // save position for this vertex 1558 | vertexMap[vertex] = { 1559 | x: x++, 1560 | y: depth 1561 | }; 1562 | 1563 | // go right 1564 | traverseLayout.call(this, this.heap.rightChild(vertex), depth + 1); 1565 | } 1566 | } 1567 | 1568 | // traverse from root, depth 1 1569 | // TODO: Figure out why I have to use call here to preserve the scope??? 1570 | traverseLayout.call(this, algo.core.Heap.kROOT, 0); 1571 | 1572 | // create a grid layout using the calculate rows and columns required 1573 | var grid = new algo.layout.GridLayout(box, maxDepth + 1, x); 1574 | 1575 | // now update the vertex and edge positions. Any edges that we are going to reuse will get moved 1576 | // into 'newEdgeMap'. Any that remain in this.edgeMap after the update process will get destroyed. 1577 | 1578 | var newEdgeMap = {}; 1579 | 1580 | // use a stack to recur into the structure rather than a recursive function. 1581 | var stack = [algo.core.Heap.kROOT]; 1582 | 1583 | while (stack.length) { 1584 | 1585 | var current = stack.pop(); 1586 | 1587 | if (!this.heap.isNull(current)) { 1588 | // get position for this vertex 1589 | var p = vertexMap[current]; 1590 | // get box for this position 1591 | var b = grid.getBox(p.y, p.x); 1592 | // invoke the update method for the vertex, if there is one 1593 | var e = this.heap.element(current); 1594 | if (e) { 1595 | this.heap.invoke('updateVertex', this.heap.value(current), e, {x: b.cx, y: b.cy}); 1596 | } 1597 | // update the edge connecting this vertex to its parent 1598 | var edge = this.edgeMap[current]; 1599 | 1600 | // if the current vertex has a parent then we need to create/update its edge 1601 | // otherwise the edge will be destroy at the end of the update process below 1602 | 1603 | if (!this.heap.isNull(this.heap.parent(current))) { 1604 | if (!edge) { 1605 | edge = newEdgeMap[current] = this.heap.invoke('createEdge'); 1606 | } else { 1607 | // move to the newEdgeMap and remove from edgeMap so it is not destroyed 1608 | newEdgeMap[current] = edge; 1609 | delete this.edgeMap[current]; 1610 | } 1611 | 1612 | // get parent element and current element 1613 | var e1 = this.heap.element(this.heap.parent(current)); 1614 | var e2 = this.heap.element(current); 1615 | 1616 | // update the edge 1617 | this.heap.invoke('updateEdge', edge, e1, e2); 1618 | } 1619 | 1620 | // repeat for children 1621 | stack.push(this.heap.leftChild(current)); 1622 | stack.push(this.heap.rightChild(current)); 1623 | } 1624 | 1625 | } 1626 | 1627 | // any edges remaining in edgeMap must be destroyed and then we can swap 1628 | // newEdgeMap and edgeMap 1629 | 1630 | _.each(this.edgeMap, _.bind(function(edge) { 1631 | this.heap.invoke('destroyEdge', edge); 1632 | }, this)); 1633 | 1634 | this.edgeMap = newEdgeMap; 1635 | 1636 | }; 1637 | 1638 | 1639 | /** 1640 | * The directed graph layout strategy is implemented using the dagre js library 1641 | * https://github.com/cpettitt/dagre 1642 | * @param {algo.core.Graph} dataStructure 1643 | * @constructor 1644 | */ 1645 | algo.layout.GraphDirected = function (dataStructure, options) { 1646 | 1647 | algo.layout.Strategy.call(this, dataStructure); 1648 | 1649 | // syntactic sugar 1650 | this.graph = dataStructure; 1651 | 1652 | // extend ourselves with the options so we don't have to write this.options.stiffness etc 1653 | _.extend(this, _.defaults(options || {}, { 1654 | 1655 | vertexWidth : 40, 1656 | vertexHeight : 40, 1657 | nodeSeparation: 15, 1658 | edgeSeparation: 15, 1659 | rankSeparation: 15, 1660 | direction : "TB" 1661 | 1662 | })); 1663 | 1664 | }; 1665 | 1666 | /** 1667 | * Start the layout algorithm and run for the specified number of ms OR until the total energy 1668 | * in the system goes below a threshold 1669 | * @param {box} box - the bounding box for the layout 1670 | */ 1671 | algo.layout.GraphDirected.prototype.update = function (box) { 1672 | 1673 | // create and populate a dagre using from our graph data, ignore self connected edges and multi edges 1674 | 1675 | var g = new dagre.Digraph(); 1676 | 1677 | // add all vertices and edges in our graph to the dagre graph 1678 | 1679 | _.each(this.graph.vertices, _.bind(function (v) { 1680 | 1681 | g.addNode(v.id, {label: v.id, width: this.vertexWidth, height: this.vertexHeight}); 1682 | 1683 | }, this)); 1684 | 1685 | // add all edges 1686 | 1687 | _.each(this.graph.edges, _.bind(function (v) { 1688 | 1689 | g.addEdge(v.id, v.source.id, v.target.id); 1690 | 1691 | }, this)); 1692 | 1693 | // layout the graph 1694 | 1695 | var layout = dagre.layout() 1696 | .nodeSep(this.nodeSeparation) 1697 | .edgeSep(this.edgeSeparation) 1698 | .rankSep(this.rankSeparation) 1699 | .rankDir(this.direction) 1700 | .run(g); 1701 | 1702 | // get graph size 1703 | var graphSize = new algo.layout.Vector(layout.graph().width, layout.graph().height); 1704 | 1705 | // if the graph is larger than the box it will be scaled, if it is smaller it will be centered 1706 | // these vectors are used to represent the scaling / translation to be applied to vertices and edge geometry 1707 | 1708 | var S = new algo.layout.Vector(1, 1), T = new algo.layout.Vector(0, 0); 1709 | 1710 | if (graphSize.x > box.w) { 1711 | S.x = box.w / graphSize.x; 1712 | } else { 1713 | T.x = (box.w - graphSize.x) / 2; 1714 | } 1715 | 1716 | if (graphSize.y > box.h) { 1717 | S.y = box.h / graphSize.y; 1718 | } else { 1719 | T.y = (box.h - graphSize.y) / 2; 1720 | } 1721 | 1722 | // add the original position of the box to the translation 1723 | T = T.add(new algo.layout.Vector(box.x, box.y)); 1724 | 1725 | var vertexBoxes = {}; 1726 | 1727 | // update vertices 1728 | layout.eachNode(_.bind(function (nodeName, node) { 1729 | 1730 | // get bounds of node/vertex 1731 | var b = new algo.layout.Box(node.x - (node.width >> 1), node.y - (node.height >> 1), node.width, node.height); 1732 | 1733 | // apply scaling and translation 1734 | b = b.mul(S).add(T); 1735 | 1736 | // save the vertex position for when we layout the edges 1737 | vertexBoxes[nodeName] = b; 1738 | 1739 | // get vertex from the source graph 1740 | var v = this.graph.vertices[nodeName]; 1741 | 1742 | // call update vertex if owner created an element for this vertex 1743 | if (v.element) { 1744 | this.dataStructure.invoke('updateVertex', v, v.element, b, this.graph); 1745 | } 1746 | 1747 | }, this)); 1748 | 1749 | layout.eachEdge(_.bind(function (e, u, v, value) { 1750 | 1751 | // get center of vertices that are connected, the inflection point of the edge 1752 | // will need to be transformed 1753 | 1754 | var start = vertexBoxes[u].center, 1755 | end = vertexBoxes[v].center, 1756 | middle = new algo.layout.Vector(value.points[0].x, value.points[0].y).multiply(S).add(T); 1757 | 1758 | // get edge from original graph 1759 | 1760 | var edge = this.graph.edges[e]; 1761 | 1762 | // update edge if element exists 1763 | if (edge.element) { 1764 | this.dataStructure.invoke('updateEdge', edge, edge.element, start, middle, end, this.graph); 1765 | } 1766 | 1767 | }, this)); 1768 | }; 1769 | 1770 | 1771 | 1772 | 1773 | 1774 | 1775 | 1776 | 1777 | 1778 | 1779 | 1780 | 1781 | 1782 | 1783 | 1784 | 1785 | 1786 | 1787 | 1788 | 1789 | 1790 | 1791 | 1792 | 1793 | 1794 | 1795 | 1796 | 1797 | 1798 | 1799 | 1800 | 1801 | 1802 | 1803 | 1804 | 1805 | 1806 | 1807 | 1808 | 1809 | 1810 | 1811 | 1812 | 1813 | -------------------------------------------------------------------------------- /apis/1.0/samples/atemplate.js: -------------------------------------------------------------------------------- 1 | function* algorithm() { 2 | 3 | //=Initialize, display the array/string we are going to reverse 4 | var WORD = "ALGORITHMS"; 5 | 6 | // create and display WORD using letter tiles 7 | var varray = makeArray(); 8 | 9 | // start the algorithm 10 | var left = 0, 11 | right = WORD.length - 1; 12 | yield ({ 13 | step: "The string we are going to reverse. Initialize two indices, left and right to either end of the array.", 14 | line: "//=Initialize", 15 | variables: { 16 | Word: WORD, 17 | left: left, 18 | right: right 19 | } 20 | }); 21 | 22 | // start reversing the array by swapping elements at either end 23 | //=swap 24 | while (left < right) { 25 | 26 | yield ({ 27 | step: "Exchange the items in the slots identified by the left and right variables.", 28 | line: "//=swap", 29 | variables: { 30 | left: left, 31 | right: right 32 | } 33 | }); 34 | 35 | // swap the two items on the array and reposition their elements 36 | varray.swap(left, right); 37 | 38 | yield ({ 39 | autoskip: true 40 | }); 41 | 42 | // move left and right indices towards the center of the array 43 | left += 1; 44 | right -= 1; 45 | } 46 | 47 | yield ({ 48 | step: 'The algorithm is complete when the left and right indices either converge on the middle element or pass over each other.' 49 | }); 50 | 51 | // display the WORD variables 52 | function makeArray() { 53 | 54 | // get bounds of surface we are displayed on 55 | var bounds = algo.BOUNDS; 56 | 57 | // derive tile size from surface size and word length 58 | var kS = bounds.w / (WORD.length + 2); 59 | 60 | // we only need a simple single row grid 61 | var layout = new algo.layout.GridLayout(bounds, 1, WORD.length); 62 | 63 | // create the array wrapper 64 | return new algo.core.Array({ 65 | 66 | // initialize with the word 67 | 68 | data: WORD, 69 | 70 | // called whenever a new item is added to the array, you should return the element used 71 | // to visualize the item 72 | 73 | createElement: _.bind(function(value, index) { 74 | 75 | // create a new letter tile. Display the letter and the array index in the tile 76 | var element = new algo.render.LetterTile({ 77 | text: value, 78 | w: kS, 79 | h: kS, 80 | value: index 81 | }); 82 | 83 | // position within the appropriate row/column of the layout 84 | element.layout(layout.getBox(0, index)); 85 | 86 | return element; 87 | 88 | }, this), 89 | 90 | // Use a path based animation to transition swapped elements to thier new locations 91 | swapElement: _.bind(function(value, newIndex, oldIndex, element) { 92 | 93 | // get the bounding box of the new and old cell 94 | var newCell = layout.getBox(0, newIndex), 95 | oldCell = layout.getBox(0, oldIndex); 96 | 97 | // get x position for element centered in cell 98 | var newX = newCell.cx - element.w / 2, 99 | oldX = oldCell.cx - element.w / 2; 100 | 101 | // height of the cell is used to move the items above or below the displayed string and 102 | // the start/end vertical position of the tiles is a constant 103 | 104 | var H = element.h * 1.5, 105 | Y = element.y; 106 | 107 | // if item was in the left/lower half of the array move it up and over, 108 | // if item was in the right/upper half of the array move it down and under 109 | 110 | var yoffset = oldIndex < WORD.length / 2 ? H : -H; 111 | 112 | element.set({ 113 | y: [Y + yoffset, Y + yoffset, Y], 114 | x: [oldX, newX, newX], 115 | state: [algo.render.kS_BLUE, algo.render.kS_BLUE, algo.render.kS_FADED] 116 | }); 117 | }) 118 | }); 119 | } 120 | } -------------------------------------------------------------------------------- /apis/1.0/surface.js: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2014 Duncan Meech / Algomation 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | */ 24 | 25 | /*globals _, $*/ 26 | "use strict"; 27 | 28 | /** 29 | * namespaces for the render library 30 | * @namespace algo 31 | */ 32 | var algo = algo || {}; 33 | 34 | /** 35 | * @namespace 36 | */ 37 | algo.render = algo.render || {}; 38 | 39 | /** 40 | * 41 | * @const {algo.render.Surface} Singleton instance of surface 42 | */ 43 | algo.SURFACE = null; 44 | 45 | /** 46 | * an animation / display surface composed of n layers, each composed of n elements. 47 | */ 48 | algo.render.Surface = function (options) { 49 | 50 | // setup singleton. There is only ever one surface so this is the best way to access it 51 | algo.SURFACE = this; 52 | 53 | // the options tell us if we are the worker or the dom version of the library 54 | 55 | this.options = options; 56 | 57 | // create our DOM element if we are DOM based 58 | 59 | if (this.isDOM) { 60 | 61 | // we expect the selector for a DOM element that we work on 62 | 63 | this.dom = options.domSelector; 64 | 65 | // maps commands to handlers 66 | 67 | this.commandHandlers = {}; 68 | 69 | this.commandHandlers[algo.render.Surface.UPDATE_COMMAND] = this.handleUpdateCommand; 70 | 71 | this.commandHandlers[algo.render.Surface.DESTROY_COMMAND] = this.handleDestroyCommand; 72 | 73 | $('.algo-surface').bind('mousemove', _.bind(function (e) { 74 | 75 | if (!algo.G.live && algo.ROOT && algo.ROOT.dom) { 76 | 77 | if (!this.mpDIV) { 78 | this.mpDIV = $('
'); 79 | this.mpDIV.css({ 80 | position : 'absolute', 81 | width : '5em', 82 | height : '1.5em', 83 | //left : '1em', 84 | //top : '1em', 85 | color : 'white', 86 | background: 'red' 87 | }); 88 | this.mpDIV.appendTo('.algo-surface'); 89 | } 90 | 91 | var offset = algo.ROOT.dom.offset(); 92 | this.mpDIV.html(_.sprintf("%d, %d", (e.pageX - offset.left) * (1 / this.scaling) >> 0,(e.pageY - offset.top) * (1 / this.scaling) >> 0)); 93 | 94 | } 95 | 96 | }, this)); 97 | 98 | } else { 99 | 100 | // create a queue of commands that will be passed to our DOM twin 101 | 102 | this.commands = []; 103 | 104 | // ensure element class is reset 105 | 106 | algo.render.Element.resetClass(); 107 | 108 | // surface has a single element that acts as the root of the scene graph. We only create this for the worker 109 | // since its will be created on the DOM when the first commands are sent over. 110 | 111 | this.root = algo.ROOT = new algo.render.Rectangle({ 112 | root : true, 113 | visible : false, 114 | x : options.bounds.x, 115 | y : options.bounds.y, 116 | w : options.bounds.w, 117 | h : options.bounds.h, 118 | strokeWidth: 0 119 | }); 120 | 121 | // provide the bounds of the root element as a global 122 | 123 | algo.BOUNDS = this.root.getBounds(); 124 | 125 | } 126 | }; 127 | 128 | /** 129 | * make the root element of the surface, assign ROOT and BOUNDS globals 130 | */ 131 | algo.render.Surface.prototype.empty = function () { 132 | 133 | // clear the surface and reset of our root, the first replay command 134 | // will construct the root element 135 | 136 | if (this.root && this.root.dom) { 137 | this.root.dom.remove(); 138 | } 139 | 140 | delete this.root; 141 | 142 | // surface has a single element that acts as the root of the scene graph. We only create this for the worker 143 | // since its will be created on the DOM when the first commands are sent over. 144 | // 145 | //this.root = algo.ROOT = new algo.render.Rectangle({ 146 | // root : true, 147 | // visible : false, 148 | // x : 0, 149 | // y : 0, 150 | // w : algo.Player.kSW, 151 | // h : algo.Player.kSH, 152 | // strokeWidth: 0 153 | //}); 154 | // 155 | //// provide the bounds of the root element as a global 156 | // 157 | //algo.BOUNDS = this.root.getBounds(); 158 | }; 159 | 160 | /** 161 | * save and detach the current surface and return a new, temporary surface 162 | */ 163 | algo.render.Surface.enterHistoryMode = function () { 164 | 165 | if (algo.render.Surface.history) { 166 | throw new Error("Surface history already exists"); 167 | } 168 | 169 | // save current singleton 170 | algo.render.Surface.history = algo.SURFACE; 171 | 172 | // detach the current root dom, the temp surface will replace it 173 | algo.SURFACE.root.dom.detach(); 174 | 175 | // create new surface with options from old surface 176 | new algo.render.Surface(algo.SURFACE.options); 177 | 178 | // mark current frame as nothing 179 | algo.SURFACE.historyFrame = -1; 180 | }; 181 | 182 | /** 183 | * exit history mode and restore old surface 184 | */ 185 | algo.render.Surface.exitHistoryMode = function () { 186 | 187 | if (!algo.render.Surface.history) { 188 | throw new Error("Surface class is not in history mode"); 189 | } 190 | 191 | // empty container dom if there is any ( might have rewound to start of algorithm ) 192 | if (algo.SURFACE.root && algo.SURFACE.root.dom) { 193 | algo.SURFACE.root.dom.remove(); 194 | } 195 | 196 | // reset singleton back to the original surface 197 | algo.SURFACE = algo.render.Surface.history; 198 | 199 | // reattach old surface DOM 200 | algo.SURFACE.root.dom.appendTo(algo.SURFACE.dom); 201 | 202 | // delete history 203 | delete algo.render.Surface.history; 204 | 205 | }; 206 | 207 | /** 208 | * true if we are the worker library 209 | */ 210 | algo.render.Surface.prototype.__defineGetter__('isWorker', function () { 211 | 212 | return this.options.location === algo.render.Surface.WORKER; 213 | }); 214 | 215 | /** 216 | * true if we are the DOM library 217 | */ 218 | algo.render.Surface.prototype.__defineGetter__('isDOM', function () { 219 | 220 | return this.options.location === algo.render.Surface.DOM; 221 | }); 222 | 223 | /** 224 | * these constants indicate the location of this instance of the surface 225 | * @type {string} 226 | */ 227 | algo.render.Surface.WORKER = 'worker'; 228 | 229 | algo.render.Surface.DOM = 'dom'; 230 | 231 | /** 232 | * these constants define all the commands that the worker may send to the DOM 233 | * @type {string} 234 | */ 235 | 236 | algo.render.Surface.UPDATE_COMMAND = 'updateElement'; 237 | 238 | algo.render.Surface.DESTROY_COMMAND = 'destroyElement'; 239 | 240 | /** 241 | * when an element is destoryed 242 | * @param element 243 | * @param options 244 | */ 245 | algo.render.Surface.prototype.elementDestroyed = function (element) { 246 | 247 | // generate a create command 248 | 249 | this.addCommand(algo.render.Surface.DESTROY_COMMAND, element, {}); 250 | 251 | }; 252 | 253 | /** 254 | * when an elements properties are updated 255 | * @param element 256 | * @param options 257 | */ 258 | algo.render.Surface.prototype.elementUpdated = function (element, options) { 259 | 260 | // generate a create command 261 | 262 | this.addCommand(algo.render.Surface.UPDATE_COMMAND, element, options); 263 | 264 | }; 265 | 266 | /** 267 | * create a command with the given name and clone a copy of the options but with the black listed options 268 | * removed e.g. the surface itself which we don't want serialized and set back to the DOM 269 | * also the parent since the parentID is all that is required 270 | * @param name 271 | * @param options 272 | */ 273 | algo.render.Surface.prototype.addCommand = function (name, element, options) { 274 | 275 | // ignore if we aren't the worker side 276 | if (this.isDOM) { 277 | return; 278 | } 279 | 280 | // omit certain properties that don't need to get transmitted e.g. the array of states that each element has. 281 | // We also don't transmit the state property since it works by applying properties that will generate their own 282 | // commands. Finally we don't transmit the shape property since, if it applies to the element, it will have 283 | // been realized in other properties , 284 | 285 | // ( this also clones the options object ) 286 | 287 | var c = _.omit(options, 'states', 'state', 'shape'); 288 | 289 | // replace parent element with parent id, otherwise the entire parent chain would get serialized 290 | 291 | if (options.parent) { 292 | c.parent = options.parent.id; 293 | } 294 | 295 | // add the element ID to all commands 296 | c.id = element.id; 297 | 298 | // if there is an update command for this element already in the buffer then just extend it with the new 299 | // options, otherwise create a new command 300 | 301 | if (name === algo.render.Surface.UPDATE_COMMAND) { 302 | 303 | var existingCommand = _.find(this.commands, function (command) { 304 | 305 | return command.name === algo.render.Surface.UPDATE_COMMAND && command.options.id === c.id; 306 | 307 | }, this); 308 | 309 | // if existing update command found for this item just extend it 310 | 311 | if (existingCommand) { 312 | 313 | _.extend(existingCommand.options, c); 314 | 315 | return; 316 | } 317 | } 318 | 319 | // add to command buffer 320 | 321 | this.commands.push({ 322 | name : name, 323 | options: c 324 | }); 325 | }; 326 | 327 | /** 328 | * return a shallow copy of our current commands and reset the buffer 329 | */ 330 | algo.render.Surface.prototype.flushCommands = function () { 331 | 332 | var buffer = this.commands.slice(); 333 | 334 | this.commands.length = 0; 335 | 336 | return buffer; 337 | }; 338 | 339 | /** 340 | * execute commands from the worker on the dom 341 | */ 342 | algo.render.Surface.prototype.executeCommands = function (commands) { 343 | 344 | // iterate all commands in the buffer 345 | 346 | _.each(commands, function (command) { 347 | 348 | // fix up some properties that were proxied in this.addCommand on the worker side 349 | var params = command.options; 350 | 351 | // replace parent ID with the real parent 352 | if (params.parent) { 353 | params.parent = algo.render.Element.findElement(params.parent); 354 | } 355 | 356 | // execute the command 357 | this.commandHandlers[command.name].call(this, command.options); 358 | 359 | }, this); 360 | 361 | // update the dom 362 | 363 | this.update(); 364 | 365 | // validate 366 | 367 | this.validate(); 368 | }; 369 | 370 | /** 371 | * execute multiple sets of render command and only update when all have been executed. 372 | * @param commandsHistory 373 | */ 374 | algo.render.Surface.prototype.executeCommandHistory = function (commandsHistory, frameIndex) { 375 | 376 | // if going forward in time we can play from present, otherwise, if going backwards 377 | // we have to play from beginning 378 | 379 | if (this.historyFrame === -1 || frameIndex < this.historyFrame) { 380 | 381 | this.resetElements(); 382 | 383 | this.playHistory(commandsHistory, 0, frameIndex); 384 | 385 | } else { 386 | 387 | this.playHistory(commandsHistory, this.historyFrame, frameIndex); 388 | } 389 | 390 | this.historyFrame = frameIndex; 391 | }; 392 | 393 | /** 394 | * before jumping to a new frame of the history the current surface must be cleared and the element class 395 | * reset ( ONLY if going backwards ) 396 | */ 397 | algo.render.Surface.prototype.resetElements = function () { 398 | 399 | // clear the surface, reset element class 400 | 401 | algo.render.Element.resetClass(); 402 | 403 | this.empty(); 404 | 405 | }; 406 | 407 | /** 408 | * play the given sub-section of the history buffer 409 | * @param commandsHistory 410 | * @param start 411 | * @param end 412 | */ 413 | algo.render.Surface.prototype.playHistory = function (commandsHistory, start, end) { 414 | 415 | for (var i = start; i < end; i += 1) { 416 | 417 | var commands = commandsHistory[i]; 418 | 419 | for (var j = 0; j < commands.renderCommands.length; j += 1) { 420 | 421 | var command = commands.renderCommands[j]; 422 | 423 | var params = command.options; 424 | 425 | // replace parent ID with the real parent 426 | if (params.parent) { 427 | params.parent = algo.render.Element.findElement(params.parent); 428 | } 429 | 430 | // execute the command 431 | this.commandHandlers[command.name].call(this, params); 432 | 433 | // replace parent with the parent ID 434 | if (params.parent) { 435 | params.parent = params.parent.id; 436 | } 437 | } 438 | } 439 | 440 | // update the dom 441 | 442 | this.update(); 443 | 444 | // validate 445 | 446 | this.validate(); 447 | }; 448 | 449 | /** 450 | * undo the prep work done in executeCommands, restore parent property to parent id 451 | * @param commands 452 | */ 453 | algo.render.Surface.prototype.restoreParentIds = function (commands) { 454 | 455 | // iterate all commands in the buffer 456 | 457 | _.each(commands, function (command) { 458 | 459 | var params = command.options; 460 | 461 | // replace parent element with parent id 462 | 463 | if (params.parent) { 464 | params.parent = params.parent.id; 465 | } 466 | 467 | }, this); 468 | }; 469 | 470 | /** 471 | * update element command handler 472 | * @param options 473 | */ 474 | algo.render.Surface.prototype.handleUpdateCommand = function (options) { 475 | 476 | // if this is new element we need to construct it then apply options 477 | 478 | var e = algo.render.Element.findElement(options.id); 479 | 480 | if (!e) { 481 | 482 | // invoke the constructor by name from the algo.render namespace 483 | e = new algo.render[options.type](options); 484 | 485 | // if this was the root element assign it 486 | if (options.root) { 487 | this.root = algo.ROOT = e; 488 | } 489 | } 490 | 491 | // apply properties in the command 492 | e.set(options); 493 | 494 | }; 495 | 496 | /** 497 | * If the options object contains any non empty arrays then return a clone 498 | * with the next value from each array exchanged for the array. Otherwise 499 | * return null 500 | * @param options 501 | */ 502 | algo.render.Surface.prototype.processOptions = function (options) { 503 | 504 | // reset more flag in options, 505 | delete options.more; 506 | 507 | // create empty clone 508 | var clone = {}; 509 | 510 | // process all keys 511 | _.each(options, function (value, key) { 512 | 513 | if (_.isArray(value)) { 514 | clone[key] = value.shift(); 515 | 516 | // if the array is now empty then we can remove 517 | 518 | if (value.length === 0) { 519 | delete options[key]; 520 | 521 | } else { 522 | 523 | // if not empty mark the options object as having more values 524 | options.more = true; 525 | } 526 | } else { 527 | clone[key] = value; 528 | } 529 | 530 | }, this); 531 | 532 | return clone; 533 | }; 534 | 535 | /** 536 | * handle an element destory command 537 | * @param options 538 | */ 539 | algo.render.Surface.prototype.handleDestroyCommand = function (options) { 540 | 541 | // get the element and destroy it 542 | 543 | algo.render.Element.findElement(options.id).destroy(); 544 | 545 | }; 546 | 547 | /** 548 | * update the surface and all layers and all elements on those layers 549 | */ 550 | algo.render.Surface.prototype.update = function () { 551 | 552 | // call update on the root element which recursively calls children. Root may not 553 | // exist if we back track through history to the beginning 554 | 555 | if (this.root) { 556 | this.root.update(); 557 | if (this.root.dom.parent().length === 0) { 558 | this.root.dom.appendTo(this.dom); 559 | } 560 | } 561 | }; 562 | 563 | /** 564 | * perform various validations of the state of the surface and the elements on it. 565 | */ 566 | algo.render.Surface.prototype.validate = function () { 567 | 568 | 569 | // validate on DOM side only, but not in production 570 | 571 | if (this.isDOM && !algo.G.live) { 572 | 573 | // bail if nothing to validate 574 | 575 | if (!this.root || !this.root.element) { 576 | return; 577 | } 578 | 579 | // match elements on the surface / root element to those in the static element map 580 | 581 | var elements = $('.algo-element', this.root.element); 582 | 583 | _.each(elements, function (element) { 584 | 585 | var e = $(element); 586 | 587 | var id = e.attr('id'); 588 | 589 | if (!algo.render.Element.map[id]) { 590 | 591 | throw new Error("Element in root not found in element map"); 592 | } 593 | 594 | }, this); 595 | 596 | // basically the opposite, check that every element ID in the map is in the DOM 597 | 598 | _.each(algo.render.Element.map, function (element) { 599 | 600 | var id = element.id; 601 | 602 | var d = $('#' + id); 603 | 604 | if (d.length !== 1) { 605 | throw new Error("Element in map not found in DOM"); 606 | } 607 | 608 | }, this); 609 | 610 | // ensure there is a one to one match 611 | 612 | if (elements.length !== _.keys(algo.render.Element.map).length) { 613 | 614 | throw new Error("Mismatch between length of static map and elements in root"); 615 | } 616 | } 617 | }; 618 | -------------------------------------------------------------------------------- /apis/1.0/tests/algo_core_tests.js: -------------------------------------------------------------------------------- 1 | /*globals $, _, algo, ok, test */ 2 | 3 | module("algo.array tests"); 4 | 5 | 6 | test("isEmptyArray", function () { 7 | 8 | // test array types 9 | 10 | ok(algo.array.isEmptyArray([]), "Passed"); 11 | 12 | // test some non array types 13 | 14 | ok(!algo.array.isEmptyArray(arguments), "Passed"); 15 | 16 | ok(!algo.array.isEmptyArray(null), "Passed"); 17 | 18 | ok(!algo.array.isEmptyArray(void 0), "Passed"); 19 | 20 | ok(!algo.array.isEmptyArray(123), "Passed"); 21 | 22 | ok(!algo.array.isEmptyArray("123"), "Passed"); 23 | 24 | ok(!algo.array.isEmptyArray({}), "Passed"); 25 | 26 | // test non empty array 27 | 28 | ok(!algo.array.isEmptyArray([0]), "Passed"); 29 | 30 | }); 31 | 32 | test("isArrayLengthAtLeast", function () { 33 | 34 | // test lengths 35 | 36 | ok(algo.array.isArrayLengthAtLeast([], 0), "Passed"); 37 | 38 | ok(algo.array.isArrayLengthAtLeast([1, 2, 3, 4], 4), "Passed"); 39 | 40 | // test failure cases 41 | 42 | ok(!algo.array.isArrayLengthAtLeast([], 1), "Passed"); 43 | 44 | ok(!algo.array.isArrayLengthAtLeast([1, 2, 3, 4], 5), "Passed"); 45 | 46 | 47 | // test some non array types with length properties 48 | 49 | ok(!algo.array.isArrayLengthAtLeast(arguments, 0), "Passed"); 50 | 51 | ok(!algo.array.isArrayLengthAtLeast({length: 0}, 0), "Passed"); 52 | 53 | ok(!algo.array.isArrayLengthAtLeast([]), "Passed"); 54 | 55 | }); 56 | 57 | test("fill", function () { 58 | 59 | function fillTest(a, start, end, value) { 60 | 61 | for (var i = start; i < end; i += 1) { 62 | 63 | if (a[i] !== value) { 64 | return false; 65 | } 66 | 67 | return true; 68 | } 69 | } 70 | 71 | // test without step 72 | 73 | var a = new Uint32Array(100); 74 | 75 | algo.array.fill(a, 0, a.length, 0xcccc); 76 | 77 | ok(fillTest(a, 0, a.length, 0xcccc), "Passed"); 78 | 79 | algo.array.fill(a, 50, a.length, 0x5555); 80 | 81 | ok(fillTest(a, 0, 50, 0xcccc), "Passed"); 82 | 83 | ok(fillTest(a, 50, a.length, 0x5555), "Passed"); 84 | 85 | // test with step 86 | 87 | algo.array.fill(a, 0, a.length, 1000, 1); 88 | 89 | var errors = 0; 90 | 91 | for (var i = 0; i < a.length; i += 1) { 92 | 93 | if (a[i] != 1000 + i) { 94 | errors++; 95 | } 96 | } 97 | 98 | ok(errors == 0, "passed"); 99 | 100 | }); 101 | 102 | test("randomize", function () { 103 | 104 | var a = []; 105 | 106 | // 10000 elements between 100 - 200 107 | 108 | algo.array.randomize(a, 0, 10000, 11, 100, 200); 109 | 110 | var errors = 0; 111 | 112 | for (var i = 0; i < 10000; i += 1) { 113 | 114 | if (a[i] < 100 || a[i] > 200) { 115 | errors++; 116 | } 117 | } 118 | 119 | ok(errors === 0, "Passed"); 120 | 121 | a.length = 0; 122 | }); 123 | 124 | test("reverse", function () { 125 | 126 | // 1000 zeros 127 | 128 | var a = new Int32Array(1000); 129 | 130 | // set last 500 to 0,1,2,3 etc 131 | 132 | for (var i = 0; i < 500; i += 1) { 133 | a[500 + i] = i; 134 | } 135 | 136 | // reverse the 500 items 137 | 138 | algo.array.reverse(a, 500, 1000); 139 | 140 | // verify 141 | 142 | var errors = 0; 143 | 144 | for (var i = 0; i < 500; i += 1) { 145 | 146 | if (a[500 + i] != (499 - i)) { 147 | errors++; 148 | } 149 | } 150 | 151 | ok(errors === 0, "Passed"); 152 | 153 | a.length = 0; 154 | }); 155 | 156 | test("swap", function () { 157 | 158 | var a = []; 159 | 160 | a[0] = 1234; 161 | 162 | a[1] = 4567; 163 | 164 | algo.array.swap(a, 0, 1); 165 | 166 | ok(a[0] == 4567, "Passed"); 167 | 168 | ok(a[1] == 1234, "Passed"); 169 | }); 170 | 171 | test("isSortedLowToHigh", function () { 172 | 173 | var a = new Int32Array(1000); 174 | 175 | algo.array.fill(a, 0, a.length, 0, 1); 176 | 177 | // test using native comparison 178 | 179 | ok(algo.array.isSortedLowToHigh(a, 0, a.length), "Passed"); 180 | 181 | // test using custom comparison 182 | 183 | ok(algo.array.isSortedLowToHigh(a, 0, a.length, function (a, b) { 184 | 185 | if (a == b) return 0; 186 | 187 | if (a < b) return -1; 188 | 189 | return 1; 190 | 191 | }), "Passed"); 192 | 193 | // now reverse and the test should fail 194 | 195 | algo.array.reverse(a, 0, a.length); 196 | 197 | // test using native comparison 198 | 199 | ok(!algo.array.isSortedLowToHigh(a, 0, a.length), "Passed"); 200 | 201 | // test using custom comparison 202 | 203 | ok(!algo.array.isSortedLowToHigh(a, 0, a.length, function (a, b) { 204 | 205 | if (a == b) return 0; 206 | 207 | if (a < b) return -1; 208 | 209 | return 1; 210 | 211 | }), "Passed"); 212 | }); 213 | 214 | test("shuffle", function () { 215 | 216 | // create a deck of 52 cards 217 | 218 | var a = new Int32Array(52); 219 | 220 | // fill with 0 -> 51 221 | 222 | algo.array.fill(a, 0, a.length, 0, 1); 223 | 224 | // shuffle 225 | 226 | algo.array.shuffle(a, 0, a.length, Date.now()); 227 | 228 | // count the number of items that are in their initial positions 229 | 230 | var k = 0; 231 | 232 | for (var i = 0; i < a.length; i += 1) { 233 | 234 | if (a[i] == i) { 235 | k++; 236 | } 237 | } 238 | 239 | // we expect k to be very small 240 | 241 | ok(k < 10, "Passed"); 242 | }); 243 | 244 | test("algo.compare tests", function () { 245 | 246 | ok(algo.array.compare([1, 2, 3], [1, 2, 3], 0, 0, 3), "passed"); 247 | 248 | ok(!algo.array.compare([0, 0, 0, 1, 2, 3], [0, 0, 0, 4, 5, 6], 3, 3, 3), "passed"); 249 | 250 | ok(algo.array.compare(["ABC", "XYZ"], ["ABC", "XYZ"], 0, 0, 2), "passed"); 251 | 252 | var v1 = { 253 | value: 3.14 254 | } 255 | 256 | var v2 = { 257 | value: 3.14 258 | } 259 | 260 | ok(algo.array.compare([v1, v1, v1], [v2, v2, v2], 0, 0, 3, function (a, b) { 261 | 262 | return a.value - b.value; 263 | })); 264 | 265 | v2.value = 0; 266 | 267 | ok(!algo.array.compare([v1, v1, v1], [v2, v2, v2], 0, 0, 3, function (a, b) { 268 | 269 | return a.value - b.value; 270 | })); 271 | }); 272 | 273 | module("algo.sort tests"); 274 | 275 | 276 | test("quicksort", function () { 277 | 278 | var a = new Int32Array(100); 279 | 280 | algo.array.randomize(a, 0, a.length, Date.now(), 0, 1000); 281 | 282 | algo.core.quickSortArray(a, 0, a.length - 1); 283 | 284 | }); 285 | 286 | test("mergeSort", function () { 287 | 288 | var a = []; 289 | 290 | algo.array.randomize(a, 0, 100, Date.now(), 0, 1000); 291 | 292 | var sorted = algo.core.mergeSortArray(a); 293 | }); 294 | 295 | module("algo.search tests"); 296 | 297 | 298 | test("binarySearch", function () { 299 | 300 | // create sorted array 301 | 302 | var a = new Int32Array(10); 303 | 304 | algo.array.fill(a, 0, a.length, 0, 1); 305 | 306 | // test value outside and inside the range 307 | 308 | for (var i = -a.length; i < a.length * 2; i += 1) { 309 | 310 | if (i >= 0 && i < a.length) { 311 | 312 | ok(algo.search.binarySearch(a, i) !== null, "Passed"); 313 | 314 | } else { 315 | 316 | ok(algo.search.binarySearch(a, i) === null, "Passed"); 317 | } 318 | } 319 | }); 320 | 321 | module("algo.core tests"); 322 | 323 | test("factorial", function () { 324 | 325 | ok(algo.core.factorial(1) === 1, "passed"); 326 | 327 | ok(algo.core.factorial(4) === 24, "passed"); 328 | 329 | ok(algo.core.factorial(3) === 6, "passed"); 330 | 331 | }); 332 | 333 | test("permutations generator", function () { 334 | 335 | // create a generator for 4! 336 | 337 | var p = algo.core.permutations(4); 338 | 339 | // collect all the permutations and test that none is like any other and they contain 0..3 340 | 341 | 342 | var y = p.next(); 343 | 344 | var a = []; 345 | 346 | while (!y.done) { 347 | 348 | a.push(y.value); 349 | 350 | y = p.next(); 351 | 352 | } 353 | 354 | // number of permutations should === 4! 355 | 356 | ok(algo.core.factorial(4) === a.length, "passed"); 357 | 358 | // check that each array contains 0..3 in some order 359 | 360 | for (var i = 0; i < a.length; i += 1) { 361 | 362 | ok(a[i].length === 4, "passed"); 363 | 364 | ok(a[i].indexOf(0) >= 0, "passed"); 365 | 366 | ok(a[i].indexOf(1) >= 0, "passed"); 367 | 368 | ok(a[i].indexOf(2) >= 0, "passed"); 369 | 370 | ok(a[i].indexOf(3) >= 0, "passed"); 371 | 372 | } 373 | 374 | }); 375 | 376 | 377 | -------------------------------------------------------------------------------- /apis/1.0/worker_core.js: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2014 Duncan Meech / Algomation 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | */ 24 | 25 | /*global algo,_*/ 26 | 27 | var globalScope = this; 28 | 29 | /** 30 | * handle and respond to messages from our owing page/application 31 | * @param event 32 | */ 33 | self.onmessage = function (event) { 34 | 35 | // handle named events 36 | 37 | switch (event.data.name) { 38 | 39 | // initialization message 40 | 41 | case "M_Initialize": 42 | { 43 | // create the singleton app for this worker and supply the worker object so it can send messages 44 | 45 | new WorkerApp(this, event.data.algorithmURI, event.data.api); 46 | } 47 | break; 48 | 49 | // After the user acknowledges a pause we continue with the algorithm 50 | 51 | case "M_Continue": 52 | { 53 | WorkerApp.I.pauseOrContinue(); 54 | } 55 | break; 56 | 57 | } 58 | }; 59 | 60 | 61 | /** 62 | * the application class that runs in this worker 63 | * @constructor 64 | */ 65 | var WorkerApp = function (worker, algorithmURI, api) { 66 | 67 | // save the worker object so we can post messages using it 68 | 69 | this.worker = worker; 70 | 71 | // make ourselves the singleton instance 72 | 73 | WorkerApp.I = this; 74 | 75 | /* when the algorithm is being edited the full source code is used for all files, including underscore and the regenerator 76 | modules. When the player is running stand alone ( or embedded ) the uglified / compressed source is used. 77 | If we are part of the minified library that global/namespace algo will exist, otherwise it won't. 78 | */ 79 | 80 | if (!globalScope['algo']) { 81 | 82 | // NOTE: This block must be kept in synch with the grunt task: uglify::workerjs to ensure the correct files 83 | // are imported or compressed in the a single file 84 | 85 | importScripts( 86 | '/javascripts/underscore.js', 87 | '/javascripts/underscore-string.js', 88 | '/javascripts/regenerator-runtime.js', 89 | '/javascripts/apis/' + api + '/core.js', 90 | '/javascripts/apis/' + api + '/color.js', 91 | '/javascripts/apis/' + api + '/layout.js', 92 | '/javascripts/apis/' + api + '/element.js', 93 | '/javascripts/apis/' + api + '/surface.js', 94 | '/javascripts/apis/' + api + '/dagre.js' 95 | 96 | ); 97 | 98 | } 99 | 100 | // add underscore string mixin 101 | _.mixin(_.str.exports()); 102 | 103 | // create our surface and tell it that it is running in a worker not the DOM 104 | 105 | this.surface = new algo.render.Surface({ 106 | 107 | // TODO, this size must match algo.Player.kSW, kSH in player.js, find a good way to do that 108 | location: algo.render.Surface.WORKER, 109 | bounds : new algo.layout.Box(0, 0, 900, 556) 110 | 111 | }); 112 | 113 | // load the actual file containing the algorithm. 114 | 115 | importScripts(algorithmURI); 116 | 117 | // create the users algorithm as a generator and start running it 118 | 119 | try { 120 | 121 | this.userAlgorithm = algorithm(); 122 | 123 | } catch (error) { 124 | 125 | this.postError(error); 126 | return; 127 | } 128 | 129 | // send acknowledgement that we are initialized 130 | 131 | this.worker.postMessage({ 132 | "name": "M_Initialize_ACK" 133 | }); 134 | 135 | // call continue to start the algorithm 136 | 137 | this.continue(); 138 | 139 | }; 140 | 141 | /** 142 | * continue with the algorithm 143 | */ 144 | WorkerApp.prototype.continue = function () { 145 | 146 | // run until we hit the first yield 147 | 148 | 149 | var y; 150 | 151 | try { 152 | 153 | y = this.userAlgorithm.next(); 154 | 155 | } catch (error) { 156 | 157 | this.postError(error); 158 | 159 | } 160 | 161 | // send results of yield to the DOM side or signal the algorithm is complete 162 | 163 | if (y.done) { 164 | this.done(); 165 | } else { 166 | this.pause(y.value); 167 | } 168 | }; 169 | 170 | /** 171 | * post the exception to the main thread. The worker is usually terminated on exceptions so we don't need to do anything 172 | * else. 173 | * @param error 174 | */ 175 | WorkerApp.prototype.postError = function(error) { 176 | 177 | this.worker.postMessage({ 178 | 179 | name : 'M_Exception', 180 | message : error.message, 181 | stack : error.stack 182 | }); 183 | 184 | }; 185 | 186 | /** 187 | * called whenever the algorithm yields 188 | * @param options 189 | */ 190 | WorkerApp.prototype.pause = function (pauseParameters) { 191 | 192 | // save the current commands, update commands with arrays instead of values are sent one a time 193 | 194 | this.currentCommands = this.surface.flushCommands(); 195 | 196 | // process options, which converts arrays of values to single values and marks then options objects 197 | // as containing more commands 198 | 199 | var activeCommands = []; 200 | 201 | _.each(this.currentCommands, function (command) { 202 | 203 | // only update commands can contain arrays 204 | 205 | if (command.name === "updateElement") { 206 | 207 | activeCommands.push({ 208 | name : command.name, 209 | options: this.surface.processOptions(command.options) 210 | }); 211 | 212 | } else { 213 | 214 | // all other commands just get copied in ( currently only the delete command ) 215 | activeCommands.push(command); 216 | } 217 | 218 | }, this); 219 | 220 | // send a pause event to the DOM and supply all current commands and options 221 | 222 | this.worker.postMessage({ 223 | 224 | name : 'M_Pause', 225 | renderCommands: activeCommands, 226 | options : pauseParameters 227 | }); 228 | 229 | }; 230 | 231 | /** 232 | * if we are done with the last set of render commands then call continue otherwise send the next batch 233 | * @param options 234 | */ 235 | WorkerApp.prototype.pauseOrContinue = function () { 236 | 237 | // scan all commands and send those with remaining options 238 | 239 | var activeCommands = []; 240 | 241 | _.each(this.currentCommands, function (command) { 242 | 243 | if (command.name === "updateElement" && command.options.more) { 244 | 245 | activeCommands.push({ 246 | name : command.name, 247 | options: this.surface.processOptions(command.options) 248 | }); 249 | } 250 | 251 | }, this); 252 | 253 | // if there are still active commands then send them otherwise continue 254 | 255 | if (activeCommands.length) { 256 | 257 | this.worker.postMessage({ 258 | 259 | name : 'M_Pause', 260 | renderCommands: activeCommands, 261 | options : {autoskip: true} // force an autoskip when chaining updates 262 | 263 | }); 264 | } else { 265 | 266 | // no remaining commands so continue 267 | 268 | this.continue(); 269 | } 270 | 271 | }; 272 | 273 | /** 274 | * called whenever the algorithm is completed 275 | * @param options 276 | */ 277 | WorkerApp.prototype.done = function () { 278 | 279 | // signal the algorithm is complete and flush any remaining graphics commands 280 | 281 | this.worker.postMessage({ 282 | 283 | name : 'M_Done', 284 | renderCommands: this.surface.flushCommands() 285 | }); 286 | 287 | }; --------------------------------------------------------------------------------