├── 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 | };
--------------------------------------------------------------------------------