/** * Kendo UI v2016.1.226 (http://www.telerik.com/kendo-ui) * Copyright 2016 Telerik AD. All rights reserved. * * Kendo UI commercial licenses may be obtained at * http://www.telerik.com/purchase/license-agreement/kendo-ui-complete * If you do not own a commercial license, this file shall be governed by the trial license terms. */ (function (f, define) { define('util/main', ['kendo.core'], f); }(function () { (function () { var math = Math, kendo = window.kendo, deepExtend = kendo.deepExtend; var DEG_TO_RAD = math.PI / 180, MAX_NUM = Number.MAX_VALUE, MIN_NUM = -Number.MAX_VALUE, UNDEFINED = 'undefined'; function defined(value) { return typeof value !== UNDEFINED; } function round(value, precision) { var power = pow(precision); return math.round(value * power) / power; } function pow(p) { if (p) { return math.pow(10, p); } else { return 1; } } function limitValue(value, min, max) { return math.max(math.min(value, max), min); } function rad(degrees) { return degrees * DEG_TO_RAD; } function deg(radians) { return radians / DEG_TO_RAD; } function isNumber(val) { return typeof val === 'number' && !isNaN(val); } function valueOrDefault(value, defaultValue) { return defined(value) ? value : defaultValue; } function sqr(value) { return value * value; } function objectKey(object) { var parts = []; for (var key in object) { parts.push(key + object[key]); } return parts.sort().join(''); } function hashKey(str) { var hash = 2166136261; for (var i = 0; i < str.length; ++i) { hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24); hash ^= str.charCodeAt(i); } return hash >>> 0; } function hashObject(object) { return hashKey(objectKey(object)); } var now = Date.now; if (!now) { now = function () { return new Date().getTime(); }; } function arrayLimits(arr) { var length = arr.length, i, min = MAX_NUM, max = MIN_NUM; for (i = 0; i < length; i++) { max = math.max(max, arr[i]); min = math.min(min, arr[i]); } return { min: min, max: max }; } function arrayMin(arr) { return arrayLimits(arr).min; } function arrayMax(arr) { return arrayLimits(arr).max; } function sparseArrayMin(arr) { return sparseArrayLimits(arr).min; } function sparseArrayMax(arr) { return sparseArrayLimits(arr).max; } function sparseArrayLimits(arr) { var min = MAX_NUM, max = MIN_NUM; for (var i = 0, length = arr.length; i < length; i++) { var n = arr[i]; if (n !== null && isFinite(n)) { min = math.min(min, n); max = math.max(max, n); } } return { min: min === MAX_NUM ? undefined : min, max: max === MIN_NUM ? undefined : max }; } function last(array) { if (array) { return array[array.length - 1]; } } function append(first, second) { first.push.apply(first, second); return first; } function renderTemplate(text) { return kendo.template(text, { useWithBlock: false, paramName: 'd' }); } function renderAttr(name, value) { return defined(value) && value !== null ? ' ' + name + '=\'' + value + '\' ' : ''; } function renderAllAttr(attrs) { var output = ''; for (var i = 0; i < attrs.length; i++) { output += renderAttr(attrs[i][0], attrs[i][1]); } return output; } function renderStyle(attrs) { var output = ''; for (var i = 0; i < attrs.length; i++) { var value = attrs[i][1]; if (defined(value)) { output += attrs[i][0] + ':' + value + ';'; } } if (output !== '') { return output; } } function renderSize(size) { if (typeof size !== 'string') { size += 'px'; } return size; } function renderPos(pos) { var result = []; if (pos) { var parts = kendo.toHyphens(pos).split('-'); for (var i = 0; i < parts.length; i++) { result.push('k-pos-' + parts[i]); } } return result.join(' '); } function isTransparent(color) { return color === '' || color === null || color === 'none' || color === 'transparent' || !defined(color); } function arabicToRoman(n) { var literals = { 1: 'i', 10: 'x', 100: 'c', 2: 'ii', 20: 'xx', 200: 'cc', 3: 'iii', 30: 'xxx', 300: 'ccc', 4: 'iv', 40: 'xl', 400: 'cd', 5: 'v', 50: 'l', 500: 'd', 6: 'vi', 60: 'lx', 600: 'dc', 7: 'vii', 70: 'lxx', 700: 'dcc', 8: 'viii', 80: 'lxxx', 800: 'dccc', 9: 'ix', 90: 'xc', 900: 'cm', 1000: 'm' }; var values = [ 1000, 900, 800, 700, 600, 500, 400, 300, 200, 100, 90, 80, 70, 60, 50, 40, 30, 20, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 ]; var roman = ''; while (n > 0) { if (n < values[0]) { values.shift(); } else { roman += literals[values[0]]; n -= values[0]; } } return roman; } function romanToArabic(r) { r = r.toLowerCase(); var digits = { i: 1, v: 5, x: 10, l: 50, c: 100, d: 500, m: 1000 }; var value = 0, prev = 0; for (var i = 0; i < r.length; ++i) { var v = digits[r.charAt(i)]; if (!v) { return null; } value += v; if (v > prev) { value -= 2 * prev; } prev = v; } return value; } function memoize(f) { var cache = Object.create(null); return function () { var id = ''; for (var i = arguments.length; --i >= 0;) { id += ':' + arguments[i]; } if (id in cache) { return cache[id]; } return f.apply(this, arguments); }; } function ucs2decode(string) { var output = [], counter = 0, length = string.length, value, extra; while (counter < length) { value = string.charCodeAt(counter++); if (value >= 55296 && value <= 56319 && counter < length) { extra = string.charCodeAt(counter++); if ((extra & 64512) == 56320) { output.push(((value & 1023) << 10) + (extra & 1023) + 65536); } else { output.push(value); counter--; } } else { output.push(value); } } return output; } function ucs2encode(array) { return array.map(function (value) { var output = ''; if (value > 65535) { value -= 65536; output += String.fromCharCode(value >>> 10 & 1023 | 55296); value = 56320 | value & 1023; } output += String.fromCharCode(value); return output; }).join(''); } deepExtend(kendo, { util: { MAX_NUM: MAX_NUM, MIN_NUM: MIN_NUM, append: append, arrayLimits: arrayLimits, arrayMin: arrayMin, arrayMax: arrayMax, defined: defined, deg: deg, hashKey: hashKey, hashObject: hashObject, isNumber: isNumber, isTransparent: isTransparent, last: last, limitValue: limitValue, now: now, objectKey: objectKey, round: round, rad: rad, renderAttr: renderAttr, renderAllAttr: renderAllAttr, renderPos: renderPos, renderSize: renderSize, renderStyle: renderStyle, renderTemplate: renderTemplate, sparseArrayLimits: sparseArrayLimits, sparseArrayMin: sparseArrayMin, sparseArrayMax: sparseArrayMax, sqr: sqr, valueOrDefault: valueOrDefault, romanToArabic: romanToArabic, arabicToRoman: arabicToRoman, memoize: memoize, ucs2encode: ucs2encode, ucs2decode: ucs2decode } }); kendo.drawing.util = kendo.util; kendo.dataviz.util = kendo.util; }()); return window.kendo; }, typeof define == 'function' && define.amd ? define : function (a1, a2, a3) { (a3 || a2)(); })); (function (f, define) { define('util/text-metrics', [ 'kendo.core', 'util/main' ], f); }(function () { (function ($) { var doc = document, kendo = window.kendo, Class = kendo.Class, util = kendo.util, defined = util.defined; var LRUCache = Class.extend({ init: function (size) { this._size = size; this._length = 0; this._map = {}; }, put: function (key, value) { var lru = this, map = lru._map, entry = { key: key, value: value }; map[key] = entry; if (!lru._head) { lru._head = lru._tail = entry; } else { lru._tail.newer = entry; entry.older = lru._tail; lru._tail = entry; } if (lru._length >= lru._size) { map[lru._head.key] = null; lru._head = lru._head.newer; lru._head.older = null; } else { lru._length++; } }, get: function (key) { var lru = this, entry = lru._map[key]; if (entry) { if (entry === lru._head && entry !== lru._tail) { lru._head = entry.newer; lru._head.older = null; } if (entry !== lru._tail) { if (entry.older) { entry.older.newer = entry.newer; entry.newer.older = entry.older; } entry.older = lru._tail; entry.newer = null; lru._tail.newer = entry; lru._tail = entry; } return entry.value; } } }); var defaultMeasureBox = $('
')[0]; function zeroSize() { return { width: 0, height: 0, baseline: 0 }; } var TextMetrics = Class.extend({ init: function (options) { this._cache = new LRUCache(1000); this._initOptions(options); }, options: { baselineMarkerSize: 1 }, measure: function (text, style, box) { if (!text) { return zeroSize(); } var styleKey = util.objectKey(style), cacheKey = util.hashKey(text + styleKey), cachedResult = this._cache.get(cacheKey); if (cachedResult) { return cachedResult; } var size = zeroSize(); var measureBox = box ? box : defaultMeasureBox; var baselineMarker = this._baselineMarker().cloneNode(false); for (var key in style) { var value = style[key]; if (defined(value)) { measureBox.style[key] = value; } } $(measureBox).text(text); measureBox.appendChild(baselineMarker); doc.body.appendChild(measureBox); if ((text + '').length) { size.width = measureBox.offsetWidth - this.options.baselineMarkerSize; size.height = measureBox.offsetHeight; size.baseline = baselineMarker.offsetTop + this.options.baselineMarkerSize; } if (size.width > 0 && size.height > 0) { this._cache.put(cacheKey, size); } measureBox.parentNode.removeChild(measureBox); return size; }, _baselineMarker: function () { return $('')[0]; } }); TextMetrics.current = new TextMetrics(); function measureText(text, style, measureBox) { return TextMetrics.current.measure(text, style, measureBox); } function loadFonts(fonts, callback) { var promises = []; if (fonts.length > 0 && document.fonts) { try { promises = fonts.map(function (font) { return document.fonts.load(font); }); } catch (e) { kendo.logToConsole(e); } Promise.all(promises).then(callback, callback); } else { callback(); } } kendo.util.TextMetrics = TextMetrics; kendo.util.LRUCache = LRUCache; kendo.util.loadFonts = loadFonts; kendo.util.measureText = measureText; }(window.kendo.jQuery)); }, typeof define == 'function' && define.amd ? define : function (a1, a2, a3) { (a3 || a2)(); })); (function (f, define) { define('util/base64', ['util/main'], f); }(function () { (function () { var kendo = window.kendo, deepExtend = kendo.deepExtend, fromCharCode = String.fromCharCode; var KEY_STR = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; function encodeBase64(input) { var output = ''; var chr1, chr2, chr3, enc1, enc2, enc3, enc4; var i = 0; input = encodeUTF8(input); while (i < input.length) { chr1 = input.charCodeAt(i++); chr2 = input.charCodeAt(i++); chr3 = input.charCodeAt(i++); enc1 = chr1 >> 2; enc2 = (chr1 & 3) << 4 | chr2 >> 4; enc3 = (chr2 & 15) << 2 | chr3 >> 6; enc4 = chr3 & 63; if (isNaN(chr2)) { enc3 = enc4 = 64; } else if (isNaN(chr3)) { enc4 = 64; } output = output + KEY_STR.charAt(enc1) + KEY_STR.charAt(enc2) + KEY_STR.charAt(enc3) + KEY_STR.charAt(enc4); } return output; } function encodeUTF8(input) { var output = ''; for (var i = 0; i < input.length; i++) { var c = input.charCodeAt(i); if (c < 128) { output += fromCharCode(c); } else if (c < 2048) { output += fromCharCode(192 | c >>> 6); output += fromCharCode(128 | c & 63); } else if (c < 65536) { output += fromCharCode(224 | c >>> 12); output += fromCharCode(128 | c >>> 6 & 63); output += fromCharCode(128 | c & 63); } } return output; } deepExtend(kendo.util, { encodeBase64: encodeBase64, encodeUTF8: encodeUTF8 }); }()); return window.kendo; }, typeof define == 'function' && define.amd ? define : function (a1, a2, a3) { (a3 || a2)(); })); (function (f, define) { define('mixins/observers', ['kendo.core'], f); }(function () { (function ($) { var math = Math, kendo = window.kendo, deepExtend = kendo.deepExtend, inArray = $.inArray; var ObserversMixin = { observers: function () { this._observers = this._observers || []; return this._observers; }, addObserver: function (element) { if (!this._observers) { this._observers = [element]; } else { this._observers.push(element); } return this; }, removeObserver: function (element) { var observers = this.observers(); var index = inArray(element, observers); if (index != -1) { observers.splice(index, 1); } return this; }, trigger: function (methodName, event) { var observers = this._observers; var observer; var idx; if (observers && !this._suspended) { for (idx = 0; idx < observers.length; idx++) { observer = observers[idx]; if (observer[methodName]) { observer[methodName](event); } } } return this; }, optionsChange: function (e) { this.trigger('optionsChange', e); }, geometryChange: function (e) { this.trigger('geometryChange', e); }, suspend: function () { this._suspended = (this._suspended || 0) + 1; return this; }, resume: function () { this._suspended = math.max((this._suspended || 0) - 1, 0); return this; }, _observerField: function (field, value) { if (this[field]) { this[field].removeObserver(this); } this[field] = value; value.addObserver(this); } }; deepExtend(kendo, { mixins: { ObserversMixin: ObserversMixin } }); }(window.kendo.jQuery)); return window.kendo; }, typeof define == 'function' && define.amd ? define : function (a1, a2, a3) { (a3 || a2)(); })); (function (f, define) { define('dataviz/diagram/utils', ['kendo.core'], f); }(function () { (function ($, undefined) { var kendo = window.kendo, diagram = kendo.dataviz.diagram = {}, deepExtend = kendo.deepExtend, isArray = $.isArray, EPSILON = 0.000001; var Utils = {}; deepExtend(Utils, { isNearZero: function (num) { return Math.abs(num) < EPSILON; }, isDefined: function (obj) { return typeof obj !== 'undefined'; }, isUndefined: function (obj) { return typeof obj === 'undefined' || obj === null; }, isObject: function (obj) { return obj === Object(obj); }, has: function (obj, key) { return Object.hasOwnProperty.call(obj, key); }, isString: function (obj) { return Object.prototype.toString.call(obj) == '[object String]'; }, isBoolean: function (obj) { return Object.prototype.toString.call(obj) == '[object Boolean]'; }, isType: function (obj, type) { return Object.prototype.toString.call(obj) == '[object ' + type + ']'; }, isNumber: function (obj) { return !isNaN(parseFloat(obj)) && isFinite(obj); }, isEmpty: function (obj) { if (obj === null) { return true; } if (isArray(obj) || Utils.isString(obj)) { return obj.length === 0; } for (var key in obj) { if (Utils.has(obj, key)) { return false; } } return true; }, simpleExtend: function (destination, source) { if (!Utils.isObject(source)) { return; } for (var name in source) { destination[name] = source[name]; } }, initArray: function createIdArray(size, value) { var array = []; for (var i = 0; i < size; ++i) { array[i] = value; } return array; }, serializePoints: function (points) { var res = []; for (var i = 0; i < points.length; i++) { var p = points[i]; res.push(p.x + ';' + p.y); } return res.join(';'); }, deserializePoints: function (s) { var v = s.split(';'), points = []; if (v.length % 2 !== 0) { throw 'Not an array of points.'; } for (var i = 0; i < v.length; i += 2) { points.push(new diagram.Point(parseInt(v[i], 10), parseInt(v[i + 1], 10))); } return points; }, randomInteger: function (lower, upper) { return parseInt(Math.floor(Math.random() * upper) + lower, 10); }, DFT: function (el, func) { func(el); if (el.childNodes) { for (var i = 0; i < el.childNodes.length; i++) { var item = el.childNodes[i]; this.DFT(item, func); } } }, getMatrixAngle: function (m) { if (m === null || m.d === 0) { return 0; } return Math.atan2(m.b, m.d) * 180 / Math.PI; }, getMatrixScaling: function (m) { var sX = Math.sqrt(m.a * m.a + m.c * m.c); var sY = Math.sqrt(m.b * m.b + m.d * m.d); return [ sX, sY ]; } }); function Range(start, stop, step) { if (typeof start == 'undefined' || typeof stop == 'undefined') { return []; } if (step && Utils.sign(stop - start) != Utils.sign(step)) { throw 'The sign of the increment should allow to reach the stop-value.'; } step = step || 1; start = start || 0; stop = stop || start; if ((stop - start) / step === Infinity) { throw 'Infinite range defined.'; } var range = [], i = -1, j; function rangeIntegerScale(x) { var k = 1; while (x * k % 1) { k *= 10; } return k; } var k = rangeIntegerScale(Math.abs(step)); start *= k; stop *= k; step *= k; if (start > stop && step > 0) { step = -step; } if (step < 0) { while ((j = start + step * ++i) >= stop) { range.push(j / k); } } else { while ((j = start + step * ++i) <= stop) { range.push(j / k); } } return range; } function findRadian(start, end) { if (start == end) { return 0; } var sngXComp = end.x - start.x, sngYComp = start.y - end.y, atan = Math.atan(sngXComp / sngYComp); if (sngYComp >= 0) { return sngXComp < 0 ? atan + 2 * Math.PI : atan; } return atan + Math.PI; } Utils.sign = function (number) { return number ? number < 0 ? -1 : 1 : 0; }; Utils.findAngle = function (center, end) { return findRadian(center, end) * 180 / Math.PI; }; Utils.forEach = function (arr, iterator, thisRef) { for (var i = 0; i < arr.length; i++) { iterator.call(thisRef, arr[i], i, arr); } }; Utils.any = function (arr, predicate) { for (var i = 0; i < arr.length; ++i) { if (predicate(arr[i])) { return arr[i]; } } return null; }; Utils.remove = function (arr, what) { var ax; while ((ax = Utils.indexOf(arr, what)) !== -1) { arr.splice(ax, 1); } return arr; }; Utils.contains = function (arr, obj) { return Utils.indexOf(arr, obj) !== -1; }; Utils.indexOf = function (arr, what) { return $.inArray(what, arr); }; Utils.fold = function (list, iterator, acc, context) { var initial = arguments.length > 2; for (var i = 0; i < list.length; i++) { var value = list[i]; if (!initial) { acc = value; initial = true; } else { acc = iterator.call(context, acc, value, i, list); } } if (!initial) { throw 'Reduce of empty array with no initial value'; } return acc; }; Utils.find = function (arr, iterator, context) { var result; Utils.any(arr, function (value, index, list) { if (iterator.call(context, value, index, list)) { result = value; return true; } return false; }); return result; }; Utils.first = function (arr, constraint, context) { if (arr.length === 0) { return null; } if (Utils.isUndefined(constraint)) { return arr[0]; } return Utils.find(arr, constraint, context); }; Utils.insert = function (arr, element, position) { arr.splice(position, 0, element); return arr; }; Utils.all = function (arr, iterator, context) { var result = true; var value; for (var i = 0; i < arr.length; i++) { value = arr[i]; result = result && iterator.call(context, value, i, arr); if (!result) { break; } } return result; }; Utils.clear = function (arr) { arr.splice(0, arr.length); }; Utils.bisort = function (a, b, sortfunc) { if (Utils.isUndefined(a)) { throw 'First array is not specified.'; } if (Utils.isUndefined(b)) { throw 'Second array is not specified.'; } if (a.length != b.length) { throw 'The two arrays should have equal length'; } var all = [], i; for (i = 0; i < a.length; i++) { all.push({ 'x': a[i], 'y': b[i] }); } if (Utils.isUndefined(sortfunc)) { all.sort(function (m, n) { return m.x - n.x; }); } else { all.sort(function (m, n) { return sortfunc(m.x, n.x); }); } Utils.clear(a); Utils.clear(b); for (i = 0; i < all.length; i++) { a.push(all[i].x); b.push(all[i].y); } }; Utils.addRange = function (arr, range) { arr.push.apply(arr, range); }; var Easing = { easeInOut: function (pos) { return -Math.cos(pos * Math.PI) / 2 + 0.5; } }; var Ticker = kendo.Class.extend({ init: function () { this.adapters = []; this.target = 0; this.tick = 0; this.interval = 20; this.duration = 800; this.lastTime = null; this.handlers = []; var _this = this; this.transition = Easing.easeInOut; this.timerDelegate = function () { _this.onTimerEvent(); }; }, addAdapter: function (a) { this.adapters.push(a); }, onComplete: function (handler) { this.handlers.push(handler); }, removeHandler: function (handler) { this.handlers = $.grep(this.handlers, function (h) { return h !== handler; }); }, trigger: function () { var _this = this; if (this.handlers) { Utils.forEach(this.handlers, function (h) { return h.call(_this.caller !== null ? _this.caller : _this); }); } }, onStep: function () { }, seekTo: function (to) { this.seekFromTo(this.tick, to); }, seekFromTo: function (from, to) { this.target = Math.max(0, Math.min(1, to)); this.tick = Math.max(0, Math.min(1, from)); this.lastTime = new Date().getTime(); if (!this.intervalId) { this.intervalId = window.setInterval(this.timerDelegate, this.interval); } }, stop: function () { if (this.intervalId) { window.clearInterval(this.intervalId); this.intervalId = null; this.trigger(); } }, play: function (origin) { if (this.adapters.length === 0) { return; } if (origin !== null) { this.caller = origin; } this.initState(); this.seekFromTo(0, 1); }, reverse: function () { this.seekFromTo(1, 0); }, initState: function () { if (this.adapters.length === 0) { return; } for (var i = 0; i < this.adapters.length; i++) { this.adapters[i].initState(); } }, propagate: function () { var value = this.transition(this.tick); for (var i = 0; i < this.adapters.length; i++) { this.adapters[i].update(value); } }, onTimerEvent: function () { var now = new Date().getTime(); var timePassed = now - this.lastTime; this.lastTime = now; var movement = timePassed / this.duration * (this.tick < this.target ? 1 : -1); if (Math.abs(movement) >= Math.abs(this.tick - this.target)) { this.tick = this.target; } else { this.tick += movement; } try { this.propagate(); } finally { this.onStep.call(this); if (this.target == this.tick) { this.stop(); } } } }); kendo.deepExtend(diagram, { init: function (element) { kendo.init(element, diagram.ui); }, Utils: Utils, Range: Range, Ticker: Ticker }); }(window.kendo.jQuery)); }, typeof define == 'function' && define.amd ? define : function (a1, a2, a3) { (a3 || a2)(); })); (function (f, define) { define('dataviz/diagram/math', [ 'dataviz/diagram/utils', 'kendo.dataviz.core' ], f); }(function () { (function ($, undefined) { var kendo = window.kendo, diagram = kendo.dataviz.diagram, Class = kendo.Class, deepExtend = kendo.deepExtend, dataviz = kendo.dataviz, Utils = diagram.Utils, Point = dataviz.Point2D, isFunction = kendo.isFunction, contains = Utils.contains, map = $.map; var HITTESTAREA = 3, EPSILON = 0.000001; deepExtend(Point.fn, { plus: function (p) { return new Point(this.x + p.x, this.y + p.y); }, minus: function (p) { return new Point(this.x - p.x, this.y - p.y); }, offset: function (value) { return new Point(this.x - value, this.y - value); }, times: function (s) { return new Point(this.x * s, this.y * s); }, normalize: function () { if (this.length() === 0) { return new Point(); } return this.times(1 / this.length()); }, length: function () { return Math.sqrt(this.x * this.x + this.y * this.y); }, toString: function () { return '(' + this.x + ',' + this.y + ')'; }, lengthSquared: function () { return this.x * this.x + this.y * this.y; }, middleOf: function MiddleOf(p, q) { return new Point(q.x - p.x, q.y - p.y).times(0.5).plus(p); }, toPolar: function (useDegrees) { var factor = 1; if (useDegrees) { factor = 180 / Math.PI; } var a = Math.atan2(Math.abs(this.y), Math.abs(this.x)); var halfpi = Math.PI / 2; var len = this.length(); if (this.x === 0) { if (this.y === 0) { return new Polar(0, 0); } if (this.y > 0) { return new Polar(len, factor * halfpi); } if (this.y < 0) { return new Polar(len, factor * 3 * halfpi); } } else if (this.x > 0) { if (this.y === 0) { return new Polar(len, 0); } if (this.y > 0) { return new Polar(len, factor * a); } if (this.y < 0) { return new Polar(len, factor * (4 * halfpi - a)); } } else { if (this.y === 0) { return new Polar(len, 2 * halfpi); } if (this.y > 0) { return new Polar(len, factor * (2 * halfpi - a)); } if (this.y < 0) { return new Polar(len, factor * (2 * halfpi + a)); } } }, isOnLine: function (from, to) { if (from.x > to.x) { var temp = to; to = from; from = temp; } var r1 = new Rect(from.x, from.y).inflate(HITTESTAREA, HITTESTAREA), r2 = new Rect(to.x, to.y).inflate(HITTESTAREA, HITTESTAREA), o1, u1; if (r1.union(r2).contains(this)) { if (from.x === to.x || from.y === to.y) { return true; } else if (from.y < to.y) { o1 = r1.x + (r2.x - r1.x) * (this.y - (r1.y + r1.height)) / (r2.y + r2.height - (r1.y + r1.height)); u1 = r1.x + r1.width + (r2.x + r2.width - (r1.x + r1.width)) * (this.y - r1.y) / (r2.y - r1.y); } else { o1 = r1.x + (r2.x - r1.x) * (this.y - r1.y) / (r2.y - r1.y); u1 = r1.x + r1.width + (r2.x + r2.width - (r1.x + r1.width)) * (this.y - (r1.y + r1.height)) / (r2.y + r2.height - (r1.y + r1.height)); } return this.x > o1 && this.x < u1; } return false; } }); deepExtend(Point, { parse: function (str) { var tempStr = str.slice(1, str.length - 1), xy = tempStr.split(','), x = parseInt(xy[0], 10), y = parseInt(xy[1], 10); if (!isNaN(x) && !isNaN(y)) { return new Point(x, y); } } }); var PathDefiner = Class.extend({ init: function (p, left, right) { this.point = p; this.left = left; this.right = right; } }); var Rect = Class.extend({ init: function (x, y, width, height) { this.x = x || 0; this.y = y || 0; this.width = width || 0; this.height = height || 0; }, contains: function (point) { return point.x >= this.x && point.x <= this.x + this.width && point.y >= this.y && point.y <= this.y + this.height; }, inflate: function (dx, dy) { if (dy === undefined) { dy = dx; } this.x -= dx; this.y -= dy; this.width += 2 * dx + 1; this.height += 2 * dy + 1; return this; }, offset: function (dx, dy) { var x = dx, y = dy; if (dx instanceof Point) { x = dx.x; y = dx.y; } this.x += x; this.y += y; return this; }, union: function (r) { var x1 = Math.min(this.x, r.x); var y1 = Math.min(this.y, r.y); var x2 = Math.max(this.x + this.width, r.x + r.width); var y2 = Math.max(this.y + this.height, r.y + r.height); return new Rect(x1, y1, x2 - x1, y2 - y1); }, center: function () { return new Point(this.x + this.width / 2, this.y + this.height / 2); }, top: function () { return new Point(this.x + this.width / 2, this.y); }, right: function () { return new Point(this.x + this.width, this.y + this.height / 2); }, bottom: function () { return new Point(this.x + this.width / 2, this.y + this.height); }, left: function () { return new Point(this.x, this.y + this.height / 2); }, topLeft: function () { return new Point(this.x, this.y); }, topRight: function () { return new Point(this.x + this.width, this.y); }, bottomLeft: function () { return new Point(this.x, this.y + this.height); }, bottomRight: function () { return new Point(this.x + this.width, this.y + this.height); }, clone: function () { return new Rect(this.x, this.y, this.width, this.height); }, isEmpty: function () { return !this.width && !this.height; }, equals: function (rect) { return this.x === rect.x && this.y === rect.y && this.width === rect.width && this.height === rect.height; }, rotatedBounds: function (angle) { var rect = this.clone(), points = this.rotatedPoints(angle), tl = points[0], tr = points[1], br = points[2], bl = points[3]; rect.x = Math.min(br.x, tl.x, tr.x, bl.x); rect.y = Math.min(br.y, tl.y, tr.y, bl.y); rect.width = Math.max(br.x, tl.x, tr.x, bl.x) - rect.x; rect.height = Math.max(br.y, tl.y, tr.y, bl.y) - rect.y; return rect; }, rotatedPoints: function (angle) { var rect = this, c = rect.center(), br = rect.bottomRight().rotate(c, 360 - angle), tl = rect.topLeft().rotate(c, 360 - angle), tr = rect.topRight().rotate(c, 360 - angle), bl = rect.bottomLeft().rotate(c, 360 - angle); return [ tl, tr, br, bl ]; }, toString: function (delimiter) { delimiter = delimiter || ' '; return this.x + delimiter + this.y + delimiter + this.width + delimiter + this.height; }, scale: function (scaleX, scaleY, staicPoint, adornerCenter, angle) { var tl = this.topLeft(); var thisCenter = this.center(); tl.rotate(thisCenter, 360 - angle).rotate(adornerCenter, angle); var delta = staicPoint.minus(tl); var scaled = new Point(delta.x * scaleX, delta.y * scaleY); var position = delta.minus(scaled); tl = tl.plus(position); tl.rotate(adornerCenter, 360 - angle).rotate(thisCenter, angle); this.x = tl.x; this.y = tl.y; this.width *= scaleX; this.height *= scaleY; }, zoom: function (zoom) { this.x *= zoom; this.y *= zoom; this.width *= zoom; this.height *= zoom; return this; }, overlaps: function (rect) { var bottomRight = this.bottomRight(); var rectBottomRight = rect.bottomRight(); var overlaps = !(bottomRight.x < rect.x || bottomRight.y < rect.y || rectBottomRight.x < this.x || rectBottomRight.y < this.y); return overlaps; } }); var Size = Class.extend({ init: function (width, height) { this.width = width; this.height = height; } }); Size.prototype.Empty = new Size(0, 0); Rect.toRect = function (rect) { if (!(rect instanceof Rect)) { rect = new Rect(rect.x, rect.y, rect.width, rect.height); } return rect; }; Rect.empty = function () { return new Rect(0, 0, 0, 0); }; Rect.fromPoints = function (p, q) { if (isNaN(p.x) || isNaN(p.y) || isNaN(q.x) || isNaN(q.y)) { throw 'Some values are NaN.'; } return new Rect(Math.min(p.x, q.x), Math.min(p.y, q.y), Math.abs(p.x - q.x), Math.abs(p.y - q.y)); }; function isNearZero(num) { return Math.abs(num) < EPSILON; } function intersectLine(start1, end1, start2, end2, isSegment) { var tangensdiff = (end1.x - start1.x) * (end2.y - start2.y) - (end1.y - start1.y) * (end2.x - start2.x); if (isNearZero(tangensdiff)) { return; } var num1 = (start1.y - start2.y) * (end2.x - start2.x) - (start1.x - start2.x) * (end2.y - start2.y); var num2 = (start1.y - start2.y) * (end1.x - start1.x) - (start1.x - start2.x) * (end1.y - start1.y); var r = num1 / tangensdiff; var s = num2 / tangensdiff; if (isSegment && (r < 0 || r > 1 || s < 0 || s > 1)) { return; } return new Point(start1.x + r * (end1.x - start1.x), start1.y + r * (end1.y - start1.y)); } var Intersect = { lines: function (start1, end1, start2, end2) { return intersectLine(start1, end1, start2, end2); }, segments: function (start1, end1, start2, end2) { return intersectLine(start1, end1, start2, end2, true); }, rectWithLine: function (rect, start, end) { return Intersect.segments(start, end, rect.topLeft(), rect.topRight()) || Intersect.segments(start, end, rect.topRight(), rect.bottomRight()) || Intersect.segments(start, end, rect.bottomLeft(), rect.bottomRight()) || Intersect.segments(start, end, rect.topLeft(), rect.bottomLeft()); }, rects: function (rect1, rect2, angle) { var tl = rect2.topLeft(), tr = rect2.topRight(), bl = rect2.bottomLeft(), br = rect2.bottomRight(); var center = rect2.center(); if (angle) { tl = tl.rotate(center, angle); tr = tr.rotate(center, angle); bl = bl.rotate(center, angle); br = br.rotate(center, angle); } var intersect = rect1.contains(tl) || rect1.contains(tr) || rect1.contains(bl) || rect1.contains(br) || Intersect.rectWithLine(rect1, tl, tr) || Intersect.rectWithLine(rect1, tl, bl) || Intersect.rectWithLine(rect1, tr, br) || Intersect.rectWithLine(rect1, bl, br); if (!intersect) { tl = rect1.topLeft(); tr = rect1.topRight(); bl = rect1.bottomLeft(); br = rect1.bottomRight(); if (angle) { var reverseAngle = 360 - angle; tl = tl.rotate(center, reverseAngle); tr = tr.rotate(center, reverseAngle); bl = bl.rotate(center, reverseAngle); br = br.rotate(center, reverseAngle); } intersect = rect2.contains(tl) || rect2.contains(tr) || rect2.contains(bl) || rect2.contains(br); } return intersect; } }; var RectAlign = Class.extend({ init: function (container) { this.container = Rect.toRect(container); }, align: function (content, alignment) { var alignValues = alignment.toLowerCase().split(' '); for (var i = 0; i < alignValues.length; i++) { content = this._singleAlign(content, alignValues[i]); } return content; }, _singleAlign: function (content, alignment) { if (isFunction(this[alignment])) { return this[alignment](content); } else { return content; } }, left: function (content) { return this._align(content, this._left); }, center: function (content) { return this._align(content, this._center); }, right: function (content) { return this._align(content, this._right); }, stretch: function (content) { return this._align(content, this._stretch); }, top: function (content) { return this._align(content, this._top); }, middle: function (content) { return this._align(content, this._middle); }, bottom: function (content) { return this._align(content, this._bottom); }, _left: function (container, content) { content.x = container.x; }, _center: function (container, content) { content.x = (container.width - content.width) / 2 || 0; }, _right: function (container, content) { content.x = container.width - content.width; }, _top: function (container, content) { content.y = container.y; }, _middle: function (container, content) { content.y = (container.height - content.height) / 2 || 0; }, _bottom: function (container, content) { content.y = container.height - content.height; }, _stretch: function (container, content) { content.x = 0; content.y = 0; content.height = container.height; content.width = container.width; }, _align: function (content, alignCalc) { content = Rect.toRect(content); alignCalc(this.container, content); return content; } }); var Polar = Class.extend({ init: function (r, a) { this.r = r; this.angle = a; } }); var Matrix = Class.extend({ init: function (a, b, c, d, e, f) { this.a = a || 0; this.b = b || 0; this.c = c || 0; this.d = d || 0; this.e = e || 0; this.f = f || 0; }, plus: function (m) { this.a += m.a; this.b += m.b; this.c += m.c; this.d += m.d; this.e += m.e; this.f += m.f; }, minus: function (m) { this.a -= m.a; this.b -= m.b; this.c -= m.c; this.d -= m.d; this.e -= m.e; this.f -= m.f; }, times: function (m) { return new Matrix(this.a * m.a + this.c * m.b, this.b * m.a + this.d * m.b, this.a * m.c + this.c * m.d, this.b * m.c + this.d * m.d, this.a * m.e + this.c * m.f + this.e, this.b * m.e + this.d * m.f + this.f); }, apply: function (p) { return new Point(this.a * p.x + this.c * p.y + this.e, this.b * p.x + this.d * p.y + this.f); }, applyRect: function (r) { return Rect.fromPoints(this.apply(r.topLeft()), this.apply(r.bottomRight())); }, toString: function () { return 'matrix(' + this.a + ' ' + this.b + ' ' + this.c + ' ' + this.d + ' ' + this.e + ' ' + this.f + ')'; } }); deepExtend(Matrix, { fromSVGMatrix: function (vm) { var m = new Matrix(); m.a = vm.a; m.b = vm.b; m.c = vm.c; m.d = vm.d; m.e = vm.e; m.f = vm.f; return m; }, fromMatrixVector: function (v) { var m = new Matrix(); m.a = v.a; m.b = v.b; m.c = v.c; m.d = v.d; m.e = v.e; m.f = v.f; return m; }, fromList: function (v) { if (v.length !== 6) { throw 'The given list should consist of six elements.'; } var m = new Matrix(); m.a = v[0]; m.b = v[1]; m.c = v[2]; m.d = v[3]; m.e = v[4]; m.f = v[5]; return m; }, translation: function (x, y) { var m = new Matrix(); m.a = 1; m.b = 0; m.c = 0; m.d = 1; m.e = x; m.f = y; return m; }, unit: function () { return new Matrix(1, 0, 0, 1, 0, 0); }, rotation: function (angle, x, y) { var m = new Matrix(); m.a = Math.cos(angle * Math.PI / 180); m.b = Math.sin(angle * Math.PI / 180); m.c = -m.b; m.d = m.a; m.e = x - x * m.a + y * m.b || 0; m.f = y - y * m.a - x * m.b || 0; return m; }, scaling: function (scaleX, scaleY) { var m = new Matrix(); m.a = scaleX; m.b = 0; m.c = 0; m.d = scaleY; m.e = 0; m.f = 0; return m; }, parse: function (v) { var parts, nums; if (v) { v = v.trim(); if (v.slice(0, 6).toLowerCase() === 'matrix') { nums = v.slice(7, v.length - 1).trim(); parts = nums.split(','); if (parts.length === 6) { return Matrix.fromList(map(parts, function (p) { return parseFloat(p); })); } parts = nums.split(' '); if (parts.length === 6) { return Matrix.fromList(map(parts, function (p) { return parseFloat(p); })); } } if (v.slice(0, 1) === '(' && v.slice(v.length - 1) === ')') { v = v.substr(1, v.length - 1); } if (v.indexOf(',') > 0) { parts = v.split(','); if (parts.length === 6) { return Matrix.fromList(map(parts, function (p) { return parseFloat(p); })); } } if (v.indexOf(' ') > 0) { parts = v.split(' '); if (parts.length === 6) { return Matrix.fromList(map(parts, function (p) { return parseFloat(p); })); } } } return parts; } }); var MatrixVector = Class.extend({ init: function (a, b, c, d, e, f) { this.a = a || 0; this.b = b || 0; this.c = c || 0; this.d = d || 0; this.e = e || 0; this.f = f || 0; }, fromMatrix: function FromMatrix(m) { var v = new MatrixVector(); v.a = m.a; v.b = m.b; v.c = m.c; v.d = m.d; v.e = m.e; v.f = m.f; return v; } }); function normalVariable(mean, deviation) { var x, y, r; do { x = Math.random() * 2 - 1; y = Math.random() * 2 - 1; r = x * x + y * y; } while (!r || r > 1); return mean + deviation * x * Math.sqrt(-2 * Math.log(r) / r); } function randomId(length) { if (Utils.isUndefined(length)) { length = 10; } var result = ''; var chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; for (var i = length; i > 0; --i) { result += chars.charAt(Math.round(Math.random() * (chars.length - 1))); } return result; } var Geometry = { _distanceToLineSquared: function (p, a, b) { function d2(pt1, pt2) { return (pt1.x - pt2.x) * (pt1.x - pt2.x) + (pt1.y - pt2.y) * (pt1.y - pt2.y); } if (a === b) { return d2(p, a); } var vx = b.x - a.x, vy = b.y - a.y, dot = (p.x - a.x) * vx + (p.y - a.y) * vy; if (dot < 0) { return d2(a, p); } dot = (b.x - p.x) * vx + (b.y - p.y) * vy; if (dot < 0) { return d2(b, p); } dot = (b.x - p.x) * vy - (b.y - p.y) * vx; return dot * dot / (vx * vx + vy * vy); }, distanceToLine: function (p, a, b) { return Math.sqrt(this._distanceToLineSquared(p, a, b)); }, distanceToPolyline: function (p, points) { var minimum = Number.MAX_VALUE; if (Utils.isUndefined(points) || points.length === 0) { return Number.MAX_VALUE; } for (var s = 0; s < points.length - 1; s++) { var p1 = points[s]; var p2 = points[s + 1]; var d = this._distanceToLineSquared(p, p1, p2); if (d < minimum) { minimum = d; } } return Math.sqrt(minimum); } }; var HashTable = kendo.Class.extend({ init: function () { this._buckets = []; this.length = 0; }, add: function (key, value) { var obj = this._createGetBucket(key); if (Utils.isDefined(value)) { obj.value = value; } return obj; }, get: function (key) { if (this._bucketExists(key)) { return this._createGetBucket(key); } return null; }, set: function (key, value) { this.add(key, value); }, containsKey: function (key) { return this._bucketExists(key); }, remove: function (key) { if (this._bucketExists(key)) { var hashId = this._hash(key); delete this._buckets[hashId]; this.length--; return key; } }, forEach: function (func) { var hashes = this._hashes(); for (var i = 0, len = hashes.length; i < len; i++) { var hash = hashes[i]; var bucket = this._buckets[hash]; if (Utils.isUndefined(bucket)) { continue; } func(bucket); } }, clone: function () { var ht = new HashTable(); var hashes = this._hashes(); for (var i = 0, len = hashes.length; i < len; i++) { var hash = hashes[i]; var bucket = this._buckets[hash]; if (Utils.isUndefined(bucket)) { continue; } ht.add(bucket.key, bucket.value); } return ht; }, _hashes: function () { var hashes = []; for (var hash in this._buckets) { if (this._buckets.hasOwnProperty(hash)) { hashes.push(hash); } } return hashes; }, _bucketExists: function (key) { var hashId = this._hash(key); return Utils.isDefined(this._buckets[hashId]); }, _createGetBucket: function (key) { var hashId = this._hash(key); var bucket = this._buckets[hashId]; if (Utils.isUndefined(bucket)) { bucket = { key: key }; this._buckets[hashId] = bucket; this.length++; } return bucket; }, _hash: function (key) { if (Utils.isNumber(key)) { return key; } if (Utils.isString(key)) { return this._hashString(key); } if (Utils.isObject(key)) { return this._objectHashId(key); } throw 'Unsupported key type.'; }, _hashString: function (s) { var result = 0; if (s.length === 0) { return result; } for (var i = 0; i < s.length; i++) { var ch = s.charCodeAt(i); result = result * 32 - result + ch; } return result; }, _objectHashId: function (key) { var id = key._hashId; if (Utils.isUndefined(id)) { id = randomId(); key._hashId = id; } return id; } }); var Dictionary = kendo.Observable.extend({ init: function (dictionary) { var that = this; kendo.Observable.fn.init.call(that); this._hashTable = new HashTable(); this.length = 0; if (Utils.isDefined(dictionary)) { if ($.isArray(dictionary)) { for (var i = 0; i < dictionary.length; i++) { this.add(dictionary[i]); } } else { dictionary.forEach(function (k, v) { this.add(k, v); }, this); } } }, add: function (key, value) { var entry = this._hashTable.get(key); if (!entry) { entry = this._hashTable.add(key); this.length++; this.trigger('changed'); } entry.value = value; }, set: function (key, value) { this.add(key, value); }, get: function (key) { var entry = this._hashTable.get(key); if (entry) { return entry.value; } throw new Error('Cannot find key ' + key); }, containsKey: function (key) { return this._hashTable.containsKey(key); }, remove: function (key) { if (this.containsKey(key)) { this.trigger('changed'); this.length--; return this._hashTable.remove(key); } }, forEach: function (func, thisRef) { this._hashTable.forEach(function (entry) { func.call(thisRef, entry.key, entry.value); }); }, forEachValue: function (func, thisRef) { this._hashTable.forEach(function (entry) { func.call(thisRef, entry.value); }); }, forEachKey: function (func, thisRef) { this._hashTable.forEach(function (entry) { func.call(thisRef, entry.key); }); }, keys: function () { var keys = []; this.forEachKey(function (key) { keys.push(key); }); return keys; } }); var Queue = kendo.Class.extend({ init: function () { this._tail = null; this._head = null; this.length = 0; }, enqueue: function (value) { var entry = { value: value, next: null }; if (!this._head) { this._head = entry; this._tail = this._head; } else { this._tail.next = entry; this._tail = this._tail.next; } this.length++; }, dequeue: function () { if (this.length < 1) { throw new Error('The queue is empty.'); } var value = this._head.value; this._head = this._head.next; this.length--; return value; }, contains: function (item) { var current = this._head; while (current) { if (current.value === item) { return true; } current = current.next; } return false; } }); var Set = kendo.Observable.extend({ init: function (resource) { var that = this; kendo.Observable.fn.init.call(that); this._hashTable = new HashTable(); this.length = 0; if (Utils.isDefined(resource)) { if (resource instanceof HashTable) { resource.forEach(function (d) { this.add(d); }); } else if (resource instanceof Dictionary) { resource.forEach(function (k, v) { this.add({ key: k, value: v }); }, this); } } }, contains: function (item) { return this._hashTable.containsKey(item); }, add: function (item) { var entry = this._hashTable.get(item); if (!entry) { this._hashTable.add(item, item); this.length++; this.trigger('changed'); } }, get: function (item) { if (this.contains(item)) { return this._hashTable.get(item).value; } else { return null; } }, hash: function (item) { return this._hashTable._hash(item); }, remove: function (item) { if (this.contains(item)) { this._hashTable.remove(item); this.length--; this.trigger('changed'); } }, forEach: function (func, context) { this._hashTable.forEach(function (kv) { func(kv.value); }, context); }, toArray: function () { var r = []; this.forEach(function (d) { r.push(d); }); return r; } }); var Node = kendo.Class.extend({ init: function (id, shape) { this.links = []; this.outgoing = []; this.incoming = []; this.weight = 1; if (Utils.isDefined(id)) { this.id = id; } else { this.id = randomId(); } if (Utils.isDefined(shape)) { this.associatedShape = shape; var b = shape.bounds(); this.width = b.width; this.height = b.height; this.x = b.x; this.y = b.y; } else { this.associatedShape = null; } this.data = null; this.type = 'Node'; this.shortForm = 'Node \'' + this.id + '\''; this.isVirtual = false; }, isIsolated: function () { return Utils.isEmpty(this.links); }, bounds: function (r) { if (!Utils.isDefined(r)) { return new diagram.Rect(this.x, this.y, this.width, this.height); } this.x = r.x; this.y = r.y; this.width = r.width; this.height = r.height; }, isLinkedTo: function (node) { var that = this; return Utils.any(that.links, function (link) { return link.getComplement(that) === node; }); }, getChildren: function () { if (this.outgoing.length === 0) { return []; } var children = []; for (var i = 0, len = this.outgoing.length; i < len; i++) { var link = this.outgoing[i]; children.push(link.getComplement(this)); } return children; }, getParents: function () { if (this.incoming.length === 0) { return []; } var parents = []; for (var i = 0, len = this.incoming.length; i < len; i++) { var link = this.incoming[i]; parents.push(link.getComplement(this)); } return parents; }, clone: function () { var copy = new Node(); if (Utils.isDefined(this.weight)) { copy.weight = this.weight; } if (Utils.isDefined(this.balance)) { copy.balance = this.balance; } if (Utils.isDefined(this.owner)) { copy.owner = this.owner; } copy.associatedShape = this.associatedShape; copy.x = this.x; copy.y = this.y; copy.width = this.width; copy.height = this.height; return copy; }, adjacentTo: function (node) { return this.isLinkedTo(node) !== null; }, removeLink: function (link) { if (link.source === this) { Utils.remove(this.links, link); Utils.remove(this.outgoing, link); link.source = null; } if (link.target === this) { Utils.remove(this.links, link); Utils.remove(this.incoming, link); link.target = null; } }, hasLinkTo: function (node) { return Utils.any(this.outgoing, function (link) { return link.target === node; }); }, degree: function () { return this.links.length; }, incidentWith: function (link) { return contains(this.links, link); }, getLinksWith: function (node) { return Utils.all(this.links, function (link) { return link.getComplement(this) === node; }, this); }, getNeighbors: function () { var neighbors = []; Utils.forEach(this.incoming, function (e) { neighbors.push(e.getComplement(this)); }, this); Utils.forEach(this.outgoing, function (e) { neighbors.push(e.getComplement(this)); }, this); return neighbors; } }); var Link = kendo.Class.extend({ init: function (source, target, id, connection) { if (Utils.isUndefined(source)) { throw 'The source of the new link is not set.'; } if (Utils.isUndefined(target)) { throw 'The target of the new link is not set.'; } var sourceFound, targetFound; if (Utils.isString(source)) { sourceFound = new Node(source); } else { sourceFound = source; } if (Utils.isString(target)) { targetFound = new Node(target); } else { targetFound = target; } this.source = sourceFound; this.target = targetFound; this.source.links.push(this); this.target.links.push(this); this.source.outgoing.push(this); this.target.incoming.push(this); if (Utils.isDefined(id)) { this.id = id; } else { this.id = randomId(); } if (Utils.isDefined(connection)) { this.associatedConnection = connection; } else { this.associatedConnection = null; } this.type = 'Link'; this.shortForm = 'Link \'' + this.source.id + '->' + this.target.id + '\''; }, getComplement: function (node) { if (this.source !== node && this.target !== node) { throw 'The given node is not incident with this link.'; } return this.source === node ? this.target : this.source; }, getCommonNode: function (link) { if (this.source === link.source || this.source === link.target) { return this.source; } if (this.target === link.source || this.target === link.target) { return this.target; } return null; }, isBridging: function (v1, v2) { return this.source === v1 && this.target === v2 || this.source === v2 && this.target === v1; }, getNodes: function () { return [ this.source, this.target ]; }, incidentWith: function (node) { return this.source === node || this.target === node; }, adjacentTo: function (link) { return contains(this.source.links, link) || contains(this.target.links, link); }, changeSource: function (node) { Utils.remove(this.source.links, this); Utils.remove(this.source.outgoing, this); node.links.push(this); node.outgoing.push(this); this.source = node; }, changeTarget: function (node) { Utils.remove(this.target.links, this); Utils.remove(this.target.incoming, this); node.links.push(this); node.incoming.push(this); this.target = node; }, changesNodes: function (v, w) { if (this.source === v) { this.changeSource(w); } else if (this.target === v) { this.changeTarget(w); } }, reverse: function () { var oldSource = this.source; var oldTarget = this.target; this.source = oldTarget; Utils.remove(oldSource.outgoing, this); this.source.outgoing.push(this); this.target = oldSource; Utils.remove(oldTarget.incoming, this); this.target.incoming.push(this); return this; }, directTo: function (target) { if (this.source !== target && this.target !== target) { throw 'The given node is not incident with this link.'; } if (this.target !== target) { this.reverse(); } }, createReverseEdge: function () { var r = this.clone(); r.reverse(); r.reversed = true; return r; }, clone: function () { var clone = new Link(this.source, this.target); return clone; } }); var Graph = kendo.Class.extend({ init: function (idOrDiagram) { this.links = []; this.nodes = []; this._nodeMap = new Dictionary(); this.diagram = null; this._root = null; if (Utils.isDefined(idOrDiagram)) { if (Utils.isString(idOrDiagram)) { this.id = idOrDiagram; } else { this.diagram = idOrDiagram; this.id = idOrDiagram.id; } } else { this.id = randomId(); } this.bounds = new Rect(); this._hasCachedRelationships = false; this.type = 'Graph'; }, cacheRelationships: function (forceRebuild) { if (Utils.isUndefined(forceRebuild)) { forceRebuild = false; } if (this._hasCachedRelationships && !forceRebuild) { return; } for (var i = 0, len = this.nodes.length; i < len; i++) { var node = this.nodes[i]; node.children = this.getChildren(node); node.parents = this.getParents(node); } this._hasCachedRelationships = true; }, assignLevels: function (startNode, offset, visited) { if (!startNode) { throw 'Start node not specified.'; } if (Utils.isUndefined(offset)) { offset = 0; } this.cacheRelationships(); if (Utils.isUndefined(visited)) { visited = new Dictionary(); Utils.forEach(this.nodes, function (n) { visited.add(n, false); }); } visited.set(startNode, true); startNode.level = offset; var children = startNode.children; for (var i = 0, len = children.length; i < len; i++) { var child = children[i]; if (!child || visited.get(child)) { continue; } this.assignLevels(child, offset + 1, visited); } }, root: function (value) { if (Utils.isUndefined(value)) { if (!this._root) { var found = Utils.first(this.nodes, function (n) { return n.incoming.length === 0; }); if (found) { return found; } return Utils.first(this.nodes); } else { return this._root; } } else { this._root = value; } }, getConnectedComponents: function () { this.componentIndex = 0; this.setItemIndices(); var componentId = Utils.initArray(this.nodes.length, -1); for (var v = 0; v < this.nodes.length; v++) { if (componentId[v] === -1) { this._collectConnectedNodes(componentId, v); this.componentIndex++; } } var components = [], i; for (i = 0; i < this.componentIndex; ++i) { components[i] = new Graph(); } for (i = 0; i < componentId.length; ++i) { var graph = components[componentId[i]]; graph.addNodeAndOutgoings(this.nodes[i]); } components.sort(function (a, b) { return b.nodes.length - a.nodes.length; }); return components; }, _collectConnectedNodes: function (setIds, nodeIndex) { setIds[nodeIndex] = this.componentIndex; var node = this.nodes[nodeIndex]; Utils.forEach(node.links, function (link) { var next = link.getComplement(node); var nextId = next.index; if (setIds[nextId] === -1) { this._collectConnectedNodes(setIds, nextId); } }, this); }, calcBounds: function () { if (this.isEmpty()) { this.bounds = new Rect(); return this.bounds; } var b = null; for (var i = 0, len = this.nodes.length; i < len; i++) { var node = this.nodes[i]; if (!b) { b = node.bounds(); } else { b = b.union(node.bounds()); } } this.bounds = b; return this.bounds; }, getSpanningTree: function (root) { var tree = new Graph(); var map = new Dictionary(), source, target; tree.root = root.clone(); tree.root.level = 0; tree.root.id = root.id; map.add(root, tree.root); root.level = 0; var visited = []; var remaining = []; tree._addNode(tree.root); visited.push(root); remaining.push(root); var levelCount = 1; while (remaining.length > 0) { var next = remaining.pop(); for (var ni = 0; ni < next.links.length; ni++) { var link = next.links[ni]; var cn = link.getComplement(next); if (contains(visited, cn)) { continue; } cn.level = next.level + 1; if (levelCount < cn.level + 1) { levelCount = cn.level + 1; } if (!contains(remaining, cn)) { remaining.push(cn); } if (!contains(visited, cn)) { visited.push(cn); } if (map.containsKey(next)) { source = map.get(next); } else { source = next.clone(); source.level = next.level; source.id = next.id; map.add(next, source); } if (map.containsKey(cn)) { target = map.get(cn); } else { target = cn.clone(); target.level = cn.level; target.id = cn.id; map.add(cn, target); } var newLink = new Link(source, target); tree.addLink(newLink); } } var treeLevels = []; for (var i = 0; i < levelCount; i++) { treeLevels.push([]); } Utils.forEach(tree.nodes, function (node) { treeLevels[node.level].push(node); }); tree.treeLevels = treeLevels; tree.cacheRelationships(); return tree; }, takeRandomNode: function (excludedNodes, incidenceLessThan) { if (Utils.isUndefined(excludedNodes)) { excludedNodes = []; } if (Utils.isUndefined(incidenceLessThan)) { incidenceLessThan = 4; } if (this.nodes.length === 0) { return null; } if (this.nodes.length === 1) { return contains(excludedNodes, this.nodes[0]) ? null : this.nodes[0]; } var pool = $.grep(this.nodes, function (node) { return !contains(excludedNodes, node) && node.degree() <= incidenceLessThan; }); if (Utils.isEmpty(pool)) { return null; } return pool[Utils.randomInteger(0, pool.length)]; }, isEmpty: function () { return Utils.isEmpty(this.nodes); }, isHealthy: function () { return Utils.all(this.links, function (link) { return contains(this.nodes, link.source) && contains(this.nodes, link.target); }, this); }, getParents: function (n) { if (!this.hasNode(n)) { throw 'The given node is not part of this graph.'; } return n.getParents(); }, getChildren: function (n) { if (!this.hasNode(n)) { throw 'The given node is not part of this graph.'; } return n.getChildren(); }, addLink: function (sourceOrLink, target, owner) { if (Utils.isUndefined(sourceOrLink)) { throw 'The source of the link is not defined.'; } if (Utils.isUndefined(target)) { if (Utils.isDefined(sourceOrLink.type) && sourceOrLink.type === 'Link') { this.addExistingLink(sourceOrLink); return; } else { throw 'The target of the link is not defined.'; } } var foundSource = this.getNode(sourceOrLink); if (Utils.isUndefined(foundSource)) { foundSource = this.addNode(sourceOrLink); } var foundTarget = this.getNode(target); if (Utils.isUndefined(foundTarget)) { foundTarget = this.addNode(target); } var newLink = new Link(foundSource, foundTarget); if (Utils.isDefined(owner)) { newLink.owner = owner; } this.links.push(newLink); return newLink; }, removeAllLinks: function () { while (this.links.length > 0) { var link = this.links[0]; this.removeLink(link); } }, addExistingLink: function (link) { if (this.hasLink(link)) { return; } this.links.push(link); if (this.hasNode(link.source.id)) { var s = this.getNode(link.source.id); link.changeSource(s); } else { this.addNode(link.source); } if (this.hasNode(link.target.id)) { var t = this.getNode(link.target.id); link.changeTarget(t); } else { this.addNode(link.target); } }, hasLink: function (linkOrId) { if (Utils.isString(linkOrId)) { return Utils.any(this.links, function (link) { return link.id === linkOrId; }); } if (linkOrId.type === 'Link') { return contains(this.links, linkOrId); } throw 'The given object is neither an identifier nor a Link.'; }, getNode: function (nodeOrId) { var id = nodeOrId.id || nodeOrId; if (this._nodeMap.containsKey(id)) { return this._nodeMap.get(id); } }, hasNode: function (nodeOrId) { var id = nodeOrId.id || nodeOrId; return this._nodeMap.containsKey(id); }, _addNode: function (node) { this.nodes.push(node); this._nodeMap.add(node.id, node); }, _removeNode: function (node) { Utils.remove(this.nodes, node); this._nodeMap.remove(node.id); }, removeNode: function (nodeOrId) { var n = nodeOrId; if (Utils.isString(nodeOrId)) { n = this.getNode(nodeOrId); } if (Utils.isDefined(n)) { var links = n.links; n.links = []; for (var i = 0, len = links.length; i < len; i++) { var link = links[i]; this.removeLink(link); } this._removeNode(n); } else { throw 'The identifier should be a Node or the Id (string) of a node.'; } }, areConnected: function (n1, n2) { return Utils.any(this.links, function (link) { return link.source == n1 && link.target == n2 || link.source == n2 && link.target == n1; }); }, removeLink: function (link) { Utils.remove(this.links, link); Utils.remove(link.source.outgoing, link); Utils.remove(link.source.links, link); Utils.remove(link.target.incoming, link); Utils.remove(link.target.links, link); }, addNode: function (nodeOrId, layoutRect, owner) { var newNode = null; if (!Utils.isDefined(nodeOrId)) { throw 'No Node or identifier for a new Node is given.'; } if (Utils.isString(nodeOrId)) { if (this.hasNode(nodeOrId)) { return this.getNode(nodeOrId); } newNode = new Node(nodeOrId); } else { if (this.hasNode(nodeOrId)) { return this.getNode(nodeOrId); } newNode = nodeOrId; } if (Utils.isDefined(layoutRect)) { newNode.bounds(layoutRect); } if (Utils.isDefined(owner)) { newNode.owner = owner; } this._addNode(newNode); return newNode; }, addNodeAndOutgoings: function (node) { if (!this.hasNode(node)) { this._addNode(node); } var newLinks = node.outgoing; node.outgoing = []; Utils.forEach(newLinks, function (link) { this.addExistingLink(link); }, this); }, setItemIndices: function () { var i; for (i = 0; i < this.nodes.length; ++i) { this.nodes[i].index = i; } for (i = 0; i < this.links.length; ++i) { this.links[i].index = i; } }, clone: function (saveMapping) { var copy = new Graph(); var save = Utils.isDefined(saveMapping) && saveMapping === true; if (save) { copy.nodeMap = new Dictionary(); copy.linkMap = new Dictionary(); } var map = new Dictionary(); Utils.forEach(this.nodes, function (nOriginal) { var nCopy = nOriginal.clone(); map.set(nOriginal, nCopy); copy._addNode(nCopy); if (save) { copy.nodeMap.set(nCopy, nOriginal); } }); Utils.forEach(this.links, function (linkOriginal) { if (map.containsKey(linkOriginal.source) && map.containsKey(linkOriginal.target)) { var linkCopy = copy.addLink(map.get(linkOriginal.source), map.get(linkOriginal.target)); if (save) { copy.linkMap.set(linkCopy, linkOriginal); } } }); return copy; }, linearize: function (addIds) { return Graph.Utils.linearize(this, addIds); }, depthFirstTraversal: function (startNode, action) { if (Utils.isUndefined(startNode)) { throw 'You need to supply a starting node.'; } if (Utils.isUndefined(action)) { throw 'You need to supply an action.'; } if (!this.hasNode(startNode)) { throw 'The given start-node is not part of this graph'; } var foundNode = this.getNode(startNode); var visited = []; this._dftIterator(foundNode, action, visited); }, _dftIterator: function (node, action, visited) { action(node); visited.push(node); var children = node.getChildren(); for (var i = 0, len = children.length; i < len; i++) { var child = children[i]; if (contains(visited, child)) { continue; } this._dftIterator(child, action, visited); } }, breadthFirstTraversal: function (startNode, action) { if (Utils.isUndefined(startNode)) { throw 'You need to supply a starting node.'; } if (Utils.isUndefined(action)) { throw 'You need to supply an action.'; } if (!this.hasNode(startNode)) { throw 'The given start-node is not part of this graph'; } var foundNode = this.getNode(startNode); var queue = new Queue(); var visited = []; queue.enqueue(foundNode); while (queue.length > 0) { var node = queue.dequeue(); action(node); visited.push(node); var children = node.getChildren(); for (var i = 0, len = children.length; i < len; i++) { var child = children[i]; if (contains(visited, child) || contains(queue, child)) { continue; } queue.enqueue(child); } } }, _stronglyConnectedComponents: function (excludeSingleItems, node, indices, lowLinks, connected, stack, index) { indices.add(node, index); lowLinks.add(node, index); index++; stack.push(node); var children = node.getChildren(), next; for (var i = 0, len = children.length; i < len; i++) { next = children[i]; if (!indices.containsKey(next)) { this._stronglyConnectedComponents(excludeSingleItems, next, indices, lowLinks, connected, stack, index); lowLinks.add(node, Math.min(lowLinks.get(node), lowLinks.get(next))); } else if (contains(stack, next)) { lowLinks.add(node, Math.min(lowLinks.get(node), indices.get(next))); } } if (lowLinks.get(node) === indices.get(node)) { var component = []; do { next = stack.pop(); component.push(next); } while (next !== node); if (!excludeSingleItems || component.length > 1) { connected.push(component); } } }, findCycles: function (excludeSingleItems) { if (Utils.isUndefined(excludeSingleItems)) { excludeSingleItems = true; } var indices = new Dictionary(); var lowLinks = new Dictionary(); var connected = []; var stack = []; for (var i = 0, len = this.nodes.length; i < len; i++) { var node = this.nodes[i]; if (indices.containsKey(node)) { continue; } this._stronglyConnectedComponents(excludeSingleItems, node, indices, lowLinks, connected, stack, 0); } return connected; }, isAcyclic: function () { return Utils.isEmpty(this.findCycles()); }, isSubGraph: function (other) { var otherArray = other.linearize(); var thisArray = this.linearize(); return Utils.all(otherArray, function (s) { return contains(thisArray, s); }); }, makeAcyclic: function () { if (this.isEmpty() || this.nodes.length <= 1 || this.links.length <= 1) { return []; } if (this.nodes.length == 2) { var result = []; if (this.links.length > 1) { var oneLink = this.links[0]; var oneNode = oneLink.source; for (var i = 0, len = this.links.length; i < len; i++) { var link = this.links[i]; if (link.source == oneNode) { continue; } var rev = link.reverse(); result.push(rev); } } return result; } var copy = this.clone(true); var N = this.nodes.length; var intensityCatalog = new Dictionary(); var flowIntensity = function (node) { if (node.outgoing.length === 0) { return 2 - N; } else if (node.incoming.length === 0) { return N - 2; } else { return node.outgoing.length - node.incoming.length; } }; var catalogEqualIntensity = function (node, intensityCatalog) { var intensity = flowIntensity(node, N); if (!intensityCatalog.containsKey(intensity)) { intensityCatalog.set(intensity, []); } intensityCatalog.get(intensity).push(node); }; Utils.forEach(copy.nodes, function (v) { catalogEqualIntensity(v, intensityCatalog); }); var sourceStack = []; var targetStack = []; while (copy.nodes.length > 0) { var source, target, intensity; if (intensityCatalog.containsKey(2 - N)) { var targets = intensityCatalog.get(2 - N); while (targets.length > 0) { target = targets.pop(); for (var li = 0; li < target.links.length; li++) { var targetLink = target.links[li]; source = targetLink.getComplement(target); intensity = flowIntensity(source, N); Utils.remove(intensityCatalog.get(intensity), source); source.removeLink(targetLink); catalogEqualIntensity(source, intensityCatalog); } copy._removeNode(target); targetStack.unshift(target); } } if (intensityCatalog.containsKey(N - 2)) { var sources = intensityCatalog.get(N - 2); while (sources.length > 0) { source = sources.pop(); for (var si = 0; si < source.links.length; si++) { var sourceLink = source.links[si]; target = sourceLink.getComplement(source); intensity = flowIntensity(target, N); Utils.remove(intensityCatalog.get(intensity), target); target.removeLink(sourceLink); catalogEqualIntensity(target, intensityCatalog); } sourceStack.push(source); copy._removeNode(source); } } if (copy.nodes.length > 0) { for (var k = N - 3; k > 2 - N; k--) { if (intensityCatalog.containsKey(k) && intensityCatalog.get(k).length > 0) { var maxdiff = intensityCatalog.get(k); var v = maxdiff.pop(); for (var ri = 0; ri < v.links.length; ri++) { var ril = v.links[ri]; var u = ril.getComplement(v); intensity = flowIntensity(u, N); Utils.remove(intensityCatalog.get(intensity), u); u.removeLink(ril); catalogEqualIntensity(u, intensityCatalog); } sourceStack.push(v); copy._removeNode(v); break; } } } } sourceStack = sourceStack.concat(targetStack); var vertexOrder = new Dictionary(); for (var kk = 0; kk < this.nodes.length; kk++) { vertexOrder.set(copy.nodeMap.get(sourceStack[kk]), kk); } var reversedEdges = []; Utils.forEach(this.links, function (link) { if (vertexOrder.get(link.source) > vertexOrder.get(link.target)) { link.reverse(); reversedEdges.push(link); } }); return reversedEdges; } }); Graph.Predefined = { EightGraph: function () { return Graph.Utils.parse([ '1->2', '2->3', '3->4', '4->1', '3->5', '5->6', '6->7', '7->3' ]); }, Mindmap: function () { return Graph.Utils.parse([ '0->1', '0->2', '0->3', '0->4', '0->5', '1->6', '1->7', '7->8', '2->9', '9->10', '9->11', '3->12', '12->13', '13->14', '4->15', '4->16', '15->17', '15->18', '18->19', '18->20', '14->21', '14->22', '5->23', '23->24', '23->25', '6->26' ]); }, ThreeGraph: function () { return Graph.Utils.parse([ '1->2', '2->3', '3->1' ]); }, BinaryTree: function (levels) { if (Utils.isUndefined(levels)) { levels = 5; } return Graph.Utils.createBalancedTree(levels, 2); }, Linear: function (length) { if (Utils.isUndefined(length)) { length = 10; } return Graph.Utils.createBalancedTree(length, 1); }, Tree: function (levels, siblingsCount) { return Graph.Utils.createBalancedTree(levels, siblingsCount); }, Forest: function (levels, siblingsCount, trees) { return Graph.Utils.createBalancedForest(levels, siblingsCount, trees); }, Workflow: function () { return Graph.Utils.parse([ '0->1', '1->2', '2->3', '1->4', '4->3', '3->5', '5->6', '6->3', '6->7', '5->4' ]); }, Grid: function (n, m) { var g = new diagram.Graph(); if (n <= 0 && m <= 0) { return g; } for (var i = 0; i < n + 1; i++) { var previous = null; for (var j = 0; j < m + 1; j++) { var node = new Node(i.toString() + '.' + j.toString()); g.addNode(node); if (previous) { g.addLink(previous, node); } if (i > 0) { var left = g.getNode((i - 1).toString() + '.' + j.toString()); g.addLink(left, node); } previous = node; } } return g; } }; Graph.Utils = { parse: function (graphString) { var previousLink, graph = new diagram.Graph(), parts = graphString.slice(); for (var i = 0, len = parts.length; i < len; i++) { var part = parts[i]; if (Utils.isString(part)) { if (part.indexOf('->') < 0) { throw 'The link should be specified as \'a->b\'.'; } var p = part.split('->'); if (p.length != 2) { throw 'The link should be specified as \'a->b\'.'; } previousLink = new Link(p[0], p[1]); graph.addLink(previousLink); } if (Utils.isObject(part)) { if (!previousLink) { throw 'Specification found before Link definition.'; } kendo.deepExtend(previousLink, part); } } return graph; }, linearize: function (graph, addIds) { if (Utils.isUndefined(graph)) { throw 'Expected an instance of a Graph object in slot one.'; } if (Utils.isUndefined(addIds)) { addIds = false; } var lin = []; for (var i = 0, len = graph.links.length; i < len; i++) { var link = graph.links[i]; lin.push(link.source.id + '->' + link.target.id); if (addIds) { lin.push({ id: link.id }); } } return lin; }, _addShape: function (kendoDiagram, p, id, shapeDefaults) { if (Utils.isUndefined(p)) { p = new diagram.Point(0, 0); } if (Utils.isUndefined(id)) { id = randomId(); } shapeDefaults = kendo.deepExtend({ width: 20, height: 20, id: id, radius: 10, fill: '#778899', data: 'circle', undoable: false, x: p.x, y: p.y }, shapeDefaults); return kendoDiagram.addShape(shapeDefaults); }, _addConnection: function (diagram, from, to, options) { return diagram.connect(from, to, options); }, createDiagramFromGraph: function (diagram, graph, doLayout, randomSize) { if (Utils.isUndefined(diagram)) { throw 'The diagram surface is undefined.'; } if (Utils.isUndefined(graph)) { throw 'No graph specification defined.'; } if (Utils.isUndefined(doLayout)) { doLayout = true; } if (Utils.isUndefined(randomSize)) { randomSize = false; } var width = diagram.element.clientWidth || 200; var height = diagram.element.clientHeight || 200; var map = [], node, shape; for (var i = 0, len = graph.nodes.length; i < len; i++) { node = graph.nodes[i]; var p = node.position; if (Utils.isUndefined(p)) { if (Utils.isDefined(node.x) && Utils.isDefined(node.y)) { p = new Point(node.x, node.y); } else { p = new Point(Utils.randomInteger(10, width - 20), Utils.randomInteger(10, height - 20)); } } var opt = {}; if (node.id === '0') { } else if (randomSize) { kendo.deepExtend(opt, { width: Math.random() * 150 + 20, height: Math.random() * 80 + 50, data: 'rectangle', fill: { color: '#778899' } }); } shape = this._addShape(diagram, p, node.id, opt); var bounds = shape.bounds(); if (Utils.isDefined(bounds)) { node.x = bounds.x; node.y = bounds.y; node.width = bounds.width; node.height = bounds.height; } map[node.id] = shape; } for (var gli = 0; gli < graph.links.length; gli++) { var link = graph.links[gli]; var sourceShape = map[link.source.id]; if (Utils.isUndefined(sourceShape)) { continue; } var targetShape = map[link.target.id]; if (Utils.isUndefined(targetShape)) { continue; } this._addConnection(diagram, sourceShape, targetShape, { id: link.id }); } if (doLayout) { var l = new diagram.SpringLayout(diagram); l.layoutGraph(graph, { limitToView: false }); for (var shi = 0; shi < graph.nodes.length; shi++) { node = graph.nodes[shi]; shape = map[node.id]; shape.bounds(new Rect(node.x, node.y, node.width, node.height)); } } }, createBalancedTree: function (levels, siblingsCount) { if (Utils.isUndefined(levels)) { levels = 3; } if (Utils.isUndefined(siblingsCount)) { siblingsCount = 3; } var g = new diagram.Graph(), counter = -1, lastAdded = [], news; if (levels <= 0 || siblingsCount <= 0) { return g; } var root = new Node((++counter).toString()); g.addNode(root); g.root = root; lastAdded.push(root); for (var i = 0; i < levels; i++) { news = []; for (var j = 0; j < lastAdded.length; j++) { var parent = lastAdded[j]; for (var k = 0; k < siblingsCount; k++) { var item = new Node((++counter).toString()); g.addLink(parent, item); news.push(item); } } lastAdded = news; } return g; }, createBalancedForest: function (levels, siblingsCount, treeCount) { if (Utils.isUndefined(levels)) { levels = 3; } if (Utils.isUndefined(siblingsCount)) { siblingsCount = 3; } if (Utils.isUndefined(treeCount)) { treeCount = 5; } var g = new diagram.Graph(), counter = -1, lastAdded = [], news; if (levels <= 0 || siblingsCount <= 0 || treeCount <= 0) { return g; } for (var t = 0; t < treeCount; t++) { var root = new Node((++counter).toString()); g.addNode(root); lastAdded = [root]; for (var i = 0; i < levels; i++) { news = []; for (var j = 0; j < lastAdded.length; j++) { var parent = lastAdded[j]; for (var k = 0; k < siblingsCount; k++) { var item = new Node((++counter).toString()); g.addLink(parent, item); news.push(item); } } lastAdded = news; } } return g; }, createRandomConnectedGraph: function (nodeCount, maxIncidence, isTree) { if (Utils.isUndefined(nodeCount)) { nodeCount = 40; } if (Utils.isUndefined(maxIncidence)) { maxIncidence = 4; } if (Utils.isUndefined(isTree)) { isTree = false; } var g = new diagram.Graph(), counter = -1; if (nodeCount <= 0) { return g; } var root = new Node((++counter).toString()); g.addNode(root); if (nodeCount === 1) { return g; } if (nodeCount > 1) { for (var i = 1; i < nodeCount; i++) { var poolNode = g.takeRandomNode([], maxIncidence); if (!poolNode) { break; } var newNode = g.addNode(i.toString()); g.addLink(poolNode, newNode); } if (!isTree && nodeCount > 1) { var randomAdditions = Utils.randomInteger(1, nodeCount); for (var ri = 0; ri < randomAdditions; ri++) { var n1 = g.takeRandomNode([], maxIncidence); var n2 = g.takeRandomNode([], maxIncidence); if (n1 && n2 && !g.areConnected(n1, n2)) { g.addLink(n1, n2); } } } return g; } }, randomDiagram: function (diagram, shapeCount, maxIncidence, isTree, randomSize) { var g = kendo.dataviz.diagram.Graph.Utils.createRandomConnectedGraph(shapeCount, maxIncidence, isTree); Graph.Utils.createDiagramFromGraph(diagram, g, false, randomSize); } }; kendo.deepExtend(diagram, { init: function (element) { kendo.init(element, diagram.ui); }, Point: Point, Intersect: Intersect, Geometry: Geometry, Rect: Rect, Size: Size, RectAlign: RectAlign, Matrix: Matrix, MatrixVector: MatrixVector, normalVariable: normalVariable, randomId: randomId, Dictionary: Dictionary, HashTable: HashTable, Queue: Queue, Set: Set, Node: Node, Link: Link, Graph: Graph, PathDefiner: PathDefiner }); }(window.kendo.jQuery)); }, typeof define == 'function' && define.amd ? define : function (a1, a2, a3) { (a3 || a2)(); })); (function (f, define) { define('dataviz/diagram/svg', [ 'kendo.drawing', 'dataviz/diagram/math' ], f); }(function () { (function ($, undefined) { var kendo = window.kendo, diagram = kendo.dataviz.diagram, Class = kendo.Class, deepExtend = kendo.deepExtend, Point = diagram.Point, Rect = diagram.Rect, Matrix = diagram.Matrix, Utils = diagram.Utils, isNumber = Utils.isNumber, isString = Utils.isString, MatrixVector = diagram.MatrixVector, g = kendo.geometry, d = kendo.drawing, defined = kendo.util.defined, inArray = $.inArray; var TRANSPARENT = 'transparent', Markers = { none: 'none', arrowStart: 'ArrowStart', filledCircle: 'FilledCircle', arrowEnd: 'ArrowEnd' }, FULL_CIRCLE_ANGLE = 360, START = 'start', END = 'end', WIDTH = 'width', HEIGHT = 'height', X = 'x', Y = 'y'; diagram.Markers = Markers; function diffNumericOptions(options, fields) { var elementOptions = this.options; var hasChanges = false; var value, field; for (var i = 0; i < fields.length; i++) { field = fields[i]; value = options[field]; if (isNumber(value) && elementOptions[field] !== value) { elementOptions[field] = value; hasChanges = true; } } return hasChanges; } var Scale = Class.extend({ init: function (x, y) { this.x = x; this.y = y; }, toMatrix: function () { return Matrix.scaling(this.x, this.y); }, toString: function () { return kendo.format('scale({0},{1})', this.x, this.y); }, invert: function () { return new Scale(1 / this.x, 1 / this.y); } }); var Translation = Class.extend({ init: function (x, y) { this.x = x; this.y = y; }, toMatrixVector: function () { return new MatrixVector(0, 0, 0, 0, this.x, this.y); }, toMatrix: function () { return Matrix.translation(this.x, this.y); }, toString: function () { return kendo.format('translate({0},{1})', this.x, this.y); }, plus: function (delta) { this.x += delta.x; this.y += delta.y; }, times: function (factor) { this.x *= factor; this.y *= factor; }, length: function () { return Math.sqrt(this.x * this.x + this.y * this.y); }, normalize: function () { if (this.Length === 0) { return; } this.times(1 / this.length()); }, invert: function () { return new Translation(-this.x, -this.y); } }); var Rotation = Class.extend({ init: function (angle, x, y) { this.x = x || 0; this.y = y || 0; this.angle = angle; }, toString: function () { if (this.x && this.y) { return kendo.format('rotate({0},{1},{2})', this.angle, this.x, this.y); } else { return kendo.format('rotate({0})', this.angle); } }, toMatrix: function () { return Matrix.rotation(this.angle, this.x, this.y); }, center: function () { return new Point(this.x, this.y); }, invert: function () { return new Rotation(FULL_CIRCLE_ANGLE - this.angle, this.x, this.y); } }); Rotation.ZERO = new Rotation(0); Rotation.create = function (rotation) { return new Rotation(rotation.angle, rotation.x, rotation.y); }; Rotation.parse = function (str) { var values = str.slice(1, str.length - 1).split(','), angle = values[0], x = values[1], y = values[2]; var rotation = new Rotation(angle, x, y); return rotation; }; var CompositeTransform = Class.extend({ init: function (x, y, scaleX, scaleY, angle, center) { this.translate = new Translation(x, y); if (scaleX !== undefined && scaleY !== undefined) { this.scale = new Scale(scaleX, scaleY); } if (angle !== undefined) { this.rotate = center ? new Rotation(angle, center.x, center.y) : new Rotation(angle); } }, toString: function () { var toString = function (transform) { return transform ? transform.toString() : ''; }; return toString(this.translate) + toString(this.rotate) + toString(this.scale); }, render: function (visual) { visual._transform = this; visual._renderTransform(); }, toMatrix: function () { var m = Matrix.unit(); if (this.translate) { m = m.times(this.translate.toMatrix()); } if (this.rotate) { m = m.times(this.rotate.toMatrix()); } if (this.scale) { m = m.times(this.scale.toMatrix()); } return m; }, invert: function () { var rotate = this.rotate ? this.rotate.invert() : undefined, rotateMatrix = rotate ? rotate.toMatrix() : Matrix.unit(), scale = this.scale ? this.scale.invert() : undefined, scaleMatrix = scale ? scale.toMatrix() : Matrix.unit(); var translatePoint = new Point(-this.translate.x, -this.translate.y); translatePoint = rotateMatrix.times(scaleMatrix).apply(translatePoint); var translate = new Translation(translatePoint.x, translatePoint.y); var transform = new CompositeTransform(); transform.translate = translate; transform.rotate = rotate; transform.scale = scale; return transform; } }); var AutoSizeableMixin = { _setScale: function () { var options = this.options; var originWidth = this._originWidth; var originHeight = this._originHeight; var scaleX = options.width / originWidth; var scaleY = options.height / originHeight; if (!isNumber(scaleX)) { scaleX = 1; } if (!isNumber(scaleY)) { scaleY = 1; } this._transform.scale = new Scale(scaleX, scaleY); }, _setTranslate: function () { var options = this.options; var x = options.x || 0; var y = options.y || 0; this._transform.translate = new Translation(x, y); }, _initSize: function () { var options = this.options; var transform = false; if (options.autoSize !== false && (defined(options.width) || defined(options.height))) { this._measure(true); this._setScale(); transform = true; } if (defined(options.x) || defined(options.y)) { this._setTranslate(); transform = true; } if (transform) { this._renderTransform(); } }, _updateSize: function (options) { var update = false; if (this.options.autoSize !== false && this._diffNumericOptions(options, [ WIDTH, HEIGHT ])) { update = true; this._measure(true); this._setScale(); } if (this._diffNumericOptions(options, [ X, Y ])) { update = true; this._setTranslate(); } if (update) { this._renderTransform(); } return update; } }; var Element = Class.extend({ init: function (options) { var element = this; element.options = deepExtend({}, element.options, options); element.id = element.options.id; element._originSize = Rect.empty(); element._transform = new CompositeTransform(); }, visible: function (value) { return this.drawingContainer().visible(value); }, redraw: function (options) { if (options && options.id) { this.id = options.id; } }, position: function (x, y) { var options = this.options; if (!defined(x)) { return new Point(options.x, options.y); } if (defined(y)) { options.x = x; options.y = y; } else if (x instanceof Point) { options.x = x.x; options.y = x.y; } this._transform.translate = new Translation(options.x, options.y); this._renderTransform(); }, rotate: function (angle, center) { if (defined(angle)) { this._transform.rotate = new Rotation(angle, center.x, center.y); this._renderTransform(); } return this._transform.rotate || Rotation.ZERO; }, drawingContainer: function () { return this.drawingElement; }, _renderTransform: function () { var matrix = this._transform.toMatrix(); this.drawingContainer().transform(new g.Matrix(matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f)); }, _hover: function () { }, _diffNumericOptions: diffNumericOptions, _measure: function (force) { var rect; if (!this._measured || force) { var box = this._boundingBox() || new g.Rect(); var startPoint = box.topLeft(); rect = new Rect(startPoint.x, startPoint.y, box.width(), box.height()); this._originSize = rect; this._originWidth = rect.width; this._originHeight = rect.height; this._measured = true; } else { rect = this._originSize; } return rect; }, _boundingBox: function () { return this.drawingElement.rawBBox(); } }); var VisualBase = Element.extend({ init: function (options) { Element.fn.init.call(this, options); options = this.options; options.fill = normalizeDrawingOptions(options.fill); options.stroke = normalizeDrawingOptions(options.stroke); }, options: { stroke: { color: 'gray', width: 1 }, fill: { color: TRANSPARENT } }, fill: function (color, opacity) { this._fill({ color: getColor(color), opacity: opacity }); }, stroke: function (color, width, opacity) { this._stroke({ color: getColor(color), width: width, opacity: opacity }); }, redraw: function (options) { if (options) { var stroke = options.stroke; var fill = options.fill; if (stroke) { this._stroke(normalizeDrawingOptions(stroke)); } if (fill) { this._fill(normalizeDrawingOptions(fill)); } Element.fn.redraw.call(this, options); } }, _hover: function (show) { var drawingElement = this.drawingElement; var options = this.options; var hover = options.hover; if (hover && hover.fill) { var fill = show ? normalizeDrawingOptions(hover.fill) : options.fill; drawingElement.fill(fill.color, fill.opacity); } }, _stroke: function (strokeOptions) { var options = this.options; deepExtend(options, { stroke: strokeOptions }); strokeOptions = options.stroke; var stroke = null; if (strokeOptions.width > 0) { stroke = { color: strokeOptions.color, width: strokeOptions.width, opacity: strokeOptions.opacity, dashType: strokeOptions.dashType }; } this.drawingElement.options.set('stroke', stroke); }, _fill: function (fillOptions) { var options = this.options; deepExtend(options, { fill: fillOptions || {} }); var fill = options.fill; if (fill.gradient) { var gradient = fill.gradient; var GradientClass = gradient.type === 'radial' ? d.RadialGradient : d.LinearGradient; this.drawingElement.fill(new GradientClass(gradient)); } else { this.drawingElement.fill(fill.color, fill.opacity); } } }); var TextBlock = VisualBase.extend({ init: function (options) { this._textColor(options); VisualBase.fn.init.call(this, options); this._font(); this._initText(); this._initSize(); }, options: { fontSize: 15, fontFamily: 'sans-serif', stroke: { width: 0 }, fill: { color: 'black' }, autoSize: true }, _initText: function () { var options = this.options; this.drawingElement = new d.Text(defined(options.text) ? options.text : '', new g.Point(), { font: options.font }); this._fill(); this._stroke(); }, _textColor: function (options) { if (options && options.color) { deepExtend(options, { fill: { color: options.color } }); } }, _font: function () { var options = this.options; if (options.fontFamily && defined(options.fontSize)) { options.font = options.fontSize + 'px ' + options.fontFamily; } else { delete options.font; } }, content: function (text) { return this.drawingElement.content(text); }, redraw: function (options) { if (options) { var sizeChanged = false; var textOptions = this.options; this._textColor(options); VisualBase.fn.redraw.call(this, options); if (options.fontFamily || defined(options.fontSize)) { deepExtend(textOptions, { fontFamily: options.fontFamily, fontSize: options.fontSize }); this._font(); this.drawingElement.options.set('font', textOptions.font); sizeChanged = true; } if (options.text) { this.content(options.text); sizeChanged = true; } if (!this._updateSize(options) && sizeChanged) { this._initSize(); } } } }); deepExtend(TextBlock.fn, AutoSizeableMixin); var Rectangle = VisualBase.extend({ init: function (options) { VisualBase.fn.init.call(this, options); this._initPath(); this._setPosition(); }, _setPosition: function () { var options = this.options; var x = options.x; var y = options.y; if (defined(x) || defined(y)) { this.position(x || 0, y || 0); } }, redraw: function (options) { if (options) { VisualBase.fn.redraw.call(this, options); if (this._diffNumericOptions(options, [ WIDTH, HEIGHT ])) { this._drawPath(); } if (this._diffNumericOptions(options, [ X, Y ])) { this._setPosition(); } } }, _initPath: function () { var options = this.options; this.drawingElement = new d.Path({ stroke: options.stroke, closed: true }); this._fill(); this._drawPath(); }, _drawPath: function () { var drawingElement = this.drawingElement; var sizeOptions = sizeOptionsOrDefault(this.options); var width = sizeOptions.width; var height = sizeOptions.height; drawingElement.segments.elements([ createSegment(0, 0), createSegment(width, 0), createSegment(width, height), createSegment(0, height) ]); } }); var MarkerBase = VisualBase.extend({ init: function (options) { VisualBase.fn.init.call(this, options); var anchor = this.options.anchor; this.anchor = new g.Point(anchor.x, anchor.y); this.createElement(); }, options: { stroke: { color: TRANSPARENT, width: 0 }, fill: { color: 'black' } }, _transformToPath: function (point, path) { var transform = path.transform(); if (point && transform) { point = point.transformCopy(transform); } return point; }, redraw: function (options) { if (options) { if (options.position) { this.options.position = options.position; } VisualBase.fn.redraw.call(this, options); } } }); var CircleMarker = MarkerBase.extend({ options: { radius: 4, anchor: { x: 0, y: 0 } }, createElement: function () { var options = this.options; this.drawingElement = new d.Circle(new g.Circle(this.anchor, options.radius), { fill: options.fill, stroke: options.stroke }); }, positionMarker: function (path) { var options = this.options; var position = options.position; var segments = path.segments; var targetSegment; var point; if (position == START) { targetSegment = segments[0]; } else { targetSegment = segments[segments.length - 1]; } if (targetSegment) { point = this._transformToPath(targetSegment.anchor(), path); this.drawingElement.transform(g.transform().translate(point.x, point.y)); } } }); var ArrowMarker = MarkerBase.extend({ options: { path: 'M 0 0 L 10 5 L 0 10 L 3 5 z', anchor: { x: 10, y: 5 } }, createElement: function () { var options = this.options; this.drawingElement = d.Path.parse(options.path, { fill: options.fill, stroke: options.stroke }); }, positionMarker: function (path) { var points = this._linePoints(path); var start = points.start; var end = points.end; var transform = g.transform(); if (start) { transform.rotate(lineAngle(start, end), end); } if (end) { var anchor = this.anchor; var translate = end.clone().translate(-anchor.x, -anchor.y); transform.translate(translate.x, translate.y); } this.drawingElement.transform(transform); }, _linePoints: function (path) { var options = this.options; var segments = path.segments; var startPoint, endPoint, targetSegment; if (options.position == START) { targetSegment = segments[0]; if (targetSegment) { endPoint = targetSegment.anchor(); startPoint = targetSegment.controlOut(); var nextSegment = segments[1]; if (!startPoint && nextSegment) { startPoint = nextSegment.anchor(); } } } else { targetSegment = segments[segments.length - 1]; if (targetSegment) { endPoint = targetSegment.anchor(); startPoint = targetSegment.controlIn(); var prevSegment = segments[segments.length - 2]; if (!startPoint && prevSegment) { startPoint = prevSegment.anchor(); } } } if (endPoint) { return { start: this._transformToPath(startPoint, path), end: this._transformToPath(endPoint, path) }; } } }); var MarkerPathMixin = { _getPath: function (position) { var path = this.drawingElement; if (path instanceof d.MultiPath) { if (position == START) { path = path.paths[0]; } else { path = path.paths[path.paths.length - 1]; } } if (path && path.segments.length) { return path; } }, _normalizeMarkerOptions: function (options) { var startCap = options.startCap; var endCap = options.endCap; if (isString(startCap)) { options.startCap = { type: startCap }; } if (isString(endCap)) { options.endCap = { type: endCap }; } }, _removeMarker: function (position) { var marker = this._markers[position]; if (marker) { this.drawingContainer().remove(marker.drawingElement); delete this._markers[position]; } }, _createMarkers: function () { var options = this.options; this._normalizeMarkerOptions(options); this._markers = {}; this._markers[START] = this._createMarker(options.startCap, START); this._markers[END] = this._createMarker(options.endCap, END); }, _createMarker: function (options, position) { var type = (options || {}).type; var path = this._getPath(position); var markerType, marker; if (!path) { this._removeMarker(position); return; } if (type == Markers.filledCircle) { markerType = CircleMarker; } else if (type == Markers.arrowStart || type == Markers.arrowEnd) { markerType = ArrowMarker; } else { this._removeMarker(position); } if (markerType) { marker = new markerType(deepExtend({}, options, { position: position })); marker.positionMarker(path); this.drawingContainer().append(marker.drawingElement); return marker; } }, _positionMarker: function (position) { var marker = this._markers[position]; if (marker) { var path = this._getPath(position); if (path) { marker.positionMarker(path); } else { this._removeMarker(position); } } }, _capMap: { start: 'startCap', end: 'endCap' }, _redrawMarker: function (pathChange, position, options) { this._normalizeMarkerOptions(options); var pathOptions = this.options; var cap = this._capMap[position]; var pathCapType = (pathOptions[cap] || {}).type; var optionsCap = options[cap]; var created = false; if (optionsCap) { pathOptions[cap] = deepExtend({}, pathOptions[cap], optionsCap); if (optionsCap.type && pathCapType != optionsCap.type) { this._removeMarker(position); this._markers[position] = this._createMarker(pathOptions[cap], position); created = true; } else if (this._markers[position]) { this._markers[position].redraw(optionsCap); } } else if (pathChange && !this._markers[position] && pathOptions[cap]) { this._markers[position] = this._createMarker(pathOptions[cap], position); created = true; } return created; }, _redrawMarkers: function (pathChange, options) { if (!this._redrawMarker(pathChange, START, options) && pathChange) { this._positionMarker(START); } if (!this._redrawMarker(pathChange, END, options) && pathChange) { this._positionMarker(END); } } }; var Path = VisualBase.extend({ init: function (options) { VisualBase.fn.init.call(this, options); this.container = new d.Group(); this._createElements(); this._initSize(); }, options: { autoSize: true }, drawingContainer: function () { return this.container; }, data: function (value) { var options = this.options; if (value) { if (options.data != value) { options.data = value; this._setData(value); this._initSize(); this._redrawMarkers(true, {}); } } else { return options.data; } }, redraw: function (options) { if (options) { VisualBase.fn.redraw.call(this, options); var pathOptions = this.options; var data = options.data; if (defined(data) && pathOptions.data != data) { pathOptions.data = data; this._setData(data); if (!this._updateSize(options)) { this._initSize(); } this._redrawMarkers(true, options); } else { this._updateSize(options); this._redrawMarkers(false, options); } } }, _createElements: function () { var options = this.options; this.drawingElement = d.Path.parse(options.data || '', { stroke: options.stroke }); this._fill(); this.container.append(this.drawingElement); this._createMarkers(); }, _setData: function (data) { var drawingElement = this.drawingElement; var multipath = d.Path.parse(data || ''); var paths = multipath.paths.slice(0); multipath.paths.elements([]); drawingElement.paths.elements(paths); } }); deepExtend(Path.fn, AutoSizeableMixin); deepExtend(Path.fn, MarkerPathMixin); var Line = VisualBase.extend({ init: function (options) { VisualBase.fn.init.call(this, options); this.container = new d.Group(); this._initPath(); this._createMarkers(); }, drawingContainer: function () { return this.container; }, redraw: function (options) { if (options) { options = options || {}; var from = options.from; var to = options.to; if (from) { this.options.from = from; } if (to) { this.options.to = to; } if (from || to) { this._drawPath(); this._redrawMarkers(true, options); } else { this._redrawMarkers(false, options); } VisualBase.fn.redraw.call(this, options); } }, _initPath: function () { var options = this.options; var drawingElement = this.drawingElement = new d.Path({ stroke: options.stroke }); this._fill(); this._drawPath(); this.container.append(drawingElement); }, _drawPath: function () { var options = this.options; var drawingElement = this.drawingElement; var from = options.from || new Point(); var to = options.to || new Point(); drawingElement.segments.elements([ createSegment(from.x, from.y), createSegment(to.x, to.y) ]); } }); deepExtend(Line.fn, MarkerPathMixin); var Polyline = VisualBase.extend({ init: function (options) { VisualBase.fn.init.call(this, options); this.container = new d.Group(); this._initPath(); this._createMarkers(); }, drawingContainer: function () { return this.container; }, points: function (points) { var options = this.options; if (points) { options.points = points; this._updatePath(); } else { return options.points; } }, redraw: function (options) { if (options) { var points = options.points; VisualBase.fn.redraw.call(this, options); if (points && this._pointsDiffer(points)) { this.points(points); this._redrawMarkers(true, options); } else { this._redrawMarkers(false, options); } } }, _initPath: function () { var options = this.options; this.drawingElement = new d.Path({ stroke: options.stroke }); this._fill(); this.container.append(this.drawingElement); if (options.points) { this._updatePath(); } }, _pointsDiffer: function (points) { var currentPoints = this.options.points; var differ = currentPoints.length !== points.length; if (!differ) { for (var i = 0; i < points.length; i++) { if (currentPoints[i].x !== points[i].x || currentPoints[i].y !== points[i].y) { differ = true; break; } } } return differ; }, _updatePath: function () { var drawingElement = this.drawingElement; var options = this.options; var points = options.points; var segments = []; var point; for (var i = 0; i < points.length; i++) { point = points[i]; segments.push(createSegment(point.x, point.y)); } drawingElement.segments.elements(segments); }, options: { points: [] } }); deepExtend(Polyline.fn, MarkerPathMixin); var Image = Element.extend({ init: function (options) { Element.fn.init.call(this, options); this._initImage(); }, redraw: function (options) { if (options) { if (options.source) { this.drawingElement.src(options.source); } if (this._diffNumericOptions(options, [ WIDTH, HEIGHT, X, Y ])) { this.drawingElement.rect(this._rect()); } Element.fn.redraw.call(this, options); } }, _initImage: function () { var options = this.options; var rect = this._rect(); this.drawingElement = new d.Image(options.source, rect, {}); }, _rect: function () { var sizeOptions = sizeOptionsOrDefault(this.options); var origin = new g.Point(sizeOptions.x, sizeOptions.y); var size = new g.Size(sizeOptions.width, sizeOptions.height); return new g.Rect(origin, size); } }); var Group = Element.extend({ init: function (options) { this.children = []; Element.fn.init.call(this, options); this.drawingElement = new d.Group(); this._initSize(); }, options: { autoSize: false }, append: function (visual) { this.drawingElement.append(visual.drawingContainer()); this.children.push(visual); this._childrenChange = true; }, remove: function (visual) { if (this._remove(visual)) { this._childrenChange = true; } }, _remove: function (visual) { var index = inArray(visual, this.children); if (index >= 0) { this.drawingElement.removeAt(index); this.children.splice(index, 1); return true; } }, clear: function () { this.drawingElement.clear(); this.children = []; this._childrenChange = true; }, toFront: function (visuals) { var visual; for (var i = 0; i < visuals.length; i++) { visual = visuals[i]; if (this._remove(visual)) { this.append(visual); } } }, toBack: function (visuals) { this._reorderChildren(visuals, 0); }, toIndex: function (visuals, indices) { this._reorderChildren(visuals, indices); }, _reorderChildren: function (visuals, indices) { var group = this.drawingElement; var drawingChildren = group.children.slice(0); var children = this.children; var fixedPosition = isNumber(indices); var i, index, toIndex, drawingElement, visual; for (i = 0; i < visuals.length; i++) { visual = visuals[i]; drawingElement = visual.drawingContainer(); index = inArray(visual, children); if (index >= 0) { drawingChildren.splice(index, 1); children.splice(index, 1); toIndex = fixedPosition ? indices : indices[i]; drawingChildren.splice(toIndex, 0, drawingElement); children.splice(toIndex, 0, visual); } } group.clear(); group.append.apply(group, drawingChildren); }, redraw: function (options) { if (options) { if (this._childrenChange) { this._childrenChange = false; if (!this._updateSize(options)) { this._initSize(); } } else { this._updateSize(options); } Element.fn.redraw.call(this, options); } }, _boundingBox: function () { var children = this.children; var boundingBox; var visual, childBoundingBox; for (var i = 0; i < children.length; i++) { visual = children[i]; if (visual.visible() && visual._includeInBBox !== false) { childBoundingBox = visual.drawingContainer().clippedBBox(null); if (childBoundingBox) { if (boundingBox) { boundingBox = g.Rect.union(boundingBox, childBoundingBox); } else { boundingBox = childBoundingBox; } } } } return boundingBox; } }); deepExtend(Group.fn, AutoSizeableMixin); var Layout = Group.extend({ init: function (rect, options) { this.children = []; Element.fn.init.call(this, options); this.drawingElement = new d.Layout(toDrawingRect(rect), options); this._initSize(); }, rect: function (rect) { if (rect) { this.drawingElement.rect(toDrawingRect(rect)); } else { var drawingRect = this.drawingElement.rect(); if (drawingRect) { return new Rect(drawingRect.origin.x, drawingRect.origin.y, drawingRect.size.width, drawingRect.size.height); } } }, reflow: function () { this.drawingElement.reflow(); }, redraw: function (options) { kendo.deepExtend(this.drawingElement.options, options); Group.fn.redraw.call(this, options); } }); var Circle = VisualBase.extend({ init: function (options) { VisualBase.fn.init.call(this, options); this._initCircle(); this._initSize(); }, redraw: function (options) { if (options) { var circleOptions = this.options; if (options.center) { deepExtend(circleOptions, { center: options.center }); this._center.move(circleOptions.center.x, circleOptions.center.y); } if (this._diffNumericOptions(options, ['radius'])) { this._circle.setRadius(circleOptions.radius); } this._updateSize(options); VisualBase.fn.redraw.call(this, options); } }, _initCircle: function () { var options = this.options; var width = options.width; var height = options.height; var radius = options.radius; if (!defined(radius)) { if (!defined(width)) { width = height; } if (!defined(height)) { height = width; } options.radius = radius = Math.min(width, height) / 2; } var center = options.center || { x: radius, y: radius }; this._center = new g.Point(center.x, center.y); this._circle = new g.Circle(this._center, radius); this.drawingElement = new d.Circle(this._circle, { stroke: options.stroke }); this._fill(); } }); deepExtend(Circle.fn, AutoSizeableMixin); var Canvas = Class.extend({ init: function (element, options) { options = options || {}; this.element = element; this.surface = d.Surface.create(element, options); if (kendo.isFunction(this.surface.translate)) { this.translate = this._translate; } this.drawingElement = new d.Group(); this._viewBox = new Rect(0, 0, options.width, options.height); this.size(this._viewBox); }, bounds: function () { var box = this.drawingElement.clippedBBox(); return new Rect(0, 0, box.width(), box.height()); }, size: function (size) { var viewBox = this._viewBox; if (defined(size)) { viewBox.width = size.width; viewBox.height = size.height; this.surface.setSize(size); } return { width: viewBox.width, height: viewBox.height }; }, _translate: function (x, y) { var viewBox = this._viewBox; if (defined(x) && defined(y)) { viewBox.x = x; viewBox.y = y; this.surface.translate({ x: x, y: y }); } return { x: viewBox.x, y: viewBox.y }; }, draw: function () { this.surface.draw(this.drawingElement); }, append: function (visual) { this.drawingElement.append(visual.drawingContainer()); return this; }, remove: function (visual) { this.drawingElement.remove(visual.drawingContainer()); }, insertBefore: function () { }, clear: function () { this.drawingElement.clear(); }, destroy: function (clearHtml) { this.surface.destroy(); if (clearHtml) { $(this.element).remove(); } } }); function sizeOptionsOrDefault(options) { return { x: options.x || 0, y: options.y || 0, width: options.width || 0, height: options.height || 0 }; } function normalizeDrawingOptions(options) { if (options) { var drawingOptions = options; if (isString(drawingOptions)) { drawingOptions = { color: drawingOptions }; } if (drawingOptions.color) { drawingOptions.color = getColor(drawingOptions.color); } return drawingOptions; } } function getColor(value) { var color; if (value != TRANSPARENT) { color = new d.Color(value).toHex(); } else { color = value; } return color; } function lineAngle(p1, p2) { var xDiff = p2.x - p1.x; var yDiff = p2.y - p1.y; var angle = kendo.util.deg(Math.atan2(yDiff, xDiff)); return angle; } function createSegment(x, y) { return new d.Segment(new g.Point(x, y)); } function toDrawingRect(rect) { if (rect) { return new g.Rect([ rect.x, rect.y ], [ rect.width, rect.height ]); } } kendo.deepExtend(diagram, { init: function (element) { kendo.init(element, diagram.ui); }, diffNumericOptions: diffNumericOptions, Element: Element, Scale: Scale, Translation: Translation, Rotation: Rotation, Circle: Circle, Group: Group, Rectangle: Rectangle, Canvas: Canvas, Path: Path, Layout: Layout, Line: Line, MarkerBase: MarkerBase, ArrowMarker: ArrowMarker, CircleMarker: CircleMarker, Polyline: Polyline, CompositeTransform: CompositeTransform, TextBlock: TextBlock, Image: Image, VisualBase: VisualBase }); }(window.kendo.jQuery)); }, typeof define == 'function' && define.amd ? define : function (a1, a2, a3) { (a3 || a2)(); })); (function (f, define) { define('dataviz/diagram/services', [ 'kendo.drawing', 'dataviz/diagram/svg' ], f); }(function () { (function ($, undefined) { var kendo = window.kendo, dataviz = kendo.dataviz, diagram = dataviz.diagram, Class = kendo.Class, Group = diagram.Group, Rect = diagram.Rect, Rectangle = diagram.Rectangle, Utils = diagram.Utils, isUndefined = Utils.isUndefined, Point = diagram.Point, Circle = diagram.Circle, Ticker = diagram.Ticker, deepExtend = kendo.deepExtend, Movable = kendo.ui.Movable, browser = kendo.support.browser, defined = kendo.util.defined, inArray = $.inArray, proxy = $.proxy; var Cursors = { arrow: 'default', grip: 'pointer', cross: 'pointer', add: 'pointer', move: 'move', select: 'pointer', south: 's-resize', east: 'e-resize', west: 'w-resize', north: 'n-resize', rowresize: 'row-resize', colresize: 'col-resize' }, HIT_TEST_DISTANCE = 10, AUTO = 'Auto', TOP = 'Top', RIGHT = 'Right', LEFT = 'Left', BOTTOM = 'Bottom', DEFAULT_SNAP_SIZE = 10, DEFAULT_SNAP_ANGLE = 10, DRAG_START = 'dragStart', DRAG = 'drag', DRAG_END = 'dragEnd', ITEMROTATE = 'itemRotate', ITEMBOUNDSCHANGE = 'itemBoundsChange', MIN_SNAP_SIZE = 5, MIN_SNAP_ANGLE = 5, MOUSE_ENTER = 'mouseEnter', MOUSE_LEAVE = 'mouseLeave', ZOOM_START = 'zoomStart', ZOOM_END = 'zoomEnd', SCROLL_MIN = -20000, SCROLL_MAX = 20000, FRICTION = 0.9, FRICTION_MOBILE = 0.93, VELOCITY_MULTIPLIER = 5, TRANSPARENT = 'transparent', PAN = 'pan', ROTATED = 'rotated'; diagram.Cursors = Cursors; function selectSingle(item, meta) { if (item.isSelected) { if (meta.ctrlKey) { item.select(false); } } else { item.diagram.select(item, { addToSelection: meta.ctrlKey }); } } var PositionAdapter = kendo.Class.extend({ init: function (layoutState) { this.layoutState = layoutState; this.diagram = layoutState.diagram; }, initState: function () { this.froms = []; this.tos = []; this.subjects = []; function pusher(id, bounds) { var shape = this.diagram.getShapeById(id); if (shape) { this.subjects.push(shape); this.froms.push(shape.bounds().topLeft()); this.tos.push(bounds.topLeft()); } } this.layoutState.nodeMap.forEach(pusher, this); }, update: function (tick) { if (this.subjects.length <= 0) { return; } for (var i = 0; i < this.subjects.length; i++) { this.subjects[i].position(new Point(this.froms[i].x + (this.tos[i].x - this.froms[i].x) * tick, this.froms[i].y + (this.tos[i].y - this.froms[i].y) * tick)); } } }); var LayoutUndoUnit = Class.extend({ init: function (initialState, finalState, animate) { if (isUndefined(animate)) { this.animate = false; } else { this.animate = animate; } this._initialState = initialState; this._finalState = finalState; this.title = 'Diagram layout'; }, undo: function () { this.setState(this._initialState); }, redo: function () { this.setState(this._finalState); }, setState: function (state) { var diagram = state.diagram; if (this.animate) { state.linkMap.forEach(function (id, points) { var conn = diagram.getShapeById(id); conn.visible(false); if (conn) { conn.points(points); } }); var ticker = new Ticker(); ticker.addAdapter(new PositionAdapter(state)); ticker.onComplete(function () { state.linkMap.forEach(function (id) { var conn = diagram.getShapeById(id); conn.visible(true); }); }); ticker.play(); } else { state.nodeMap.forEach(function (id, bounds) { var shape = diagram.getShapeById(id); if (shape) { shape.position(bounds.topLeft()); } }); state.linkMap.forEach(function (id, points) { var conn = diagram.getShapeById(id); if (conn) { conn.points(points); } }); } } }); var CompositeUnit = Class.extend({ init: function (unit) { this.units = []; this.title = 'Composite unit'; if (unit !== undefined) { this.units.push(unit); } }, add: function (undoUnit) { this.units.push(undoUnit); }, undo: function () { for (var i = 0; i < this.units.length; i++) { this.units[i].undo(); } }, redo: function () { for (var i = 0; i < this.units.length; i++) { this.units[i].redo(); } } }); var ConnectionEditUnit = Class.extend({ init: function (item, redoSource, redoTarget) { this.item = item; this._redoSource = redoSource; this._redoTarget = redoTarget; if (defined(redoSource)) { this._undoSource = item.source(); } if (defined(redoTarget)) { this._undoTarget = item.target(); } this.title = 'Connection Editing'; }, undo: function () { if (this._undoSource !== undefined) { this.item._updateConnector(this._undoSource, 'source'); } if (this._undoTarget !== undefined) { this.item._updateConnector(this._undoTarget, 'target'); } this.item.updateModel(); }, redo: function () { if (this._redoSource !== undefined) { this.item._updateConnector(this._redoSource, 'source'); } if (this._redoTarget !== undefined) { this.item._updateConnector(this._redoTarget, 'target'); } this.item.updateModel(); } }); var ConnectionEditUndoUnit = Class.extend({ init: function (item, undoSource, undoTarget) { this.item = item; this._undoSource = undoSource; this._undoTarget = undoTarget; this._redoSource = item.source(); this._redoTarget = item.target(); this.title = 'Connection Editing'; }, undo: function () { this.item._updateConnector(this._undoSource, 'source'); this.item._updateConnector(this._undoTarget, 'target'); this.item.updateModel(); }, redo: function () { this.item._updateConnector(this._redoSource, 'source'); this.item._updateConnector(this._redoTarget, 'target'); this.item.updateModel(); } }); var DeleteConnectionUnit = Class.extend({ init: function (connection) { this.connection = connection; this.diagram = connection.diagram; this.targetConnector = connection.targetConnector; this.title = 'Delete connection'; }, undo: function () { this.diagram._addConnection(this.connection, false); }, redo: function () { this.diagram.remove(this.connection, false); } }); var DeleteShapeUnit = Class.extend({ init: function (shape) { this.shape = shape; this.diagram = shape.diagram; this.title = 'Deletion'; }, undo: function () { this.diagram._addShape(this.shape, false); this.shape.select(false); }, redo: function () { this.shape.select(false); this.diagram.remove(this.shape, false); } }); var TransformUnit = Class.extend({ init: function (shapes, undoStates, adorner) { this.shapes = shapes; this.undoStates = undoStates; this.title = 'Transformation'; this.redoStates = []; this.adorner = adorner; for (var i = 0; i < this.shapes.length; i++) { var shape = this.shapes[i]; this.redoStates.push(shape.bounds()); } }, undo: function () { for (var i = 0; i < this.shapes.length; i++) { var shape = this.shapes[i]; shape.bounds(this.undoStates[i]); if (shape.hasOwnProperty('layout')) { shape.layout(shape, this.redoStates[i], this.undoStates[i]); } shape.updateModel(); } if (this.adorner) { this.adorner.refreshBounds(); this.adorner.refresh(); } }, redo: function () { for (var i = 0; i < this.shapes.length; i++) { var shape = this.shapes[i]; shape.bounds(this.redoStates[i]); if (shape.hasOwnProperty('layout')) { shape.layout(shape, this.undoStates[i], this.redoStates[i]); } shape.updateModel(); } if (this.adorner) { this.adorner.refreshBounds(); this.adorner.refresh(); } } }); var AddConnectionUnit = Class.extend({ init: function (connection, diagram) { this.connection = connection; this.diagram = diagram; this.title = 'New connection'; }, undo: function () { this.diagram.remove(this.connection, false); }, redo: function () { this.diagram._addConnection(this.connection, false); } }); var AddShapeUnit = Class.extend({ init: function (shape, diagram) { this.shape = shape; this.diagram = diagram; this.title = 'New shape'; }, undo: function () { this.diagram.deselect(); this.diagram.remove(this.shape, false); }, redo: function () { this.diagram._addShape(this.shape, false); } }); var PanUndoUnit = Class.extend({ init: function (initialPosition, finalPosition, diagram) { this.initial = initialPosition; this.finalPos = finalPosition; this.diagram = diagram; this.title = 'Pan Unit'; }, undo: function () { this.diagram.pan(this.initial); }, redo: function () { this.diagram.pan(this.finalPos); } }); var RotateUnit = Class.extend({ init: function (adorner, shapes, undoRotates) { this.shapes = shapes; this.undoRotates = undoRotates; this.title = 'Rotation'; this.redoRotates = []; this.redoAngle = adorner._angle; this.adorner = adorner; this.center = adorner._innerBounds.center(); for (var i = 0; i < this.shapes.length; i++) { var shape = this.shapes[i]; this.redoRotates.push(shape.rotate().angle); } }, undo: function () { var i, shape; for (i = 0; i < this.shapes.length; i++) { shape = this.shapes[i]; shape.rotate(this.undoRotates[i], this.center, false); if (shape.hasOwnProperty('layout')) { shape.layout(shape); } shape.updateModel(); } if (this.adorner) { this.adorner._initialize(); this.adorner.refresh(); } }, redo: function () { var i, shape; for (i = 0; i < this.shapes.length; i++) { shape = this.shapes[i]; shape.rotate(this.redoRotates[i], this.center, false); if (shape.hasOwnProperty('layout')) { shape.layout(shape); } shape.updateModel(); } if (this.adorner) { this.adorner._initialize(); this.adorner.refresh(); } } }); var ToFrontUnit = Class.extend({ init: function (diagram, items, initialIndices) { this.diagram = diagram; this.indices = initialIndices; this.items = items; this.title = 'Rotate Unit'; }, undo: function () { this.diagram._toIndex(this.items, this.indices); }, redo: function () { this.diagram.toFront(this.items, false); } }); var ToBackUnit = Class.extend({ init: function (diagram, items, initialIndices) { this.diagram = diagram; this.indices = initialIndices; this.items = items; this.title = 'Rotate Unit'; }, undo: function () { this.diagram._toIndex(this.items, this.indices); }, redo: function () { this.diagram.toBack(this.items, false); } }); var UndoRedoService = kendo.Observable.extend({ init: function (options) { kendo.Observable.fn.init.call(this, options); this.bind(this.events, options); this.stack = []; this.index = 0; this.capacity = 100; }, events: [ 'undone', 'redone' ], begin: function () { this.composite = new CompositeUnit(); }, cancel: function () { this.composite = undefined; }, commit: function (execute) { if (this.composite.units.length > 0) { this._restart(this.composite, execute); } this.composite = undefined; }, addCompositeItem: function (undoUnit) { if (this.composite) { this.composite.add(undoUnit); } else { this.add(undoUnit); } }, add: function (undoUnit, execute) { this._restart(undoUnit, execute); }, pop: function () { if (this.index > 0) { this.stack.pop(); this.index--; } }, count: function () { return this.stack.length; }, undo: function () { if (this.index > 0) { this.index--; this.stack[this.index].undo(); this.trigger('undone'); } }, redo: function () { if (this.stack.length > 0 && this.index < this.stack.length) { this.stack[this.index].redo(); this.index++; this.trigger('redone'); } }, _restart: function (composite, execute) { this.stack.splice(this.index, this.stack.length - this.index); this.stack.push(composite); if (execute !== false) { this.redo(); } else { this.index++; } if (this.stack.length > this.capacity) { this.stack.splice(0, this.stack.length - this.capacity); this.index = this.capacity; } }, clear: function () { this.stack = []; this.index = 0; } }); var EmptyTool = Class.extend({ init: function (toolService) { this.toolService = toolService; }, start: function () { }, move: function () { }, end: function () { }, tryActivate: function () { return false; }, getCursor: function () { return Cursors.arrow; } }); function noMeta(meta) { return meta.ctrlKey === false && meta.altKey === false && meta.shiftKey === false; } function tryActivateSelection(options, meta) { var enabled = options !== false; if (options.key && options.key != 'none') { enabled = meta[options.key + 'Key']; } return enabled; } var ScrollerTool = EmptyTool.extend({ init: function (toolService) { var tool = this; var friction = kendo.support.mobileOS ? FRICTION_MOBILE : FRICTION; EmptyTool.fn.init.call(tool, toolService); var diagram = tool.toolService.diagram, canvas = diagram.canvas; var scroller = diagram.scroller = tool.scroller = $(diagram.scrollable).kendoMobileScroller({ friction: friction, velocityMultiplier: VELOCITY_MULTIPLIER, mousewheelScrolling: false, zoom: false, scroll: proxy(tool._move, tool) }).data('kendoMobileScroller'); if (canvas.translate) { tool.movableCanvas = new Movable(canvas.element); } var virtualScroll = function (dimension, min, max) { dimension.makeVirtual(); dimension.virtualSize(min || SCROLL_MIN, max || SCROLL_MAX); }; virtualScroll(scroller.dimensions.x); virtualScroll(scroller.dimensions.y); scroller.disable(); }, tryActivate: function (p, meta) { var toolService = this.toolService; var options = toolService.diagram.options.pannable; var enabled = meta.ctrlKey; if (defined(options.key)) { if (!options.key || options.key == 'none') { enabled = noMeta(meta); } else { enabled = meta[options.key + 'Key'] && !(meta.ctrlKey && defined(toolService.hoveredItem)); } } return options !== false && enabled && !defined(toolService.hoveredAdorner) && !defined(toolService._hoveredConnector); }, start: function () { this.scroller.enable(); }, move: function () { }, _move: function (args) { var tool = this, diagram = tool.toolService.diagram, canvas = diagram.canvas, scrollPos = new Point(args.scrollLeft, args.scrollTop); if (canvas.translate) { diagram._storePan(scrollPos.times(-1)); tool.movableCanvas.moveTo(scrollPos); canvas.translate(scrollPos.x, scrollPos.y); } else { scrollPos = scrollPos.plus(diagram._pan.times(-1)); } diagram.trigger(PAN, { pan: scrollPos }); }, end: function () { this.scroller.disable(); }, getCursor: function () { return Cursors.move; } }); var PointerTool = Class.extend({ init: function (toolService) { this.toolService = toolService; }, tryActivate: function () { return true; }, start: function (p, meta) { var toolService = this.toolService, diagram = toolService.diagram, hoveredItem = toolService.hoveredItem, selectable = diagram.options.selectable; if (hoveredItem) { if (tryActivateSelection(selectable, meta)) { selectSingle(hoveredItem, meta); } if (hoveredItem.adorner) { this.adorner = hoveredItem.adorner; this.handle = this.adorner._hitTest(p); } } if (!this.handle) { this.handle = diagram._resizingAdorner._hitTest(p); if (this.handle) { this.adorner = diagram._resizingAdorner; } } if (this.adorner) { if (!this.adorner.isDragHandle(this.handle) || !diagram.trigger(DRAG_START, { shapes: this.adorner.shapes, connections: [] })) { this.adorner.start(p); } else { toolService.startPoint = p; toolService.end(p); } } }, move: function (p) { if (this.adorner) { this.adorner.move(this.handle, p); if (this.adorner.isDragHandle(this.handle)) { this.toolService.diagram.trigger(DRAG, { shapes: this.adorner.shapes, connections: [] }); } } }, end: function (p, meta) { var diagram = this.toolService.diagram, service = this.toolService, adorner = this.adorner, unit; if (adorner) { if (!adorner.isDragHandle(this.handle) || !diagram.trigger(DRAG_END, { shapes: adorner.shapes, connections: [] })) { unit = adorner.stop(); if (unit) { diagram.undoRedoService.add(unit, false); } } else { adorner.cancel(); } } if (service.hoveredItem) { this.toolService.triggerClick({ item: service.hoveredItem, point: p, meta: meta }); } this.adorner = undefined; this.handle = undefined; }, getCursor: function (p) { return this.toolService.hoveredItem ? this.toolService.hoveredItem._getCursor(p) : Cursors.arrow; } }); var SelectionTool = Class.extend({ init: function (toolService) { this.toolService = toolService; }, tryActivate: function (p, meta) { var toolService = this.toolService; var enabled = tryActivateSelection(toolService.diagram.options.selectable, meta); return enabled && !defined(toolService.hoveredItem) && !defined(toolService.hoveredAdorner); }, start: function (p) { var diagram = this.toolService.diagram; diagram.deselect(); diagram.selector.start(p); }, move: function (p) { var diagram = this.toolService.diagram; diagram.selector.move(p); }, end: function (p, meta) { var diagram = this.toolService.diagram, hoveredItem = this.toolService.hoveredItem; var rect = diagram.selector.bounds(); if ((!hoveredItem || !hoveredItem.isSelected) && !meta.ctrlKey) { diagram.deselect(); } if (!rect.isEmpty()) { diagram.selectArea(rect); } diagram.selector.end(); }, getCursor: function () { return Cursors.arrow; } }); var ConnectionTool = Class.extend({ init: function (toolService) { this.toolService = toolService; this.type = 'ConnectionTool'; }, tryActivate: function () { return this.toolService._hoveredConnector; }, start: function (p, meta) { var diagram = this.toolService.diagram, connector = this.toolService._hoveredConnector, connection = diagram._createConnection({}, connector._c, p); if (canDrag(connection) && !diagram.trigger(DRAG_START, { shapes: [], connections: [connection] }) && diagram._addConnection(connection)) { this.toolService._connectionManipulation(connection, connector._c.shape, true); this.toolService._removeHover(); selectSingle(this.toolService.activeConnection, meta); } else { connection.source(null); this.toolService.end(p); } }, move: function (p) { var toolService = this.toolService; var connection = toolService.activeConnection; connection.target(p); toolService.diagram.trigger(DRAG, { shapes: [], connections: [connection] }); return true; }, end: function (p) { var toolService = this.toolService, d = toolService.diagram, connection = toolService.activeConnection, hoveredItem = toolService.hoveredItem, connector = toolService._hoveredConnector, target; if (!connection) { return; } if (connector && connector._c != connection.sourceConnector) { target = connector._c; } else if (hoveredItem && hoveredItem instanceof diagram.Shape) { target = hoveredItem.getConnector(AUTO) || hoveredItem.getConnector(p); } else { target = p; } connection.target(target); if (!d.trigger(DRAG_END, { shapes: [], connections: [connection] })) { connection.updateModel(); d._syncConnectionChanges(); } else { d.remove(connection, false); d.undoRedoService.pop(); } toolService._connectionManipulation(); }, getCursor: function () { return Cursors.arrow; } }); var ConnectionEditTool = Class.extend({ init: function (toolService) { this.toolService = toolService; this.type = 'ConnectionTool'; }, tryActivate: function (p, meta) { var toolService = this.toolService, diagram = toolService.diagram, selectable = diagram.options.selectable, item = toolService.hoveredItem, isActive = tryActivateSelection(selectable, meta) && item && item.path && !(item.isSelected && meta.ctrlKey); if (isActive) { this._c = item; } return isActive; }, start: function (p, meta) { var connection = this._c; selectSingle(connection, meta); var adorner = connection.adorner; if (canDrag(connection) && adorner && !this.toolService.diagram.trigger(DRAG_START, { shapes: [], connections: [connection] })) { this.handle = adorner._hitTest(p); adorner.start(p); } else { this.toolService.startPoint = p; this.toolService.end(p); } }, move: function (p) { var adorner = this._c.adorner; if (canDrag(this._c) && adorner) { adorner.move(this.handle, p); this.toolService.diagram.trigger(DRAG, { shapes: [], connections: [this._c] }); return true; } }, end: function (p, meta) { var connection = this._c; var adorner = connection.adorner; var toolService = this.toolService; var diagram = toolService.diagram; if (adorner) { toolService.triggerClick({ item: connection, point: p, meta: meta }); if (canDrag(connection)) { var unit = adorner.stop(p); if (!diagram.trigger(DRAG_END, { shapes: [], connections: [connection] })) { diagram.undoRedoService.add(unit, false); connection.updateModel(); diagram._syncConnectionChanges(); } else { unit.undo(); } } } }, getCursor: function () { return Cursors.move; } }); function testKey(key, str) { return str.charCodeAt(0) == key || str.toUpperCase().charCodeAt(0) == key; } var ToolService = Class.extend({ init: function (diagram) { this.diagram = diagram; this.tools = [ new ScrollerTool(this), new ConnectionEditTool(this), new ConnectionTool(this), new SelectionTool(this), new PointerTool(this) ]; this.activeTool = undefined; }, start: function (p, meta) { meta = deepExtend({}, meta); if (this.activeTool) { this.activeTool.end(p, meta); } this._updateHoveredItem(p); this._activateTool(p, meta); this.activeTool.start(p, meta); this._updateCursor(p); this.diagram.focus(); this.startPoint = p; return true; }, move: function (p, meta) { meta = deepExtend({}, meta); var updateHovered = true; if (this.activeTool) { updateHovered = this.activeTool.move(p, meta); } if (updateHovered) { this._updateHoveredItem(p); } this._updateCursor(p); return true; }, end: function (p, meta) { meta = deepExtend({}, meta); if (this.activeTool) { this.activeTool.end(p, meta); } this.activeTool = undefined; this._updateCursor(p); return true; }, keyDown: function (key, meta) { var diagram = this.diagram; meta = deepExtend({ ctrlKey: false, metaKey: false, altKey: false }, meta); if ((meta.ctrlKey || meta.metaKey) && !meta.altKey) { if (testKey(key, 'a')) { diagram.selectAll(); diagram._destroyToolBar(); return true; } else if (testKey(key, 'z')) { diagram.undo(); diagram._destroyToolBar(); return true; } else if (testKey(key, 'y')) { diagram.redo(); diagram._destroyToolBar(); return true; } else if (testKey(key, 'c')) { diagram.copy(); diagram._destroyToolBar(); } else if (testKey(key, 'x')) { diagram.cut(); diagram._destroyToolBar(); } else if (testKey(key, 'v')) { diagram.paste(); diagram._destroyToolBar(); } else if (testKey(key, 'l')) { diagram.layout(); diagram._destroyToolBar(); } else if (testKey(key, 'd')) { diagram._destroyToolBar(); diagram.copy(); diagram.paste(); } } else if (key === 46 || key === 8) { var toRemove = this.diagram._triggerRemove(diagram.select()); if (toRemove.length) { this.diagram.remove(toRemove, true); this.diagram._syncChanges(); this.diagram._destroyToolBar(); } return true; } else if (key === 27) { this._discardNewConnection(); diagram.deselect(); diagram._destroyToolBar(); return true; } }, wheel: function (p, meta) { var diagram = this.diagram, delta = meta.delta, z = diagram.zoom(), options = diagram.options, zoomRate = options.zoomRate, zoomOptions = { point: p, meta: meta, zoom: z }; if (diagram.trigger(ZOOM_START, zoomOptions)) { return; } if (delta < 0) { z += zoomRate; } else { z -= zoomRate; } z = kendo.dataviz.round(Math.max(options.zoomMin, Math.min(options.zoomMax, z)), 2); zoomOptions.zoom = z; diagram.zoom(z, zoomOptions); diagram.trigger(ZOOM_END, zoomOptions); return true; }, setTool: function (tool, index) { tool.toolService = this; this.tools[index] = tool; }, triggerClick: function (data) { if (this.startPoint.equals(data.point)) { this.diagram.trigger('click', data); } }, _discardNewConnection: function () { if (this.newConnection) { this.diagram.remove(this.newConnection); this.newConnection = undefined; } }, _activateTool: function (p, meta) { for (var i = 0; i < this.tools.length; i++) { var tool = this.tools[i]; if (tool.tryActivate(p, meta)) { this.activeTool = tool; break; } } }, _updateCursor: function (p) { var element = this.diagram.element; var cursor = this.activeTool ? this.activeTool.getCursor(p) : this.hoveredAdorner ? this.hoveredAdorner._getCursor(p) : this.hoveredItem ? this.hoveredItem._getCursor(p) : Cursors.arrow; element.css({ cursor: cursor }); if (browser.msie && browser.version == 7) { element[0].style.cssText = element[0].style.cssText; } }, _connectionManipulation: function (connection, disabledShape, isNew) { this.activeConnection = connection; this.disabledShape = disabledShape; if (isNew) { this.newConnection = this.activeConnection; } else { this.newConnection = undefined; } }, _updateHoveredItem: function (p) { var hit = this._hitTest(p); var diagram = this.diagram; if (hit != this.hoveredItem && (!this.disabledShape || hit != this.disabledShape)) { if (this.hoveredItem) { diagram.trigger(MOUSE_LEAVE, { item: this.hoveredItem }); this.hoveredItem._hover(false); } if (hit && hit.options.enable) { diagram.trigger(MOUSE_ENTER, { item: hit }); this.hoveredItem = hit; this.hoveredItem._hover(true); } else { this.hoveredItem = undefined; } } }, _removeHover: function () { if (this.hoveredItem) { this.hoveredItem._hover(false); this.hoveredItem = undefined; } }, _hitTest: function (point) { var hit, d = this.diagram, item, i; if (this._hoveredConnector) { this._hoveredConnector._hover(false); this._hoveredConnector = undefined; } if (d._connectorsAdorner._visible) { hit = d._connectorsAdorner._hitTest(point); if (hit) { return hit; } } hit = this.diagram._resizingAdorner._hitTest(point); if (hit) { this.hoveredAdorner = d._resizingAdorner; if (hit.x !== 0 || hit.y !== 0) { return; } hit = undefined; } else { this.hoveredAdorner = undefined; } if (!this.activeTool || this.activeTool.type !== 'ConnectionTool') { var selectedConnections = []; for (i = 0; i < d._selectedItems.length; i++) { item = d._selectedItems[i]; if (item instanceof diagram.Connection) { selectedConnections.push(item); } } hit = this._hitTestItems(selectedConnections, point); } return hit || this._hitTestElements(point); }, _hitTestElements: function (point) { var diagram = this.diagram; var shapeHit = this._hitTestItems(diagram.shapes, point); var connectionHit = this._hitTestItems(diagram.connections, point); var hit; if ((!this.activeTool || this.activeTool.type != 'ConnectionTool') && shapeHit && connectionHit && !hitTestShapeConnectors(shapeHit, point)) { var mainLayer = diagram.mainLayer; var shapeIdx = inArray(shapeHit.visual, mainLayer.children); var connectionIdx = inArray(connectionHit.visual, mainLayer.children); hit = shapeIdx > connectionIdx ? shapeHit : connectionHit; } return hit || shapeHit || connectionHit; }, _hitTestItems: function (array, point) { var i, item, hit; for (i = array.length - 1; i >= 0; i--) { item = array[i]; hit = item._hitTest(point); if (hit) { return hit; } } } }); var ConnectionRouterBase = kendo.Class.extend({ init: function () { } }); var LinearConnectionRouter = ConnectionRouterBase.extend({ init: function (connection) { var that = this; ConnectionRouterBase.fn.init.call(that); this.connection = connection; }, hitTest: function (p) { var rec = this.getBounds().inflate(HIT_TEST_DISTANCE); if (!rec.contains(p)) { return false; } return diagram.Geometry.distanceToPolyline(p, this.connection.allPoints()) < HIT_TEST_DISTANCE; }, getBounds: function () { var points = this.connection.allPoints(), s = points[0], e = points[points.length - 1], right = Math.max(s.x, e.x), left = Math.min(s.x, e.x), top = Math.min(s.y, e.y), bottom = Math.max(s.y, e.y); for (var i = 1; i < points.length - 1; ++i) { right = Math.max(right, points[i].x); left = Math.min(left, points[i].x); top = Math.min(top, points[i].y); bottom = Math.max(bottom, points[i].y); } return new Rect(left, top, right - left, bottom - top); } }); var PolylineRouter = LinearConnectionRouter.extend({ init: function (connection) { var that = this; LinearConnectionRouter.fn.init.call(that); this.connection = connection; }, route: function () { } }); var CascadingRouter = LinearConnectionRouter.extend({ SAME_SIDE_DISTANCE_RATIO: 5, init: function (connection) { var that = this; LinearConnectionRouter.fn.init.call(that); this.connection = connection; }, routePoints: function (start, end, sourceConnector, targetConnector) { var result; if (sourceConnector && targetConnector) { result = this._connectorPoints(start, end, sourceConnector, targetConnector); } else { result = this._floatingPoints(start, end, sourceConnector); } return result; }, route: function () { var sourceConnector = this.connection._resolvedSourceConnector; var targetConnector = this.connection._resolvedTargetConnector; var start = this.connection.sourcePoint(); var end = this.connection.targetPoint(); var points = this.routePoints(start, end, sourceConnector, targetConnector); this.connection.points(points); }, _connectorSides: [ { name: 'Top', axis: 'y', boundsPoint: 'topLeft', secondarySign: 1 }, { name: 'Left', axis: 'x', boundsPoint: 'topLeft', secondarySign: 1 }, { name: 'Bottom', axis: 'y', boundsPoint: 'bottomRight', secondarySign: -1 }, { name: 'Right', axis: 'x', boundsPoint: 'bottomRight', secondarySign: -1 } ], _connectorSide: function (connector, targetPoint) { var position = connector.position(); var shapeBounds = connector.shape.bounds(ROTATED); var bounds = { topLeft: shapeBounds.topLeft(), bottomRight: shapeBounds.bottomRight() }; var sides = this._connectorSides; var min = kendo.util.MAX_NUM; var sideDistance; var minSide; var axis; var side; for (var idx = 0; idx < sides.length; idx++) { side = sides[idx]; axis = side.axis; sideDistance = Math.round(Math.abs(position[axis] - bounds[side.boundsPoint][axis])); if (sideDistance < min) { min = sideDistance; minSide = side; } else if (sideDistance === min && (position[axis] - targetPoint[axis]) * side.secondarySign > (position[minSide.axis] - targetPoint[minSide.axis]) * minSide.secondarySign) { minSide = side; } } return minSide.name; }, _sameSideDistance: function (connector) { var bounds = connector.shape.bounds(ROTATED); return Math.min(bounds.width, bounds.height) / this.SAME_SIDE_DISTANCE_RATIO; }, _connectorPoints: function (start, end, sourceConnector, targetConnector) { var sourceConnectorSide = this._connectorSide(sourceConnector, end); var targetConnectorSide = this._connectorSide(targetConnector, start); var deltaX = end.x - start.x; var deltaY = end.y - start.y; var sameSideDistance = this._sameSideDistance(sourceConnector); var result = []; var pointX, pointY; if (sourceConnectorSide === TOP || sourceConnectorSide == BOTTOM) { if (targetConnectorSide == TOP || targetConnectorSide == BOTTOM) { if (sourceConnectorSide == targetConnectorSide) { if (sourceConnectorSide == TOP) { pointY = Math.min(start.y, end.y) - sameSideDistance; } else { pointY = Math.max(start.y, end.y) + sameSideDistance; } result = [ new Point(start.x, pointY), new Point(end.x, pointY) ]; } else { result = [ new Point(start.x, start.y + deltaY / 2), new Point(end.x, start.y + deltaY / 2) ]; } } else { result = [new Point(start.x, end.y)]; } } else { if (targetConnectorSide == LEFT || targetConnectorSide == RIGHT) { if (sourceConnectorSide == targetConnectorSide) { if (sourceConnectorSide == LEFT) { pointX = Math.min(start.x, end.x) - sameSideDistance; } else { pointX = Math.max(start.x, end.x) + sameSideDistance; } result = [ new Point(pointX, start.y), new Point(pointX, end.y) ]; } else { result = [ new Point(start.x + deltaX / 2, start.y), new Point(start.x + deltaX / 2, start.y + deltaY) ]; } } else { result = [new Point(end.x, start.y)]; } } return result; }, _floatingPoints: function (start, end, sourceConnector) { var sourceConnectorSide = sourceConnector ? this._connectorSide(sourceConnector, end) : null; var cascadeStartHorizontal = this._startHorizontal(start, end, sourceConnectorSide); var points = [ start, start, end, end ]; var deltaX = end.x - start.x; var deltaY = end.y - start.y; var length = points.length; var shiftX; var shiftY; for (var idx = 1; idx < length - 1; ++idx) { if (cascadeStartHorizontal) { if (idx % 2 !== 0) { shiftX = deltaX / (length / 2); shiftY = 0; } else { shiftX = 0; shiftY = deltaY / ((length - 1) / 2); } } else { if (idx % 2 !== 0) { shiftX = 0; shiftY = deltaY / (length / 2); } else { shiftX = deltaX / ((length - 1) / 2); shiftY = 0; } } points[idx] = new Point(points[idx - 1].x + shiftX, points[idx - 1].y + shiftY); } idx--; if (cascadeStartHorizontal && idx % 2 !== 0 || !cascadeStartHorizontal && idx % 2 === 0) { points[length - 2] = new Point(points[length - 1].x, points[length - 2].y); } else { points[length - 2] = new Point(points[length - 2].x, points[length - 1].y); } return [ points[1], points[2] ]; }, _startHorizontal: function (start, end, sourceSide) { var horizontal; if (sourceSide !== null && (sourceSide === RIGHT || sourceSide === LEFT)) { horizontal = true; } else { horizontal = Math.abs(start.x - end.x) > Math.abs(start.y - end.y); } return horizontal; } }); var AdornerBase = Class.extend({ init: function (diagram, options) { var that = this; that.diagram = diagram; that.options = deepExtend({}, that.options, options); that.visual = new Group(); that.diagram._adorners.push(that); }, refresh: function () { } }); var ConnectionEditAdorner = AdornerBase.extend({ init: function (connection, options) { var that = this, diagram; that.connection = connection; diagram = that.connection.diagram; that._ts = diagram.toolService; AdornerBase.fn.init.call(that, diagram, options); var sp = that.connection.sourcePoint(); var tp = that.connection.targetPoint(); that.spVisual = new Circle(deepExtend(that.options.handles, { center: sp })); that.epVisual = new Circle(deepExtend(that.options.handles, { center: tp })); that.visual.append(that.spVisual); that.visual.append(that.epVisual); }, options: { handles: {} }, _getCursor: function () { return Cursors.move; }, start: function (p) { this.handle = this._hitTest(p); this.startPoint = p; this._initialSource = this.connection.source(); this._initialTarget = this.connection.target(); switch (this.handle) { case -1: if (this.connection.targetConnector) { this._ts._connectionManipulation(this.connection, this.connection.targetConnector.shape); } break; case 1: if (this.connection.sourceConnector) { this._ts._connectionManipulation(this.connection, this.connection.sourceConnector.shape); } break; } }, move: function (handle, p) { switch (handle) { case -1: this.connection.source(p); break; case 1: this.connection.target(p); break; default: var delta = p.minus(this.startPoint); this.startPoint = p; if (!this.connection.sourceConnector) { this.connection.source(this.connection.sourcePoint().plus(delta)); } if (!this.connection.targetConnector) { this.connection.target(this.connection.targetPoint().plus(delta)); } break; } this.refresh(); return true; }, stop: function (p) { var ts = this.diagram.toolService, item = ts.hoveredItem, target; if (ts._hoveredConnector) { target = ts._hoveredConnector._c; } else if (item && item instanceof diagram.Shape) { target = item.getConnector(AUTO) || item.getConnector(p); } else { target = p; } if (this.handle === -1) { this.connection.source(target); } else if (this.handle === 1) { this.connection.target(target); } this.handle = undefined; this._ts._connectionManipulation(); return new ConnectionEditUndoUnit(this.connection, this._initialSource, this._initialTarget); }, _hitTest: function (p) { var sp = this.connection.sourcePoint(), tp = this.connection.targetPoint(), rx = this.options.handles.width / 2, ry = this.options.handles.height / 2, sb = new Rect(sp.x, sp.y).inflate(rx, ry), tb = new Rect(tp.x, tp.y).inflate(rx, ry); return sb.contains(p) ? -1 : tb.contains(p) ? 1 : 0; }, refresh: function () { this.spVisual.redraw({ center: this.diagram.modelToLayer(this.connection.sourcePoint()) }); this.epVisual.redraw({ center: this.diagram.modelToLayer(this.connection.targetPoint()) }); } }); var ConnectorsAdorner = AdornerBase.extend({ init: function (diagram, options) { var that = this; AdornerBase.fn.init.call(that, diagram, options); that._refreshHandler = function (e) { if (e.item == that.shape) { that.refresh(); } }; }, show: function (shape) { var that = this, len, i, ctr; that._visible = true; that.shape = shape; that.diagram.bind(ITEMBOUNDSCHANGE, that._refreshHandler); len = shape.connectors.length; that.connectors = []; that.visual.clear(); for (i = 0; i < len; i++) { ctr = new ConnectorVisual(shape.connectors[i]); that.connectors.push(ctr); that.visual.append(ctr.visual); } that.visual.visible(true); that.refresh(); }, destroy: function () { var that = this; that.diagram.unbind(ITEMBOUNDSCHANGE, that._refreshHandler); that.shape = undefined; that._visible = undefined; that.visual.visible(false); }, _hitTest: function (p) { var ctr, i; for (i = 0; i < this.connectors.length; i++) { ctr = this.connectors[i]; if (ctr._hitTest(p)) { ctr._hover(true); this.diagram.toolService._hoveredConnector = ctr; break; } } }, refresh: function () { if (this.shape) { var bounds = this.shape.bounds(); bounds = this.diagram.modelToLayer(bounds); this.visual.position(bounds.topLeft()); $.each(this.connectors, function () { this.refresh(); }); } } }); function hitToOppositeSide(hit, bounds) { var result; if (hit.x == -1 && hit.y == -1) { result = bounds.bottomRight(); } else if (hit.x == 1 && hit.y == 1) { result = bounds.topLeft(); } else if (hit.x == -1 && hit.y == 1) { result = bounds.topRight(); } else if (hit.x == 1 && hit.y == -1) { result = bounds.bottomLeft(); } else if (hit.x === 0 && hit.y == -1) { result = bounds.bottom(); } else if (hit.x === 0 && hit.y == 1) { result = bounds.top(); } else if (hit.x == 1 && hit.y === 0) { result = bounds.left(); } else if (hit.x == -1 && hit.y === 0) { result = bounds.right(); } return result; } var ResizingAdorner = AdornerBase.extend({ init: function (diagram, options) { var that = this; AdornerBase.fn.init.call(that, diagram, options); that._manipulating = false; that.map = []; that.shapes = []; that._initSelection(); that._createHandles(); that.redraw(); that.diagram.bind('select', function (e) { that._initialize(e.selected); }); that._refreshHandler = function () { if (!that._internalChange) { that.refreshBounds(); that.refresh(); } }; that._rotatedHandler = function () { if (that.shapes.length == 1) { that._angle = that.shapes[0].rotate().angle; } that._refreshHandler(); }; that.diagram.bind(ITEMBOUNDSCHANGE, that._refreshHandler).bind(ITEMROTATE, that._rotatedHandler); that.refreshBounds(); that.refresh(); }, options: { handles: { fill: { color: '#fff' }, stroke: { color: '#282828' }, height: 7, width: 7, hover: { fill: { color: '#282828' }, stroke: { color: '#282828' } } }, selectable: { stroke: { color: '#778899', width: 1, dashType: 'dash' }, fill: { color: TRANSPARENT } }, offset: 10 }, _initSelection: function () { var that = this; var diagram = that.diagram; var selectable = diagram.options.selectable; var options = deepExtend({}, that.options.selectable, selectable); that.rect = new Rectangle(options); that.visual.append(that.rect); }, _resizable: function () { return this.options.editable && this.options.editable.resize !== false; }, _handleOptions: function () { return (this.options.editable.resize || {}).handles || this.options.handles; }, _createHandles: function () { var handles, item, y, x; if (this._resizable()) { handles = this._handleOptions(); for (x = -1; x <= 1; x++) { for (y = -1; y <= 1; y++) { if (x !== 0 || y !== 0) { item = new Rectangle(handles); item.drawingElement._hover = proxy(this._hover, this); this.map.push({ x: x, y: y, visual: item }); this.visual.append(item); } } } } }, bounds: function (value) { if (value) { this._innerBounds = value.clone(); this._bounds = this.diagram.modelToLayer(value).inflate(this.options.offset, this.options.offset); } else { return this._bounds; } }, _hitTest: function (p) { var tp = this.diagram.modelToLayer(p), i, hit, handleBounds, handlesCount = this.map.length, handle; if (this._angle) { tp = tp.clone().rotate(this._bounds.center(), this._angle); } if (this._resizable()) { for (i = 0; i < handlesCount; i++) { handle = this.map[i]; hit = new Point(handle.x, handle.y); handleBounds = this._getHandleBounds(hit); handleBounds.offset(this._bounds.x, this._bounds.y); if (handleBounds.contains(tp)) { return hit; } } } if (this._bounds.contains(tp)) { return new Point(0, 0); } }, _getHandleBounds: function (p) { if (this._resizable()) { var handles = this._handleOptions(), w = handles.width, h = handles.height, r = new Rect(0, 0, w, h); if (p.x < 0) { r.x = -w / 2; } else if (p.x === 0) { r.x = Math.floor(this._bounds.width / 2) - w / 2; } else if (p.x > 0) { r.x = this._bounds.width + 1 - w / 2; } if (p.y < 0) { r.y = -h / 2; } else if (p.y === 0) { r.y = Math.floor(this._bounds.height / 2) - h / 2; } else if (p.y > 0) { r.y = this._bounds.height + 1 - h / 2; } return r; } }, _getCursor: function (point) { var hit = this._hitTest(point); if (hit && hit.x >= -1 && hit.x <= 1 && hit.y >= -1 && hit.y <= 1 && this._resizable()) { var angle = this._angle; if (angle) { angle = 360 - angle; hit.rotate(new Point(0, 0), angle); hit = new Point(Math.round(hit.x), Math.round(hit.y)); } if (hit.x == -1 && hit.y == -1) { return 'nw-resize'; } if (hit.x == 1 && hit.y == 1) { return 'se-resize'; } if (hit.x == -1 && hit.y == 1) { return 'sw-resize'; } if (hit.x == 1 && hit.y == -1) { return 'ne-resize'; } if (hit.x === 0 && hit.y == -1) { return 'n-resize'; } if (hit.x === 0 && hit.y == 1) { return 's-resize'; } if (hit.x == 1 && hit.y === 0) { return 'e-resize'; } if (hit.x == -1 && hit.y === 0) { return 'w-resize'; } } return this._manipulating ? Cursors.move : Cursors.select; }, _initialize: function () { var that = this, i, item, items = that.diagram.select(); that.shapes = []; for (i = 0; i < items.length; i++) { item = items[i]; if (item instanceof diagram.Shape) { that.shapes.push(item); item._rotationOffset = new Point(); } } that._angle = that.shapes.length == 1 ? that.shapes[0].rotate().angle : 0; that._startAngle = that._angle; that._rotates(); that._positions(); that.refreshBounds(); that.refresh(); that.redraw(); }, _rotates: function () { var that = this, i, shape; that.initialRotates = []; for (i = 0; i < that.shapes.length; i++) { shape = that.shapes[i]; that.initialRotates.push(shape.rotate().angle); } }, _positions: function () { var that = this, i, shape; that.initialStates = []; for (i = 0; i < that.shapes.length; i++) { shape = that.shapes[i]; that.initialStates.push(shape.bounds()); } }, _hover: function (value, element) { if (this._resizable()) { var handleOptions = this._handleOptions(), hover = handleOptions.hover, stroke = handleOptions.stroke, fill = handleOptions.fill; if (value && Utils.isDefined(hover.stroke)) { stroke = deepExtend({}, stroke, hover.stroke); } if (value && Utils.isDefined(hover.fill)) { fill = hover.fill; } element.stroke(stroke.color, stroke.width, stroke.opacity); element.fill(fill.color, fill.opacity); } }, start: function (p) { this._sp = p; this._cp = p; this._lp = p; this._manipulating = true; this._internalChange = true; this.shapeStates = []; for (var i = 0; i < this.shapes.length; i++) { var shape = this.shapes[i]; this.shapeStates.push(shape.bounds()); } }, redraw: function () { var i, handle, visibleHandles = this._resizable(); for (i = 0; i < this.map.length; i++) { handle = this.map[i]; handle.visual.visible(visibleHandles); } }, angle: function (value) { if (defined(value)) { this._angle = value; } return this._angle; }, rotate: function () { var center = this._innerBounds.center(); var currentAngle = this.angle(); this._internalChange = true; for (var i = 0; i < this.shapes.length; i++) { var shape = this.shapes[i]; currentAngle = (currentAngle + this.initialRotates[i] - this._startAngle) % 360; shape.rotate(currentAngle, center); } this.refresh(); }, move: function (handle, p) { var delta, dragging, dtl = new Point(), dbr = new Point(), bounds, center, shape, i, angle, newBounds, changed = 0, staticPoint, scaleX, scaleY; if (handle.y === -2 && handle.x === -1) { center = this._innerBounds.center(); this._angle = this._truncateAngle(Utils.findAngle(center, p)); for (i = 0; i < this.shapes.length; i++) { shape = this.shapes[i]; angle = (this._angle + this.initialRotates[i] - this._startAngle) % 360; shape.rotate(angle, center); if (shape.hasOwnProperty('layout')) { shape.layout(shape); } this._rotating = true; } this.refresh(); } else { if (this.shouldSnap()) { var thr = this._truncateDistance(p.minus(this._lp)); if (thr.x === 0 && thr.y === 0) { this._cp = p; return; } delta = thr; this._lp = new Point(this._lp.x + thr.x, this._lp.y + thr.y); } else { delta = p.minus(this._cp); } if (this.isDragHandle(handle)) { dbr = dtl = delta; dragging = true; } else { if (this._angle) { delta.rotate(new Point(0, 0), this._angle); } if (handle.x == -1) { dtl.x = delta.x; } else if (handle.x == 1) { dbr.x = delta.x; } if (handle.y == -1) { dtl.y = delta.y; } else if (handle.y == 1) { dbr.y = delta.y; } } if (!dragging) { staticPoint = hitToOppositeSide(handle, this._innerBounds); scaleX = (this._innerBounds.width + delta.x * handle.x) / this._innerBounds.width; scaleY = (this._innerBounds.height + delta.y * handle.y) / this._innerBounds.height; } for (i = 0; i < this.shapes.length; i++) { shape = this.shapes[i]; bounds = shape.bounds(); if (dragging) { if (!canDrag(shape)) { continue; } newBounds = this._displaceBounds(bounds, dtl, dbr, dragging); } else { newBounds = bounds.clone(); newBounds.scale(scaleX, scaleY, staticPoint, this._innerBounds.center(), shape.rotate().angle); var newCenter = newBounds.center(); newCenter.rotate(bounds.center(), -this._angle); newBounds = new Rect(newCenter.x - newBounds.width / 2, newCenter.y - newBounds.height / 2, newBounds.width, newBounds.height); } if (newBounds.width >= shape.options.minWidth && newBounds.height >= shape.options.minHeight) { var oldBounds = bounds; shape.bounds(newBounds); if (shape.hasOwnProperty('layout')) { shape.layout(shape, oldBounds, newBounds); } if (oldBounds.width !== newBounds.width || oldBounds.height !== newBounds.height) { shape.rotate(shape.rotate().angle); } changed += 1; } } if (changed) { if (changed == i) { newBounds = this._displaceBounds(this._innerBounds, dtl, dbr, dragging); this.bounds(newBounds); } else { this.refreshBounds(); } this.refresh(); } this._positions(); } this._cp = p; }, isDragHandle: function (handle) { return handle.x === 0 && handle.y === 0; }, cancel: function () { var shapes = this.shapes; var states = this.shapeStates; for (var idx = 0; idx < shapes.length; idx++) { shapes[idx].bounds(states[idx]); } this.refreshBounds(); this.refresh(); this._manipulating = undefined; this._internalChange = undefined; this._rotating = undefined; }, _truncatePositionToGuides: function (bounds) { if (this.diagram.ruler) { return this.diagram.ruler.truncatePositionToGuides(bounds); } return bounds; }, _truncateSizeToGuides: function (bounds) { if (this.diagram.ruler) { return this.diagram.ruler.truncateSizeToGuides(bounds); } return bounds; }, _truncateAngle: function (a) { var snap = this.snapOptions(); var snapAngle = Math.max(snap.angle || DEFAULT_SNAP_ANGLE, MIN_SNAP_ANGLE); return snap ? Math.floor(a % 360 / snapAngle) * snapAngle : a % 360; }, _truncateDistance: function (d) { if (d instanceof diagram.Point) { return new diagram.Point(this._truncateDistance(d.x), this._truncateDistance(d.y)); } else { var snap = this.snapOptions() || {}; var snapSize = Math.max(snap.size || DEFAULT_SNAP_SIZE, MIN_SNAP_SIZE); return snap ? Math.floor(d / snapSize) * snapSize : d; } }, snapOptions: function () { var editable = this.diagram.options.editable; var snap = ((editable || {}).drag || {}).snap || {}; return snap; }, shouldSnap: function () { var editable = this.diagram.options.editable; var drag = (editable || {}).drag; var snap = (drag || {}).snap; return editable !== false && drag !== false && snap !== false; }, _displaceBounds: function (bounds, dtl, dbr, dragging) { var tl = bounds.topLeft().plus(dtl), br = bounds.bottomRight().plus(dbr), newBounds = Rect.fromPoints(tl, br), newCenter; if (!dragging) { newCenter = newBounds.center(); newCenter.rotate(bounds.center(), -this._angle); newBounds = new Rect(newCenter.x - newBounds.width / 2, newCenter.y - newBounds.height / 2, newBounds.width, newBounds.height); } return newBounds; }, stop: function () { var unit, i, shape; if (this._cp != this._sp) { if (this._rotating) { unit = new RotateUnit(this, this.shapes, this.initialRotates); this._rotating = false; } else if (this._diffStates()) { if (this.diagram.ruler) { for (i = 0; i < this.shapes.length; i++) { shape = this.shapes[i]; var bounds = shape.bounds(); bounds = this._truncateSizeToGuides(this._truncatePositionToGuides(bounds)); shape.bounds(bounds); this.refreshBounds(); this.refresh(); } } for (i = 0; i < this.shapes.length; i++) { shape = this.shapes[i]; shape.updateModel(); } unit = new TransformUnit(this.shapes, this.shapeStates, this); this.diagram._syncShapeChanges(); } } this._manipulating = undefined; this._internalChange = undefined; this._rotating = undefined; return unit; }, _diffStates: function () { var shapes = this.shapes; var states = this.shapeStates; for (var idx = 0; idx < shapes.length; idx++) { if (!shapes[idx].bounds().equals(states[idx])) { return true; } } return false; }, refreshBounds: function () { var bounds = this.shapes.length == 1 ? this.shapes[0].bounds().clone() : this.diagram.boundingBox(this.shapes, true); this.bounds(bounds); }, refresh: function () { var that = this, b, bounds; if (this.shapes.length > 0) { bounds = this.bounds(); this.visual.visible(true); this.visual.position(bounds.topLeft()); $.each(this.map, function () { b = that._getHandleBounds(new Point(this.x, this.y)); this.visual.position(b.topLeft()); }); this.visual.position(bounds.topLeft()); var center = new Point(bounds.width / 2, bounds.height / 2); this.visual.rotate(this._angle, center); this.rect.redraw({ width: bounds.width, height: bounds.height }); if (this.rotationThumb) { var thumb = this.options.editable.rotate.thumb; this._rotationThumbBounds = new Rect(bounds.center().x, bounds.y + thumb.y, 0, 0).inflate(thumb.width); this.rotationThumb.redraw({ x: bounds.width / 2 - thumb.width / 2 }); } } else { this.visual.visible(false); } } }); var Selector = Class.extend({ init: function (diagram) { var selectable = diagram.options.selectable; this.options = deepExtend({}, this.options, selectable); this.visual = new Rectangle(this.options); this.diagram = diagram; }, options: { stroke: { color: '#778899', width: 1, dashType: 'dash' }, fill: { color: TRANSPARENT } }, start: function (p) { this._sp = this._ep = p; this.refresh(); this.diagram._adorn(this, true); }, end: function () { this._sp = this._ep = undefined; this.diagram._adorn(this, false); }, bounds: function (value) { if (value) { this._bounds = value; } return this._bounds; }, move: function (p) { this._ep = p; this.refresh(); }, refresh: function () { if (this._sp) { var visualBounds = Rect.fromPoints(this.diagram.modelToLayer(this._sp), this.diagram.modelToLayer(this._ep)); this.bounds(Rect.fromPoints(this._sp, this._ep)); this.visual.position(visualBounds.topLeft()); this.visual.redraw({ height: visualBounds.height + 1, width: visualBounds.width + 1 }); } } }); var ConnectorVisual = Class.extend({ init: function (connector) { this.options = deepExtend({}, connector.options); this._c = connector; this.visual = new Circle(this.options); this.refresh(); }, _hover: function (value) { var options = this.options, hover = options.hover, stroke = options.stroke, fill = options.fill; if (value && Utils.isDefined(hover.stroke)) { stroke = deepExtend({}, stroke, hover.stroke); } if (value && Utils.isDefined(hover.fill)) { fill = hover.fill; } this.visual.redraw({ stroke: stroke, fill: fill }); }, refresh: function () { var p = this._c.shape.diagram.modelToView(this._c.position()), relative = p.minus(this._c.shape.bounds('transformed').topLeft()), value = new Rect(p.x, p.y, 0, 0); value.inflate(this.options.width / 2, this.options.height / 2); this._visualBounds = value; this.visual.redraw({ center: new Point(relative.x, relative.y) }); }, _hitTest: function (p) { var tp = this._c.shape.diagram.modelToView(p); return this._visualBounds.contains(tp); } }); function canDrag(element) { var editable = element.options.editable; return editable && editable.drag !== false; } function hitTestShapeConnectors(shape, point) { var connector, position, rect; for (var idx = 0; idx < shape.connectors.length; idx++) { connector = shape.connectors[idx]; position = connector.position(); rect = new Rect(position.x, position.y); rect.inflate(HIT_TEST_DISTANCE, HIT_TEST_DISTANCE); if (rect.contains(point)) { return connector; } } } deepExtend(diagram, { CompositeUnit: CompositeUnit, TransformUnit: TransformUnit, PanUndoUnit: PanUndoUnit, AddShapeUnit: AddShapeUnit, AddConnectionUnit: AddConnectionUnit, DeleteShapeUnit: DeleteShapeUnit, DeleteConnectionUnit: DeleteConnectionUnit, ConnectionEditAdorner: ConnectionEditAdorner, ConnectionTool: ConnectionTool, ConnectorVisual: ConnectorVisual, UndoRedoService: UndoRedoService, ResizingAdorner: ResizingAdorner, Selector: Selector, ToolService: ToolService, ConnectorsAdorner: ConnectorsAdorner, LayoutUndoUnit: LayoutUndoUnit, ConnectionEditUnit: ConnectionEditUnit, ToFrontUnit: ToFrontUnit, ToBackUnit: ToBackUnit, ConnectionRouterBase: ConnectionRouterBase, PolylineRouter: PolylineRouter, CascadingRouter: CascadingRouter, SelectionTool: SelectionTool, ScrollerTool: ScrollerTool, PointerTool: PointerTool, ConnectionEditTool: ConnectionEditTool, RotateUnit: RotateUnit }); }(window.kendo.jQuery)); }, typeof define == 'function' && define.amd ? define : function (a1, a2, a3) { (a3 || a2)(); })); (function (f, define) { define('dataviz/diagram/layout', ['dataviz/diagram/math'], f); }(function () { (function ($, undefined) { var kendo = window.kendo, diagram = kendo.dataviz.diagram, Graph = diagram.Graph, Node = diagram.Node, Link = diagram.Link, deepExtend = kendo.deepExtend, Size = diagram.Size, Rect = diagram.Rect, Dictionary = diagram.Dictionary, Set = diagram.Set, HyperTree = diagram.Graph, Utils = diagram.Utils, Point = diagram.Point, EPSILON = 0.000001, DEG_TO_RAD = Math.PI / 180, contains = Utils.contains, grep = $.grep; var LayoutBase = kendo.Class.extend({ defaultOptions: { type: 'Tree', subtype: 'Down', roots: null, animate: false, limitToView: false, friction: 0.9, nodeDistance: 50, iterations: 300, horizontalSeparation: 90, verticalSeparation: 50, underneathVerticalTopOffset: 15, underneathHorizontalOffset: 15, underneathVerticalSeparation: 15, grid: { width: 1500, offsetX: 50, offsetY: 50, componentSpacingX: 20, componentSpacingY: 20 }, layerSeparation: 50, layeredIterations: 2, startRadialAngle: 0, endRadialAngle: 360, radialSeparation: 150, radialFirstLevelSeparation: 200, keepComponentsInOneRadialLayout: false, ignoreContainers: true, layoutContainerChildren: false, ignoreInvisible: true, animateTransitions: false }, init: function () { }, gridLayoutComponents: function (components) { if (!components) { throw 'No components supplied.'; } Utils.forEach(components, function (c) { c.calcBounds(); }); components.sort(function (a, b) { return b.bounds.width - a.bounds.width; }); var maxWidth = this.options.grid.width, offsetX = this.options.grid.componentSpacingX, offsetY = this.options.grid.componentSpacingY, height = 0, startX = this.options.grid.offsetX, startY = this.options.grid.offsetY, x = startX, y = startY, i, resultLinkSet = [], resultNodeSet = []; while (components.length > 0) { if (x >= maxWidth) { x = startX; y += height + offsetY; height = 0; } var component = components.pop(); this.moveToOffset(component, new Point(x, y)); for (i = 0; i < component.nodes.length; i++) { resultNodeSet.push(component.nodes[i]); } for (i = 0; i < component.links.length; i++) { resultLinkSet.push(component.links[i]); } var boundingRect = component.bounds; var currentHeight = boundingRect.height; if (currentHeight <= 0 || isNaN(currentHeight)) { currentHeight = 0; } var currentWidth = boundingRect.width; if (currentWidth <= 0 || isNaN(currentWidth)) { currentWidth = 0; } if (currentHeight >= height) { height = currentHeight; } x += currentWidth + offsetX; } return { nodes: resultNodeSet, links: resultLinkSet }; }, moveToOffset: function (component, p) { var i, j, bounds = component.bounds, deltax = p.x - bounds.x, deltay = p.y - bounds.y; for (i = 0; i < component.nodes.length; i++) { var node = component.nodes[i]; var nodeBounds = node.bounds(); if (nodeBounds.width === 0 && nodeBounds.height === 0 && nodeBounds.x === 0 && nodeBounds.y === 0) { nodeBounds = new Rect(0, 0, 0, 0); } nodeBounds.x += deltax; nodeBounds.y += deltay; node.bounds(nodeBounds); } for (i = 0; i < component.links.length; i++) { var link = component.links[i]; if (link.points) { var newpoints = []; var points = link.points; for (j = 0; j < points.length; j++) { var pt = points[j]; pt.x += deltax; pt.y += deltay; newpoints.push(pt); } link.points = newpoints; } } this.currentHorizontalOffset += bounds.width + this.options.grid.offsetX; return new Point(deltax, deltay); }, transferOptions: function (options) { this.options = kendo.deepExtend({}, this.defaultOptions); if (Utils.isUndefined(options)) { return; } this.options = kendo.deepExtend(this.options, options || {}); } }); var DiagramToHyperTreeAdapter = kendo.Class.extend({ init: function (diagram) { this.nodeMap = new Dictionary(); this.shapeMap = new Dictionary(); this.nodes = []; this.edges = []; this.edgeMap = new Dictionary(); this.finalNodes = []; this.finalLinks = []; this.ignoredConnections = []; this.ignoredShapes = []; this.hyperMap = new Dictionary(); this.hyperTree = new Graph(); this.finalGraph = null; this.diagram = diagram; }, convert: function (options) { if (Utils.isUndefined(this.diagram)) { throw 'No diagram to convert.'; } this.options = kendo.deepExtend({ ignoreInvisible: true, ignoreContainers: true, layoutContainerChildren: false }, options || {}); this.clear(); this._renormalizeShapes(); this._renormalizeConnections(); this.finalNodes = new Dictionary(this.nodes); this.finalLinks = new Dictionary(this.edges); this.finalGraph = new Graph(); this.finalNodes.forEach(function (n) { this.finalGraph.addNode(n); }, this); this.finalLinks.forEach(function (l) { this.finalGraph.addExistingLink(l); }, this); return this.finalGraph; }, mapConnection: function (connection) { return this.edgeMap.get(connection.id); }, mapShape: function (shape) { return this.nodeMap.get(shape.id); }, getEdge: function (a, b) { return Utils.first(a.links, function (link) { return link.getComplement(a) === b; }); }, clear: function () { this.finalGraph = null; this.hyperTree = !this.options.ignoreContainers && this.options.layoutContainerChildren ? new HyperTree() : null; this.hyperMap = !this.options.ignoreContainers && this.options.layoutContainerChildren ? new Dictionary() : null; this.nodeMap = new Dictionary(); this.shapeMap = new Dictionary(); this.nodes = []; this.edges = []; this.edgeMap = new Dictionary(); this.ignoredConnections = []; this.ignoredShapes = []; this.finalNodes = []; this.finalLinks = []; }, listToRoot: function (containerGraph) { var list = []; var s = containerGraph.container; if (!s) { return list; } list.push(s); while (s.parentContainer) { s = s.parentContainer; list.push(s); } list.reverse(); return list; }, firstNonIgnorableContainer: function (shape) { if (shape.isContainer && !this._isIgnorableItem(shape)) { return shape; } return !shape.parentContainer ? null : this.firstNonIgnorableContainer(shape.parentContainer); }, isContainerConnection: function (a, b) { if (a.isContainer && this.isDescendantOf(a, b)) { return true; } return b.isContainer && this.isDescendantOf(b, a); }, isDescendantOf: function (scope, a) { if (!scope.isContainer) { throw 'Expecting a container.'; } if (scope === a) { return false; } if (contains(scope.children, a)) { return true; } var containers = []; for (var i = 0, len = scope.children.length; i < len; i++) { var c = scope.children[i]; if (c.isContainer && this.isDescendantOf(c, a)) { containers.push(c); } } return containers.length > 0; }, isIgnorableItem: function (shape) { if (this.options.ignoreInvisible) { if (shape.isCollapsed && this._isVisible(shape)) { return false; } if (!shape.isCollapsed && this._isVisible(shape)) { return false; } return true; } else { return shape.isCollapsed && !this._isTop(shape); } }, isShapeMapped: function (shape) { return shape.isCollapsed && !this._isVisible(shape) && !this._isTop(shape); }, leastCommonAncestor: function (a, b) { if (!a) { throw 'Parameter should not be null.'; } if (!b) { throw 'Parameter should not be null.'; } if (!this.hyperTree) { throw 'No hypertree available.'; } var al = this.listToRoot(a); var bl = this.listToRoot(b); var found = null; if (Utils.isEmpty(al) || Utils.isEmpty(bl)) { return this.hyperTree.root.data; } var xa = al[0]; var xb = bl[0]; var i = 0; while (xa === xb) { found = al[i]; i++; if (i >= al.length || i >= bl.length) { break; } xa = al[i]; xb = bl[i]; } if (!found) { return this.hyperTree.root.data; } else { return grep(this.hyperTree.nodes, function (n) { return n.data.container === found; }); } }, _isTop: function (item) { return !item.parentContainer; }, _isVisible: function (shape) { if (!shape.visible()) { return false; } return !shape.parentContainer ? shape.visible() : this._isVisible(shape.parentContainer); }, _isCollapsed: function (shape) { if (shape.isContainer && shape.isCollapsed) { return true; } return shape.parentContainer && this._isCollapsed(shape.parentContainer); }, _renormalizeShapes: function () { if (this.options.ignoreContainers) { for (var i = 0, len = this.diagram.shapes.length; i < len; i++) { var shape = this.diagram.shapes[i]; if (this.options.ignoreInvisible && !this._isVisible(shape) || shape.isContainer) { this.ignoredShapes.push(shape); continue; } var node = new Node(shape.id, shape); node.isVirtual = false; this.nodeMap.add(shape.id, node); this.nodes.push(node); } } else { throw 'Containers are not supported yet, but stay tuned.'; } }, _renormalizeConnections: function () { if (this.diagram.connections.length === 0) { return; } for (var i = 0, len = this.diagram.connections.length; i < len; i++) { var conn = this.diagram.connections[i]; if (this.isIgnorableItem(conn)) { this.ignoredConnections.push(conn); continue; } var source = !conn.sourceConnector ? null : conn.sourceConnector.shape; var sink = !conn.targetConnector ? null : conn.targetConnector.shape; if (!source || !sink) { this.ignoredConnections.push(conn); continue; } if (contains(this.ignoredShapes, source) && !this.shapeMap.containsKey(source)) { this.ignoredConnections.push(conn); continue; } if (contains(this.ignoredShapes, sink) && !this.shapeMap.containsKey(sink)) { this.ignoredConnections.push(conn); continue; } if (this.shapeMap.containsKey(source)) { source = this.shapeMap[source]; } if (this.shapeMap.containsKey(sink)) { sink = this.shapeMap[sink]; } var sourceNode = this.mapShape(source); var sinkNode = this.mapShape(sink); if (sourceNode === sinkNode || this.areConnectedAlready(sourceNode, sinkNode)) { this.ignoredConnections.push(conn); continue; } if (sourceNode === null || sinkNode === null) { throw 'A shape was not mapped to a node.'; } if (this.options.ignoreContainers) { if (sourceNode.isVirtual || sinkNode.isVirtual) { this.ignoredConnections.push(conn); continue; } var newEdge = new Link(sourceNode, sinkNode, conn.id, conn); this.edgeMap.add(conn.id, newEdge); this.edges.push(newEdge); } else { throw 'Containers are not supported yet, but stay tuned.'; } } }, areConnectedAlready: function (n, m) { return Utils.any(this.edges, function (l) { return l.source === n && l.target === m || l.source === m && l.target === n; }); } }); var SpringLayout = LayoutBase.extend({ init: function (diagram) { var that = this; LayoutBase.fn.init.call(that); if (Utils.isUndefined(diagram)) { throw 'Diagram is not specified.'; } this.diagram = diagram; }, layout: function (options) { this.transferOptions(options); var adapter = new DiagramToHyperTreeAdapter(this.diagram); var graph = adapter.convert(options); if (graph.isEmpty()) { return; } var components = graph.getConnectedComponents(); if (Utils.isEmpty(components)) { return; } for (var i = 0; i < components.length; i++) { var component = components[i]; this.layoutGraph(component, options); } var finalNodeSet = this.gridLayoutComponents(components); return new diagram.LayoutState(this.diagram, finalNodeSet); }, layoutGraph: function (graph, options) { if (Utils.isDefined(options)) { this.transferOptions(options); } this.graph = graph; var initialTemperature = this.options.nodeDistance * 9; this.temperature = initialTemperature; var guessBounds = this._expectedBounds(); this.width = guessBounds.width; this.height = guessBounds.height; for (var step = 0; step < this.options.iterations; step++) { this.refineStage = step >= this.options.iterations * 5 / 6; this.tick(); this.temperature = this.refineStage ? initialTemperature / 30 : initialTemperature * (1 - step / (2 * this.options.iterations)); } }, tick: function () { var i; for (i = 0; i < this.graph.nodes.length; i++) { this._repulsion(this.graph.nodes[i]); } for (i = 0; i < this.graph.links.length; i++) { this._attraction(this.graph.links[i]); } for (i = 0; i < this.graph.nodes.length; i++) { var node = this.graph.nodes[i]; var offset = Math.sqrt(node.dx * node.dx + node.dy * node.dy); if (offset === 0) { return; } node.x += Math.min(offset, this.temperature) * node.dx / offset; node.y += Math.min(offset, this.temperature) * node.dy / offset; if (this.options.limitToView) { node.x = Math.min(this.width, Math.max(node.width / 2, node.x)); node.y = Math.min(this.height, Math.max(node.height / 2, node.y)); } } }, _shake: function (node) { var rho = Math.random() * this.options.nodeDistance / 4; var alpha = Math.random() * 2 * Math.PI; node.x += rho * Math.cos(alpha); node.y -= rho * Math.sin(alpha); }, _InverseSquareForce: function (d, n, m) { var force; if (!this.refineStage) { force = Math.pow(d, 2) / Math.pow(this.options.nodeDistance, 2); } else { var deltax = n.x - m.x; var deltay = n.y - m.y; var wn = n.width / 2; var hn = n.height / 2; var wm = m.width / 2; var hm = m.height / 2; force = Math.pow(deltax, 2) / Math.pow(wn + wm + this.options.nodeDistance, 2) + Math.pow(deltay, 2) / Math.pow(hn + hm + this.options.nodeDistance, 2); } return force * 4 / 3; }, _SquareForce: function (d, n, m) { return 1 / this._InverseSquareForce(d, n, m); }, _repulsion: function (n) { n.dx = 0; n.dy = 0; Utils.forEach(this.graph.nodes, function (m) { if (m === n) { return; } while (n.x === m.x && n.y === m.y) { this._shake(m); } var vx = n.x - m.x; var vy = n.y - m.y; var distance = Math.sqrt(vx * vx + vy * vy); var r = this._SquareForce(distance, n, m) * 2; n.dx += vx / distance * r; n.dy += vy / distance * r; }, this); }, _attraction: function (link) { var t = link.target; var s = link.source; if (s === t) { return; } while (s.x === t.x && s.y === t.y) { this._shake(t); } var vx = s.x - t.x; var vy = s.y - t.y; var distance = Math.sqrt(vx * vx + vy * vy); var a = this._InverseSquareForce(distance, s, t) * 5; var dx = vx / distance * a; var dy = vy / distance * a; t.dx += dx; t.dy += dy; s.dx -= dx; s.dy -= dy; }, _expectedBounds: function () { var size, N = this.graph.nodes.length, ratio = 1.5, multiplier = 4; if (N === 0) { return size; } size = Utils.fold(this.graph.nodes, function (s, node) { var area = node.width * node.height; if (area > 0) { s += Math.sqrt(area); return s; } return 0; }, 0, this); var av = size / N; var squareSize = av * Math.ceil(Math.sqrt(N)); var width = squareSize * Math.sqrt(ratio); var height = squareSize / Math.sqrt(ratio); return { width: width * multiplier, height: height * multiplier }; } }); var TreeLayoutProcessor = kendo.Class.extend({ init: function (options) { this.center = null; this.options = options; }, layout: function (treeGraph, root) { this.graph = treeGraph; if (!this.graph.nodes || this.graph.nodes.length === 0) { return; } if (!contains(this.graph.nodes, root)) { throw 'The given root is not in the graph.'; } this.center = root; this.graph.cacheRelationships(); this.layoutSwitch(); }, layoutLeft: function (left) { this.setChildrenDirection(this.center, 'Left', false); this.setChildrenLayout(this.center, 'Default', false); var h = 0, w = 0, y, i, node; for (i = 0; i < left.length; i++) { node = left[i]; node.TreeDirection = 'Left'; var s = this.measure(node, Size.Empty); w = Math.max(w, s.Width); h += s.height + this.options.verticalSeparation; } h -= this.options.verticalSeparation; var x = this.center.x - this.options.horizontalSeparation; y = this.center.y + (this.center.height - h) / 2; for (i = 0; i < left.length; i++) { node = left[i]; var p = new Point(x - node.Size.width, y); this.arrange(node, p); y += node.Size.height + this.options.verticalSeparation; } }, layoutRight: function (right) { this.setChildrenDirection(this.center, 'Right', false); this.setChildrenLayout(this.center, 'Default', false); var h = 0, w = 0, y, i, node; for (i = 0; i < right.length; i++) { node = right[i]; node.TreeDirection = 'Right'; var s = this.measure(node, Size.Empty); w = Math.max(w, s.Width); h += s.height + this.options.verticalSeparation; } h -= this.options.verticalSeparation; var x = this.center.x + this.options.horizontalSeparation + this.center.width; y = this.center.y + (this.center.height - h) / 2; for (i = 0; i < right.length; i++) { node = right[i]; var p = new Point(x, y); this.arrange(node, p); y += node.Size.height + this.options.verticalSeparation; } }, layoutUp: function (up) { this.setChildrenDirection(this.center, 'Up', false); this.setChildrenLayout(this.center, 'Default', false); var w = 0, y, node, i; for (i = 0; i < up.length; i++) { node = up[i]; node.TreeDirection = 'Up'; var s = this.measure(node, Size.Empty); w += s.width + this.options.horizontalSeparation; } w -= this.options.horizontalSeparation; var x = this.center.x + this.center.width / 2 - w / 2; for (i = 0; i < up.length; i++) { node = up[i]; y = this.center.y - this.options.verticalSeparation - node.Size.height; var p = new Point(x, y); this.arrange(node, p); x += node.Size.width + this.options.horizontalSeparation; } }, layoutDown: function (down) { var node, i; this.setChildrenDirection(this.center, 'Down', false); this.setChildrenLayout(this.center, 'Default', false); var w = 0, y; for (i = 0; i < down.length; i++) { node = down[i]; node.treeDirection = 'Down'; var s = this.measure(node, Size.Empty); w += s.width + this.options.horizontalSeparation; } w -= this.options.horizontalSeparation; var x = this.center.x + this.center.width / 2 - w / 2; y = this.center.y + this.options.verticalSeparation + this.center.height; for (i = 0; i < down.length; i++) { node = down[i]; var p = new Point(x, y); this.arrange(node, p); x += node.Size.width + this.options.horizontalSeparation; } }, layoutRadialTree: function () { this.setChildrenDirection(this.center, 'Radial', false); this.setChildrenLayout(this.center, 'Default', false); this.previousRoot = null; var startAngle = this.options.startRadialAngle * DEG_TO_RAD; var endAngle = this.options.endRadialAngle * DEG_TO_RAD; if (endAngle <= startAngle) { throw 'Final angle should not be less than the start angle.'; } this.maxDepth = 0; this.origin = new Point(this.center.x, this.center.y); this.calculateAngularWidth(this.center, 0); if (this.maxDepth > 0) { this.radialLayout(this.center, this.options.radialFirstLevelSeparation, startAngle, endAngle); } this.center.Angle = endAngle - startAngle; }, tipOverTree: function (down, startFromLevel) { if (Utils.isUndefined(startFromLevel)) { startFromLevel = 0; } this.setChildrenDirection(this.center, 'Down', false); this.setChildrenLayout(this.center, 'Default', false); this.setChildrenLayout(this.center, 'Underneath', false, startFromLevel); var w = 0, y, node, i; for (i = 0; i < down.length; i++) { node = down[i]; node.TreeDirection = 'Down'; var s = this.measure(node, Size.Empty); w += s.width + this.options.horizontalSeparation; } w -= this.options.horizontalSeparation; w -= down[down.length - 1].width; w += down[down.length - 1].associatedShape.bounds().width; var x = this.center.x + this.center.width / 2 - w / 2; y = this.center.y + this.options.verticalSeparation + this.center.height; for (i = 0; i < down.length; i++) { node = down[i]; var p = new Point(x, y); this.arrange(node, p); x += node.Size.width + this.options.horizontalSeparation; } }, calculateAngularWidth: function (n, d) { if (d > this.maxDepth) { this.maxDepth = d; } var aw = 0, w = 1000, h = 1000, diameter = d === 0 ? 0 : Math.sqrt(w * w + h * h) / d; if (n.children.length > 0) { for (var i = 0, len = n.children.length; i < len; i++) { var child = n.children[i]; aw += this.calculateAngularWidth(child, d + 1); } aw = Math.max(diameter, aw); } else { aw = diameter; } n.sectorAngle = aw; return aw; }, sortChildren: function (n) { var basevalue = 0, i; if (n.parents.length > 1) { throw 'Node is not part of a tree.'; } var p = n.parents[0]; if (p) { var pl = new Point(p.x, p.y); var nl = new Point(n.x, n.y); basevalue = this.normalizeAngle(Math.atan2(pl.y - nl.y, pl.x - nl.x)); } var count = n.children.length; if (count === 0) { return null; } var angle = []; var idx = []; for (i = 0; i < count; ++i) { var c = n.children[i]; var l = new Point(c.x, c.y); idx[i] = i; angle[i] = this.normalizeAngle(-basevalue + Math.atan2(l.y - l.y, l.x - l.x)); } Utils.bisort(angle, idx); var col = []; var children = n.children; for (i = 0; i < count; ++i) { col.push(children[idx[i]]); } return col; }, normalizeAngle: function (angle) { while (angle > Math.PI * 2) { angle -= 2 * Math.PI; } while (angle < 0) { angle += Math.PI * 2; } return angle; }, radialLayout: function (node, radius, startAngle, endAngle) { var deltaTheta = endAngle - startAngle; var deltaThetaHalf = deltaTheta / 2; var parentSector = node.sectorAngle; var fraction = 0; var sorted = this.sortChildren(node); for (var i = 0, len = sorted.length; i < len; i++) { var childNode = sorted[i]; var cp = childNode; var childAngleFraction = cp.sectorAngle / parentSector; if (childNode.children.length > 0) { this.radialLayout(childNode, radius + this.options.radialSeparation, startAngle + fraction * deltaTheta, startAngle + (fraction + childAngleFraction) * deltaTheta); } this.setPolarLocation(childNode, radius, startAngle + fraction * deltaTheta + childAngleFraction * deltaThetaHalf); cp.angle = childAngleFraction * deltaTheta; fraction += childAngleFraction; } }, setPolarLocation: function (node, radius, angle) { node.x = this.origin.x + radius * Math.cos(angle); node.y = this.origin.y + radius * Math.sin(angle); node.BoundingRectangle = new Rect(node.x, node.y, node.width, node.height); }, setChildrenDirection: function (node, direction, includeStart) { var rootDirection = node.treeDirection; this.graph.depthFirstTraversal(node, function (n) { n.treeDirection = direction; }); if (!includeStart) { node.treeDirection = rootDirection; } }, setChildrenLayout: function (node, layout, includeStart, startFromLevel) { if (Utils.isUndefined(startFromLevel)) { startFromLevel = 0; } var rootLayout = node.childrenLayout; if (startFromLevel > 0) { this.graph.assignLevels(node); this.graph.depthFirstTraversal(node, function (s) { if (s.level >= startFromLevel + 1) { s.childrenLayout = layout; } }); } else { this.graph.depthFirstTraversal(node, function (s) { s.childrenLayout = layout; }); if (!includeStart) { node.childrenLayout = rootLayout; } } }, measure: function (node, givenSize) { var w = 0, h = 0, s; var result = new Size(0, 0); if (!node) { throw ''; } var b = node.associatedShape.bounds(); var shapeWidth = b.width; var shapeHeight = b.height; if (node.parents.length !== 1) { throw 'Node not in a spanning tree.'; } var parent = node.parents[0]; if (node.treeDirection === 'Undefined') { node.treeDirection = parent.treeDirection; } if (Utils.isEmpty(node.children)) { result = new Size(Math.abs(shapeWidth) < EPSILON ? 50 : shapeWidth, Math.abs(shapeHeight) < EPSILON ? 25 : shapeHeight); } else if (node.children.length === 1) { switch (node.treeDirection) { case 'Radial': s = this.measure(node.children[0], givenSize); w = shapeWidth + this.options.radialSeparation * Math.cos(node.AngleToParent) + s.width; h = shapeHeight + Math.abs(this.options.radialSeparation * Math.sin(node.AngleToParent)) + s.height; break; case 'Left': case 'Right': switch (node.childrenLayout) { case 'TopAlignedWithParent': break; case 'BottomAlignedWithParent': break; case 'Underneath': s = this.measure(node.children[0], givenSize); w = shapeWidth + s.width + this.options.underneathHorizontalOffset; h = shapeHeight + this.options.underneathVerticalTopOffset + s.height; break; case 'Default': s = this.measure(node.children[0], givenSize); w = shapeWidth + this.options.horizontalSeparation + s.width; h = Math.max(shapeHeight, s.height); break; default: throw 'Unhandled TreeDirection in the Radial layout measuring.'; } break; case 'Up': case 'Down': switch (node.childrenLayout) { case 'TopAlignedWithParent': case 'BottomAlignedWithParent': break; case 'Underneath': s = this.measure(node.children[0], givenSize); w = Math.max(shapeWidth, s.width + this.options.underneathHorizontalOffset); h = shapeHeight + this.options.underneathVerticalTopOffset + s.height; break; case 'Default': s = this.measure(node.children[0], givenSize); h = shapeHeight + this.options.verticalSeparation + s.height; w = Math.max(shapeWidth, s.width); break; default: throw 'Unhandled TreeDirection in the Down layout measuring.'; } break; default: throw 'Unhandled TreeDirection in the layout measuring.'; } result = new Size(w, h); } else { var i, childNode; switch (node.treeDirection) { case 'Left': case 'Right': switch (node.childrenLayout) { case 'TopAlignedWithParent': case 'BottomAlignedWithParent': break; case 'Underneath': w = shapeWidth; h = shapeHeight + this.options.underneathVerticalTopOffset; for (i = 0; i < node.children.length; i++) { childNode = node.children[i]; s = this.measure(childNode, givenSize); w = Math.max(w, s.width + this.options.underneathHorizontalOffset); h += s.height + this.options.underneathVerticalSeparation; } h -= this.options.underneathVerticalSeparation; break; case 'Default': w = shapeWidth; h = 0; for (i = 0; i < node.children.length; i++) { childNode = node.children[i]; s = this.measure(childNode, givenSize); w = Math.max(w, shapeWidth + this.options.horizontalSeparation + s.width); h += s.height + this.options.verticalSeparation; } h -= this.options.verticalSeparation; break; default: throw 'Unhandled TreeDirection in the Right layout measuring.'; } break; case 'Up': case 'Down': switch (node.childrenLayout) { case 'TopAlignedWithParent': case 'BottomAlignedWithParent': break; case 'Underneath': w = shapeWidth; h = shapeHeight + this.options.underneathVerticalTopOffset; for (i = 0; i < node.children.length; i++) { childNode = node.children[i]; s = this.measure(childNode, givenSize); w = Math.max(w, s.width + this.options.underneathHorizontalOffset); h += s.height + this.options.underneathVerticalSeparation; } h -= this.options.underneathVerticalSeparation; break; case 'Default': w = 0; h = 0; for (i = 0; i < node.children.length; i++) { childNode = node.children[i]; s = this.measure(childNode, givenSize); w += s.width + this.options.horizontalSeparation; h = Math.max(h, s.height + this.options.verticalSeparation + shapeHeight); } w -= this.options.horizontalSeparation; break; default: throw 'Unhandled TreeDirection in the Down layout measuring.'; } break; default: throw 'Unhandled TreeDirection in the layout measuring.'; } result = new Size(w, h); } node.SectorAngle = Math.sqrt(w * w / 4 + h * h / 4); node.Size = result; return result; }, arrange: function (n, p) { var i, pp, child, node, childrenwidth, b = n.associatedShape.bounds(); var shapeWidth = b.width; var shapeHeight = b.height; if (Utils.isEmpty(n.children)) { n.x = p.x; n.y = p.y; n.BoundingRectangle = new Rect(p.x, p.y, shapeWidth, shapeHeight); } else { var x, y; var selfLocation; switch (n.treeDirection) { case 'Left': switch (n.childrenLayout) { case 'TopAlignedWithParent': case 'BottomAlignedWithParent': break; case 'Underneath': selfLocation = p; n.x = selfLocation.x; n.y = selfLocation.y; n.BoundingRectangle = new Rect(n.x, n.y, n.width, n.height); y = p.y + shapeHeight + this.options.underneathVerticalTopOffset; for (i = 0; i < node.children.length; i++) { node = node.children[i]; x = selfLocation.x - node.associatedShape.width - this.options.underneathHorizontalOffset; pp = new Point(x, y); this.arrange(node, pp); y += node.Size.height + this.options.underneathVerticalSeparation; } break; case 'Default': selfLocation = new Point(p.x + n.Size.width - shapeWidth, p.y + (n.Size.height - shapeHeight) / 2); n.x = selfLocation.x; n.y = selfLocation.y; n.BoundingRectangle = new Rect(n.x, n.y, n.width, n.height); x = selfLocation.x - this.options.horizontalSeparation; y = p.y; for (i = 0; i < n.children.length; i++) { node = n.children[i]; pp = new Point(x - node.Size.width, y); this.arrange(node, pp); y += node.Size.height + this.options.verticalSeparation; } break; default: throw 'Unsupported TreeDirection'; } break; case 'Right': switch (n.childrenLayout) { case 'TopAlignedWithParent': case 'BottomAlignedWithParent': break; case 'Underneath': selfLocation = p; n.x = selfLocation.x; n.y = selfLocation.y; n.BoundingRectangle = new Rect(n.x, n.y, n.width, n.height); x = p.x + shapeWidth + this.options.underneathHorizontalOffset; y = p.y + shapeHeight + this.options.underneathVerticalTopOffset; for (i = 0; i < n.children.length; i++) { node = n.children[i]; pp = new Point(x, y); this.arrange(node, pp); y += node.Size.height + this.options.underneathVerticalSeparation; } break; case 'Default': selfLocation = new Point(p.x, p.y + (n.Size.height - shapeHeight) / 2); n.x = selfLocation.x; n.y = selfLocation.y; n.BoundingRectangle = new Rect(n.x, n.y, n.width, n.height); x = p.x + shapeWidth + this.options.horizontalSeparation; y = p.y; for (i = 0; i < n.children.length; i++) { node = n.children[i]; pp = new Point(x, y); this.arrange(node, pp); y += node.Size.height + this.options.verticalSeparation; } break; default: throw 'Unsupported TreeDirection'; } break; case 'Up': selfLocation = new Point(p.x + (n.Size.width - shapeWidth) / 2, p.y + n.Size.height - shapeHeight); n.x = selfLocation.x; n.y = selfLocation.y; n.BoundingRectangle = new Rect(n.x, n.y, n.width, n.height); if (Math.abs(selfLocation.x - p.x) < EPSILON) { childrenwidth = 0; for (i = 0; i < n.children.length; i++) { child = n.children[i]; childrenwidth += child.Size.width + this.options.horizontalSeparation; } childrenwidth -= this.options.horizontalSeparation; x = p.x + (shapeWidth - childrenwidth) / 2; } else { x = p.x; } for (i = 0; i < n.children.length; i++) { node = n.children[i]; y = selfLocation.y - this.options.verticalSeparation - node.Size.height; pp = new Point(x, y); this.arrange(node, pp); x += node.Size.width + this.options.horizontalSeparation; } break; case 'Down': switch (n.childrenLayout) { case 'TopAlignedWithParent': case 'BottomAlignedWithParent': break; case 'Underneath': selfLocation = p; n.x = selfLocation.x; n.y = selfLocation.y; n.BoundingRectangle = new Rect(n.x, n.y, n.width, n.height); x = p.x + this.options.underneathHorizontalOffset; y = p.y + shapeHeight + this.options.underneathVerticalTopOffset; for (i = 0; i < n.children.length; i++) { node = n.children[i]; pp = new Point(x, y); this.arrange(node, pp); y += node.Size.height + this.options.underneathVerticalSeparation; } break; case 'Default': selfLocation = new Point(p.x + (n.Size.width - shapeWidth) / 2, p.y); n.x = selfLocation.x; n.y = selfLocation.y; n.BoundingRectangle = new Rect(n.x, n.y, n.width, n.height); if (Math.abs(selfLocation.x - p.x) < EPSILON) { childrenwidth = 0; for (i = 0; i < n.children.length; i++) { child = n.children[i]; childrenwidth += child.Size.width + this.options.horizontalSeparation; } childrenwidth -= this.options.horizontalSeparation; x = p.x + (shapeWidth - childrenwidth) / 2; } else { x = p.x; } for (i = 0; i < n.children.length; i++) { node = n.children[i]; y = selfLocation.y + this.options.verticalSeparation + shapeHeight; pp = new Point(x, y); this.arrange(node, pp); x += node.Size.width + this.options.horizontalSeparation; } break; default: throw 'Unsupported TreeDirection'; } break; case 'None': break; default: throw 'Unsupported TreeDirection'; } } }, layoutSwitch: function () { if (!this.center) { return; } if (Utils.isEmpty(this.center.children)) { return; } var type = this.options.subtype; if (Utils.isUndefined(type)) { type = 'Down'; } var single, male, female, leftcount; var children = this.center.children; switch (type.toLowerCase()) { case 'radial': case 'radialtree': this.layoutRadialTree(); break; case 'mindmaphorizontal': case 'mindmap': single = this.center.children; if (this.center.children.length === 1) { this.layoutRight(single); } else { leftcount = children.length / 2; male = grep(this.center.children, function (n) { return Utils.indexOf(children, n) < leftcount; }); female = grep(this.center.children, function (n) { return Utils.indexOf(children, n) >= leftcount; }); this.layoutLeft(male); this.layoutRight(female); } break; case 'mindmapvertical': single = this.center.children; if (this.center.children.length === 1) { this.layoutDown(single); } else { leftcount = children.length / 2; male = grep(this.center.children, function (n) { return Utils.indexOf(children, n) < leftcount; }); female = grep(this.center.children, function (n) { return Utils.indexOf(children, n) >= leftcount; }); this.layoutUp(male); this.layoutDown(female); } break; case 'right': this.layoutRight(this.center.children); break; case 'left': this.layoutLeft(this.center.children); break; case 'up': case 'bottom': this.layoutUp(this.center.children); break; case 'down': case 'top': this.layoutDown(this.center.children); break; case 'tipover': case 'tipovertree': if (this.options.tipOverTreeStartLevel < 0) { throw 'The tip-over level should be a positive integer.'; } this.tipOverTree(this.center.children, this.options.tipOverTreeStartLevel); break; case 'undefined': case 'none': break; } } }); var TreeLayout = LayoutBase.extend({ init: function (diagram) { var that = this; LayoutBase.fn.init.call(that); if (Utils.isUndefined(diagram)) { throw 'No diagram specified.'; } this.diagram = diagram; }, layout: function (options) { this.transferOptions(options); var adapter = new DiagramToHyperTreeAdapter(this.diagram); this.graph = adapter.convert(); var finalNodeSet = this.layoutComponents(); return new diagram.LayoutState(this.diagram, finalNodeSet); }, layoutComponents: function () { if (this.graph.isEmpty()) { return; } var components = this.graph.getConnectedComponents(); if (Utils.isEmpty(components)) { return; } var layout = new TreeLayoutProcessor(this.options); var trees = []; for (var i = 0; i < components.length; i++) { var component = components[i]; var treeGraph = this.getTree(component); if (!treeGraph) { throw 'Failed to find a spanning tree for the component.'; } var root = treeGraph.root; var tree = treeGraph.tree; layout.layout(tree, root); trees.push(tree); } return this.gridLayoutComponents(trees); }, getTree: function (graph) { var root = null; if (this.options.roots && this.options.roots.length > 0) { for (var i = 0, len = graph.nodes.length; i < len; i++) { var node = graph.nodes[i]; for (var j = 0; j < this.options.roots.length; j++) { var givenRootShape = this.options.roots[j]; if (givenRootShape === node.associatedShape) { root = node; break; } } } } if (!root) { root = graph.root(); if (!root) { throw 'Unable to find a root for the tree.'; } } return this.getTreeForRoot(graph, root); }, getTreeForRoot: function (graph, root) { var tree = graph.getSpanningTree(root); if (Utils.isUndefined(tree) || tree.isEmpty()) { return null; } return { tree: tree, root: tree.root }; } }); var LayeredLayout = LayoutBase.extend({ init: function (diagram) { var that = this; LayoutBase.fn.init.call(that); if (Utils.isUndefined(diagram)) { throw 'Diagram is not specified.'; } this.diagram = diagram; }, layout: function (options) { this.transferOptions(options); var adapter = new DiagramToHyperTreeAdapter(this.diagram); var graph = adapter.convert(options); if (graph.isEmpty()) { return; } var components = graph.getConnectedComponents(); if (Utils.isEmpty(components)) { return; } for (var i = 0; i < components.length; i++) { var component = components[i]; this.layoutGraph(component, options); } var finalNodeSet = this.gridLayoutComponents(components); return new diagram.LayoutState(this.diagram, finalNodeSet); }, _initRuntimeProperties: function () { for (var k = 0; k < this.graph.nodes.length; k++) { var node = this.graph.nodes[k]; node.layer = -1; node.downstreamLinkCount = 0; node.upstreamLinkCount = 0; node.isVirtual = false; node.uBaryCenter = 0; node.dBaryCenter = 0; node.upstreamPriority = 0; node.downstreamPriority = 0; node.gridPosition = 0; } }, _prepare: function (graph) { var current = [], i, l, link; var layerMap = new Dictionary(); var layerCount = 0; var targetLayer, next, target; Utils.forEach(graph.nodes, function (node) { if (node.incoming.length === 0) { layerMap.set(node, 0); current.push(node); } }); while (current.length > 0) { next = current.shift(); for (i = 0; i < next.outgoing.length; i++) { link = next.outgoing[i]; target = link.target; if (layerMap.containsKey(target)) { targetLayer = Math.max(layerMap.get(next) + 1, layerMap.get(target)); } else { targetLayer = layerMap.get(next) + 1; } layerMap.set(target, targetLayer); if (targetLayer > layerCount) { layerCount = targetLayer; } if (!contains(current, target)) { current.push(target); } } } var sortedNodes = layerMap.keys(); sortedNodes.sort(function (o1, o2) { var o1layer = layerMap.get(o1); var o2layer = layerMap.get(o2); return Utils.sign(o2layer - o1layer); }); for (var n = 0; n < sortedNodes.length; ++n) { var node = sortedNodes[n]; var minLayer = Number.MAX_VALUE; if (node.outgoing.length === 0) { continue; } for (l = 0; l < node.outgoing.length; ++l) { link = node.outgoing[l]; minLayer = Math.min(minLayer, layerMap.get(link.target)); } if (minLayer > 1) { layerMap.set(node, minLayer - 1); } } this.layers = []; var layer; for (i = 0; i < layerCount + 1; i++) { layer = []; layer.linksTo = {}; this.layers.push(layer); } layerMap.forEach(function (node, layer) { node.layer = layer; this.layers[layer].push(node); }, this); for (l = 0; l < this.layers.length; l++) { layer = this.layers[l]; for (i = 0; i < layer.length; i++) { layer[i].gridPosition = i; } } }, layoutGraph: function (graph, options) { if (Utils.isUndefined(graph)) { throw 'No graph given or graph analysis of the diagram failed.'; } if (Utils.isDefined(options)) { this.transferOptions(options); } this.graph = graph; graph.setItemIndices(); var reversedEdges = graph.makeAcyclic(); this._initRuntimeProperties(); this._prepare(graph, options); this._dummify(); this._optimizeCrossings(); this._swapPairs(); this.arrangeNodes(); this._moveThingsAround(); this._dedummify(); Utils.forEach(reversedEdges, function (e) { if (e.points) { e.points.reverse(); } }); }, setMinDist: function (m, n, minDist) { var l = m.layer; var i = m.layerIndex; this.minDistances[l][i] = minDist; }, getMinDist: function (m, n) { var dist = 0, i1 = m.layerIndex, i2 = n.layerIndex, l = m.layer, min = Math.min(i1, i2), max = Math.max(i1, i2); for (var k = min; k < max; ++k) { dist += this.minDistances[l][k]; } return dist; }, placeLeftToRight: function (leftClasses) { var leftPos = new Dictionary(), n, node; for (var c = 0; c < this.layers.length; ++c) { var classNodes = leftClasses[c]; if (!classNodes) { continue; } for (n = 0; n < classNodes.length; n++) { node = classNodes[n]; if (!leftPos.containsKey(node)) { this.placeLeft(node, leftPos, c); } } var d = Number.POSITIVE_INFINITY; for (n = 0; n < classNodes.length; n++) { node = classNodes[n]; var rightSibling = this.rightSibling(node); if (rightSibling && this.nodeLeftClass.get(rightSibling) !== c) { d = Math.min(d, leftPos.get(rightSibling) - leftPos.get(node) - this.getMinDist(node, rightSibling)); } } if (d === Number.POSITIVE_INFINITY) { var D = []; for (n = 0; n < classNodes.length; n++) { node = classNodes[n]; var neighbors = []; Utils.addRange(neighbors, this.upNodes.get(node)); Utils.addRange(neighbors, this.downNodes.get(node)); for (var e = 0; e < neighbors.length; e++) { var neighbor = neighbors[e]; if (this.nodeLeftClass.get(neighbor) < c) { D.push(leftPos.get(neighbor) - leftPos.get(node)); } } } D.sort(); if (D.length === 0) { d = 0; } else if (D.length % 2 === 1) { d = D[this.intDiv(D.length, 2)]; } else { d = (D[this.intDiv(D.length, 2) - 1] + D[this.intDiv(D.length, 2)]) / 2; } } for (n = 0; n < classNodes.length; n++) { node = classNodes[n]; leftPos.set(node, leftPos.get(node) + d); } } return leftPos; }, placeRightToLeft: function (rightClasses) { var rightPos = new Dictionary(), n, node; for (var c = 0; c < this.layers.length; ++c) { var classNodes = rightClasses[c]; if (!classNodes) { continue; } for (n = 0; n < classNodes.length; n++) { node = classNodes[n]; if (!rightPos.containsKey(node)) { this.placeRight(node, rightPos, c); } } var d = Number.NEGATIVE_INFINITY; for (n = 0; n < classNodes.length; n++) { node = classNodes[n]; var leftSibling = this.leftSibling(node); if (leftSibling && this.nodeRightClass.get(leftSibling) !== c) { d = Math.max(d, rightPos.get(leftSibling) - rightPos.get(node) + this.getMinDist(leftSibling, node)); } } if (d === Number.NEGATIVE_INFINITY) { var D = []; for (n = 0; n < classNodes.length; n++) { node = classNodes[n]; var neighbors = []; Utils.addRange(neighbors, this.upNodes.get(node)); Utils.addRange(neighbors, this.downNodes.get(node)); for (var e = 0; e < neighbors.length; e++) { var neighbor = neighbors[e]; if (this.nodeRightClass.get(neighbor) < c) { D.push(rightPos.get(node) - rightPos.get(neighbor)); } } } D.sort(); if (D.length === 0) { d = 0; } else if (D.length % 2 === 1) { d = D[this.intDiv(D.length, 2)]; } else { d = (D[this.intDiv(D.length, 2) - 1] + D[this.intDiv(D.length, 2)]) / 2; } } for (n = 0; n < classNodes.length; n++) { node = classNodes[n]; rightPos.set(node, rightPos.get(node) + d); } } return rightPos; }, _getLeftWing: function () { var leftWing = { value: null }; var result = this.computeClasses(leftWing, 1); this.nodeLeftClass = leftWing.value; return result; }, _getRightWing: function () { var rightWing = { value: null }; var result = this.computeClasses(rightWing, -1); this.nodeRightClass = rightWing.value; return result; }, computeClasses: function (wingPair, d) { var currentWing = 0, wing = wingPair.value = new Dictionary(); for (var l = 0; l < this.layers.length; ++l) { currentWing = l; var layer = this.layers[l]; for (var n = d === 1 ? 0 : layer.length - 1; 0 <= n && n < layer.length; n += d) { var node = layer[n]; if (!wing.containsKey(node)) { wing.set(node, currentWing); if (node.isVirtual) { var ndsinl = this._nodesInLink(node); for (var kk = 0; kk < ndsinl.length; kk++) { var vnode = ndsinl[kk]; wing.set(vnode, currentWing); } } } else { currentWing = wing.get(node); } } } var wings = []; for (var i = 0; i < this.layers.length; i++) { wings.push(null); } wing.forEach(function (node, classIndex) { if (wings[classIndex] === null) { wings[classIndex] = []; } wings[classIndex].push(node); }); return wings; }, _isVerticalLayout: function () { return this.options.subtype.toLowerCase() === 'up' || this.options.subtype.toLowerCase() === 'down' || this.options.subtype.toLowerCase() === 'vertical'; }, _isHorizontalLayout: function () { return this.options.subtype.toLowerCase() === 'right' || this.options.subtype.toLowerCase() === 'left' || this.options.subtype.toLowerCase() === 'horizontal'; }, _isIncreasingLayout: function () { return this.options.subtype.toLowerCase() === 'right' || this.options.subtype.toLowerCase() === 'down'; }, _moveThingsAround: function () { var i, l, node, layer, n, w; for (l = 0; l < this.layers.length; ++l) { layer = this.layers[l]; layer.sort(this._gridPositionComparer); } this.minDistances = []; for (l = 0; l < this.layers.length; ++l) { layer = this.layers[l]; this.minDistances[l] = []; for (n = 0; n < layer.length; ++n) { node = layer[n]; node.layerIndex = n; this.minDistances[l][n] = this.options.nodeDistance; if (n < layer.length - 1) { if (this._isVerticalLayout()) { this.minDistances[l][n] += (node.width + layer[n + 1].width) / 2; } else { this.minDistances[l][n] += (node.height + layer[n + 1].height) / 2; } } } } this.downNodes = new Dictionary(); this.upNodes = new Dictionary(); Utils.forEach(this.graph.nodes, function (node) { this.downNodes.set(node, []); this.upNodes.set(node, []); }, this); Utils.forEach(this.graph.links, function (link) { var origin = link.source; var dest = link.target; var down = null, up = null; if (origin.layer > dest.layer) { down = link.source; up = link.target; } else { up = link.source; down = link.target; } this.downNodes.get(up).push(down); this.upNodes.get(down).push(up); }, this); this.downNodes.forEachValue(function (list) { list.sort(this._gridPositionComparer); }, this); this.upNodes.forEachValue(function (list) { list.sort(this._gridPositionComparer); }, this); for (l = 0; l < this.layers.length - 1; ++l) { layer = this.layers[l]; for (w = 0; w < layer.length - 1; w++) { var currentNode = layer[w]; if (!currentNode.isVirtual) { continue; } var currDown = this.downNodes.get(currentNode)[0]; if (!currDown.isVirtual) { continue; } for (n = w + 1; n < layer.length; ++n) { node = layer[n]; if (!node.isVirtual) { continue; } var downNode = this.downNodes.get(node)[0]; if (!downNode.isVirtual) { continue; } if (currDown.gridPosition > downNode.gridPosition) { var pos = currDown.gridPosition; currDown.gridPosition = downNode.gridPosition; downNode.gridPosition = pos; var i1 = currDown.layerIndex; var i2 = downNode.layerIndex; this.layers[l + 1][i1] = downNode; this.layers[l + 1][i2] = currDown; currDown.layerIndex = i2; downNode.layerIndex = i1; } } } } var leftClasses = this._getLeftWing(); var rightClasses = this._getRightWing(); var leftPos = this.placeLeftToRight(leftClasses); var rightPos = this.placeRightToLeft(rightClasses); var x = new Dictionary(); Utils.forEach(this.graph.nodes, function (node) { x.set(node, (leftPos.get(node) + rightPos.get(node)) / 2); }); var order = new Dictionary(); var placed = new Dictionary(); for (l = 0; l < this.layers.length; ++l) { layer = this.layers[l]; var sequenceStart = -1, sequenceEnd = -1; for (n = 0; n < layer.length; ++n) { node = layer[n]; order.set(node, 0); placed.set(node, false); if (node.isVirtual) { if (sequenceStart === -1) { sequenceStart = n; } else if (sequenceStart === n - 1) { sequenceStart = n; } else { sequenceEnd = n; order.set(layer[sequenceStart], 0); if (x.get(node) - x.get(layer[sequenceStart]) === this.getMinDist(layer[sequenceStart], node)) { placed.set(layer[sequenceStart], true); } else { placed.set(layer[sequenceStart], false); } sequenceStart = n; } } } } var directions = [ 1, -1 ]; Utils.forEach(directions, function (d) { var start = d === 1 ? 0 : this.layers.length - 1; for (var l = start; 0 <= l && l < this.layers.length; l += d) { var layer = this.layers[l]; var virtualStartIndex = this._firstVirtualNode(layer); var virtualStart = null; var sequence = null; if (virtualStartIndex !== -1) { virtualStart = layer[virtualStartIndex]; sequence = []; for (i = 0; i < virtualStartIndex; i++) { sequence.push(layer[i]); } } else { virtualStart = null; sequence = layer; } if (sequence.length > 0) { this._sequencer(x, null, virtualStart, d, sequence); for (i = 0; i < sequence.length - 1; ++i) { this.setMinDist(sequence[i], sequence[i + 1], x.get(sequence[i + 1]) - x.get(sequence[i])); } if (virtualStart) { this.setMinDist(sequence[sequence.length - 1], virtualStart, x.get(virtualStart) - x.get(sequence[sequence.length - 1])); } } while (virtualStart) { var virtualEnd = this.nextVirtualNode(layer, virtualStart); if (!virtualEnd) { virtualStartIndex = virtualStart.layerIndex; sequence = []; for (i = virtualStartIndex + 1; i < layer.length; i++) { sequence.push(layer[i]); } if (sequence.length > 0) { this._sequencer(x, virtualStart, null, d, sequence); for (i = 0; i < sequence.length - 1; ++i) { this.setMinDist(sequence[i], sequence[i + 1], x.get(sequence[i + 1]) - x.get(sequence[i])); } this.setMinDist(virtualStart, sequence[0], x.get(sequence[0]) - x.get(virtualStart)); } } else if (order.get(virtualStart) === d) { virtualStartIndex = virtualStart.layerIndex; var virtualEndIndex = virtualEnd.layerIndex; sequence = []; for (i = virtualStartIndex + 1; i < virtualEndIndex; i++) { sequence.push(layer[i]); } if (sequence.length > 0) { this._sequencer(x, virtualStart, virtualEnd, d, sequence); } placed.set(virtualStart, true); } virtualStart = virtualEnd; } this.adjustDirections(l, d, order, placed); } }, this); var fromLayerIndex = this._isIncreasingLayout() ? 0 : this.layers.length - 1; var reachedFinalLayerIndex = function (k, ctx) { if (ctx._isIncreasingLayout()) { return k < ctx.layers.length; } else { return k >= 0; } }; var layerIncrement = this._isIncreasingLayout() ? +1 : -1, offset = 0; function maximumHeight(layer, ctx) { var height = Number.MIN_VALUE; for (var n = 0; n < layer.length; ++n) { var node = layer[n]; if (ctx._isVerticalLayout()) { height = Math.max(height, node.height); } else { height = Math.max(height, node.width); } } return height; } for (i = fromLayerIndex; reachedFinalLayerIndex(i, this); i += layerIncrement) { layer = this.layers[i]; var height = maximumHeight(layer, this); for (n = 0; n < layer.length; ++n) { node = layer[n]; if (this._isVerticalLayout()) { node.x = x.get(node); node.y = offset + height / 2; } else { node.x = offset + height / 2; node.y = x.get(node); } } offset += this.options.layerSeparation + height; } }, adjustDirections: function (l, d, order, placed) { if (l + d < 0 || l + d >= this.layers.length) { return; } var prevBridge = null, prevBridgeTarget = null; var layer = this.layers[l + d]; for (var n = 0; n < layer.length; ++n) { var nextBridge = layer[n]; if (nextBridge.isVirtual) { var nextBridgeTarget = this.getNeighborOnLayer(nextBridge, l); if (nextBridgeTarget.isVirtual) { if (prevBridge) { var p = placed.get(prevBridgeTarget); var clayer = this.layers[l]; var i1 = prevBridgeTarget.layerIndex; var i2 = nextBridgeTarget.layerIndex; for (var i = i1 + 1; i < i2; ++i) { if (clayer[i].isVirtual) { p = p && placed.get(clayer[i]); } } if (p) { order.set(prevBridge, d); var j1 = prevBridge.layerIndex; var j2 = nextBridge.layerIndex; for (var j = j1 + 1; j < j2; ++j) { if (layer[j].isVirtual) { order.set(layer[j], d); } } } } prevBridge = nextBridge; prevBridgeTarget = nextBridgeTarget; } } } }, getNeighborOnLayer: function (node, l) { var neighbor = this.upNodes.get(node)[0]; if (neighbor.layer === l) { return neighbor; } neighbor = this.downNodes.get(node)[0]; if (neighbor.layer === l) { return neighbor; } return null; }, _sequencer: function (x, virtualStart, virtualEnd, dir, sequence) { if (sequence.length === 1) { this._sequenceSingle(x, virtualStart, virtualEnd, dir, sequence[0]); } if (sequence.length > 1) { var r = sequence.length, t = this.intDiv(r, 2); this._sequencer(x, virtualStart, virtualEnd, dir, sequence.slice(0, t)); this._sequencer(x, virtualStart, virtualEnd, dir, sequence.slice(t)); this.combineSequences(x, virtualStart, virtualEnd, dir, sequence); } }, _sequenceSingle: function (x, virtualStart, virtualEnd, dir, node) { var neighbors = dir === -1 ? this.downNodes.get(node) : this.upNodes.get(node); var n = neighbors.length; if (n !== 0) { if (n % 2 === 1) { x.set(node, x.get(neighbors[this.intDiv(n, 2)])); } else { x.set(node, (x.get(neighbors[this.intDiv(n, 2) - 1]) + x.get(neighbors[this.intDiv(n, 2)])) / 2); } if (virtualStart) { x.set(node, Math.max(x.get(node), x.get(virtualStart) + this.getMinDist(virtualStart, node))); } if (virtualEnd) { x.set(node, Math.min(x.get(node), x.get(virtualEnd) - this.getMinDist(node, virtualEnd))); } } }, combineSequences: function (x, virtualStart, virtualEnd, dir, sequence) { var r = sequence.length, t = this.intDiv(r, 2); var leftHeap = [], i, c, n, neighbors, neighbor, pair; for (i = 0; i < t; ++i) { c = 0; neighbors = dir === -1 ? this.downNodes.get(sequence[i]) : this.upNodes.get(sequence[i]); for (n = 0; n < neighbors.length; ++n) { neighbor = neighbors[n]; if (x.get(neighbor) >= x.get(sequence[i])) { c++; } else { c--; leftHeap.push({ k: x.get(neighbor) + this.getMinDist(sequence[i], sequence[t - 1]), v: 2 }); } } leftHeap.push({ k: x.get(sequence[i]) + this.getMinDist(sequence[i], sequence[t - 1]), v: c }); } if (virtualStart) { leftHeap.push({ k: x.get(virtualStart) + this.getMinDist(virtualStart, sequence[t - 1]), v: Number.MAX_VALUE }); } leftHeap.sort(this._positionDescendingComparer); var rightHeap = []; for (i = t; i < r; ++i) { c = 0; neighbors = dir === -1 ? this.downNodes.get(sequence[i]) : this.upNodes.get(sequence[i]); for (n = 0; n < neighbors.length; ++n) { neighbor = neighbors[n]; if (x.get(neighbor) <= x.get(sequence[i])) { c++; } else { c--; rightHeap.push({ k: x.get(neighbor) - this.getMinDist(sequence[i], sequence[t]), v: 2 }); } } rightHeap.push({ k: x.get(sequence[i]) - this.getMinDist(sequence[i], sequence[t]), v: c }); } if (virtualEnd) { rightHeap.push({ k: x.get(virtualEnd) - this.getMinDist(virtualEnd, sequence[t]), v: Number.MAX_VALUE }); } rightHeap.sort(this._positionAscendingComparer); var leftRes = 0, rightRes = 0; var m = this.getMinDist(sequence[t - 1], sequence[t]); while (x.get(sequence[t]) - x.get(sequence[t - 1]) < m) { if (leftRes < rightRes) { if (leftHeap.length === 0) { x.set(sequence[t - 1], x.get(sequence[t]) - m); break; } else { pair = leftHeap.shift(); leftRes = leftRes + pair.v; x.set(sequence[t - 1], pair.k); x.set(sequence[t - 1], Math.max(x.get(sequence[t - 1]), x.get(sequence[t]) - m)); } } else { if (rightHeap.length === 0) { x.set(sequence[t], x.get(sequence[t - 1]) + m); break; } else { pair = rightHeap.shift(); rightRes = rightRes + pair.v; x.set(sequence[t], pair.k); x.set(sequence[t], Math.min(x.get(sequence[t]), x.get(sequence[t - 1]) + m)); } } } for (i = t - 2; i >= 0; i--) { x.set(sequence[i], Math.min(x.get(sequence[i]), x.get(sequence[t - 1]) - this.getMinDist(sequence[i], sequence[t - 1]))); } for (i = t + 1; i < r; i++) { x.set(sequence[i], Math.max(x.get(sequence[i]), x.get(sequence[t]) + this.getMinDist(sequence[i], sequence[t]))); } }, placeLeft: function (node, leftPos, leftClass) { var pos = Number.NEGATIVE_INFINITY; Utils.forEach(this._getComposite(node), function (v) { var leftSibling = this.leftSibling(v); if (leftSibling && this.nodeLeftClass.get(leftSibling) === this.nodeLeftClass.get(v)) { if (!leftPos.containsKey(leftSibling)) { this.placeLeft(leftSibling, leftPos, leftClass); } pos = Math.max(pos, leftPos.get(leftSibling) + this.getMinDist(leftSibling, v)); } }, this); if (pos === Number.NEGATIVE_INFINITY) { pos = 0; } Utils.forEach(this._getComposite(node), function (v) { leftPos.set(v, pos); }); }, placeRight: function (node, rightPos, rightClass) { var pos = Number.POSITIVE_INFINITY; Utils.forEach(this._getComposite(node), function (v) { var rightSibling = this.rightSibling(v); if (rightSibling && this.nodeRightClass.get(rightSibling) === this.nodeRightClass.get(v)) { if (!rightPos.containsKey(rightSibling)) { this.placeRight(rightSibling, rightPos, rightClass); } pos = Math.min(pos, rightPos.get(rightSibling) - this.getMinDist(v, rightSibling)); } }, this); if (pos === Number.POSITIVE_INFINITY) { pos = 0; } Utils.forEach(this._getComposite(node), function (v) { rightPos.set(v, pos); }); }, leftSibling: function (node) { var layer = this.layers[node.layer], layerIndex = node.layerIndex; return layerIndex === 0 ? null : layer[layerIndex - 1]; }, rightSibling: function (node) { var layer = this.layers[node.layer]; var layerIndex = node.layerIndex; return layerIndex === layer.length - 1 ? null : layer[layerIndex + 1]; }, _getComposite: function (node) { return node.isVirtual ? this._nodesInLink(node) : [node]; }, arrangeNodes: function () { var i, l, ni, layer, node; for (l = 0; l < this.layers.length; l++) { layer = this.layers[l]; for (ni = 0; ni < layer.length; ni++) { node = layer[ni]; node.upstreamPriority = node.upstreamLinkCount; node.downstreamPriority = node.downstreamLinkCount; } } var maxLayoutIterations = 2; for (var it = 0; it < maxLayoutIterations; it++) { for (i = this.layers.length - 1; i >= 1; i--) { this.layoutLayer(false, i); } for (i = 0; i < this.layers.length - 1; i++) { this.layoutLayer(true, i); } } var gridPos = Number.MAX_VALUE; for (l = 0; l < this.layers.length; l++) { layer = this.layers[l]; for (ni = 0; ni < layer.length; ni++) { node = layer[ni]; gridPos = Math.min(gridPos, node.gridPosition); } } if (gridPos < 0) { for (l = 0; l < this.layers.length; l++) { layer = this.layers[l]; for (ni = 0; ni < layer.length; ni++) { node = layer[ni]; node.gridPosition = node.gridPosition - gridPos; } } } }, layoutLayer: function (down, layer) { var iconsidered; var considered; if (down) { considered = this.layers[iconsidered = layer + 1]; } else { considered = this.layers[iconsidered = layer - 1]; } var sorted = []; for (var n = 0; n < considered.length; n++) { sorted.push(considered[n]); } sorted.sort(function (n1, n2) { var n1Priority = (n1.upstreamPriority + n1.downstreamPriority) / 2; var n2Priority = (n2.upstreamPriority + n2.downstreamPriority) / 2; if (Math.abs(n1Priority - n2Priority) < 0.0001) { return 0; } if (n1Priority < n2Priority) { return 1; } return -1; }); Utils.forEach(sorted, function (node) { var nodeGridPos = node.gridPosition; var nodeBaryCenter = this.calcBaryCenter(node); var nodePriority = (node.upstreamPriority + node.downstreamPriority) / 2; if (Math.abs(nodeGridPos - nodeBaryCenter) < 0.0001) { return; } if (Math.abs(nodeGridPos - nodeBaryCenter) < 0.25 + 0.0001) { return; } if (nodeGridPos < nodeBaryCenter) { while (nodeGridPos < nodeBaryCenter) { if (!this.moveRight(node, considered, nodePriority)) { break; } nodeGridPos = node.gridPosition; } } else { while (nodeGridPos > nodeBaryCenter) { if (!this.moveLeft(node, considered, nodePriority)) { break; } nodeGridPos = node.gridPosition; } } }, this); if (iconsidered > 0) { this.calcDownData(iconsidered - 1); } if (iconsidered < this.layers.length - 1) { this.calcUpData(iconsidered + 1); } }, moveRight: function (node, layer, priority) { var index = Utils.indexOf(layer, node); if (index === layer.length - 1) { node.gridPosition = node.gridPosition + 0.5; return true; } var rightNode = layer[index + 1]; var rightNodePriority = (rightNode.upstreamPriority + rightNode.downstreamPriority) / 2; if (rightNode.gridPosition > node.gridPosition + 1) { node.gridPosition = node.gridPosition + 0.5; return true; } if (rightNodePriority > priority || Math.abs(rightNodePriority - priority) < 0.0001) { return false; } if (this.moveRight(rightNode, layer, priority)) { node.gridPosition = node.gridPosition + 0.5; return true; } return false; }, moveLeft: function (node, layer, priority) { var index = Utils.indexOf(layer, node); if (index === 0) { node.gridPosition = node.gridPosition - 0.5; return true; } var leftNode = layer[index - 1]; var leftNodePriority = (leftNode.upstreamPriority + leftNode.downstreamPriority) / 2; if (leftNode.gridPosition < node.gridPosition - 1) { node.gridPosition = node.gridPosition - 0.5; return true; } if (leftNodePriority > priority || Math.abs(leftNodePriority - priority) < 0.0001) { return false; } if (this.moveLeft(leftNode, layer, priority)) { node.gridPosition = node.gridPosition - 0.5; return true; } return false; }, mapVirtualNode: function (node, link) { this.nodeToLinkMap.set(node, link); if (!this.linkToNodeMap.containsKey(link)) { this.linkToNodeMap.set(link, []); } this.linkToNodeMap.get(link).push(node); }, _nodesInLink: function (node) { return this.linkToNodeMap.get(this.nodeToLinkMap.get(node)); }, _dummify: function () { this.linkToNodeMap = new Dictionary(); this.nodeToLinkMap = new Dictionary(); var layer, pos, newNode, node, r, newLink, i, l, links = this.graph.links.slice(0); var layers = this.layers; var addLinkBetweenLayers = function (upLayer, downLayer, link) { layers[upLayer].linksTo[downLayer] = layers[upLayer].linksTo[downLayer] || []; layers[upLayer].linksTo[downLayer].push(link); }; for (l = 0; l < links.length; l++) { var link = links[l]; var o = link.source; var d = link.target; var oLayer = o.layer; var dLayer = d.layer; var oPos = o.gridPosition; var dPos = d.gridPosition; var step = (dPos - oPos) / Math.abs(dLayer - oLayer); var p = o; if (oLayer - dLayer > 1) { for (i = oLayer - 1; i > dLayer; i--) { newNode = new Node(); newNode.x = o.x; newNode.y = o.y; newNode.width = o.width / 100; newNode.height = o.height / 100; layer = layers[i]; pos = (i - dLayer) * step + oPos; if (pos > layer.length) { pos = layer.length; } if (oPos >= layers[oLayer].length - 1 && dPos >= layers[dLayer].length - 1) { pos = layer.length; } else if (oPos === 0 && dPos === 0) { pos = 0; } newNode.layer = i; newNode.uBaryCenter = 0; newNode.dBaryCenter = 0; newNode.upstreamLinkCount = 0; newNode.downstreamLinkCount = 0; newNode.gridPosition = pos; newNode.isVirtual = true; Utils.insert(layer, newNode, pos); for (r = pos + 1; r < layer.length; r++) { node = layer[r]; node.gridPosition = node.gridPosition + 1; } newLink = new Link(p, newNode); newLink.depthOfDumminess = 0; addLinkBetweenLayers(i - 1, i, newLink); p = newNode; this.graph._addNode(newNode); this.graph.addLink(newLink); newNode.index = this.graph.nodes.length - 1; this.mapVirtualNode(newNode, link); } addLinkBetweenLayers(dLayer - 1, dLayer, newLink); link.changeSource(p); link.depthOfDumminess = oLayer - dLayer - 1; } else if (oLayer - dLayer < -1) { for (i = oLayer + 1; i < dLayer; i++) { newNode = new Node(); newNode.x = o.x; newNode.y = o.y; newNode.width = o.width / 100; newNode.height = o.height / 100; layer = layers[i]; pos = (i - oLayer) * step + oPos; if (pos > layer.length) { pos = layer.length; } if (oPos >= layers[oLayer].length - 1 && dPos >= layers[dLayer].length - 1) { pos = layer.length; } else if (oPos === 0 && dPos === 0) { pos = 0; } newNode.layer = i; newNode.uBaryCenter = 0; newNode.dBaryCenter = 0; newNode.upstreamLinkCount = 0; newNode.downstreamLinkCount = 0; newNode.gridPosition = pos; newNode.isVirtual = true; pos &= pos; Utils.insert(layer, newNode, pos); for (r = pos + 1; r < layer.length; r++) { node = layer[r]; node.gridPosition = node.gridPosition + 1; } newLink = new Link(p, newNode); newLink.depthOfDumminess = 0; addLinkBetweenLayers(i - 1, i, newLink); p = newNode; this.graph._addNode(newNode); this.graph.addLink(newLink); newNode.index = this.graph.nodes.length - 1; this.mapVirtualNode(newNode, link); } addLinkBetweenLayers(dLayer - 1, dLayer, link); link.changeSource(p); link.depthOfDumminess = dLayer - oLayer - 1; } else { addLinkBetweenLayers(oLayer, dLayer, link); } } }, _dedummify: function () { var dedum = true; while (dedum) { dedum = false; for (var l = 0; l < this.graph.links.length; l++) { var link = this.graph.links[l]; if (!link.depthOfDumminess) { continue; } var points = []; points.unshift({ x: link.target.x, y: link.target.y }); points.unshift({ x: link.source.x, y: link.source.y }); var temp = link; var depthOfDumminess = link.depthOfDumminess; for (var d = 0; d < depthOfDumminess; d++) { var node = temp.source; var prevLink = node.incoming[0]; points.unshift({ x: prevLink.source.x, y: prevLink.source.y }); temp = prevLink; } link.changeSource(temp.source); link.depthOfDumminess = 0; if (points.length > 2) { points.splice(0, 1); points.splice(points.length - 1); link.points = points; } else { link.points = []; } dedum = true; break; } } }, _optimizeCrossings: function () { var moves = -1, i; var maxIterations = 3; var iter = 0; while (moves !== 0) { if (iter++ > maxIterations) { break; } moves = 0; for (i = this.layers.length - 1; i >= 1; i--) { moves += this.optimizeLayerCrossings(false, i); } for (i = 0; i < this.layers.length - 1; i++) { moves += this.optimizeLayerCrossings(true, i); } } }, calcUpData: function (layer) { if (layer === 0) { return; } var considered = this.layers[layer], i, l, link; var upLayer = new Set(); var temp = this.layers[layer - 1]; for (i = 0; i < temp.length; i++) { upLayer.add(temp[i]); } for (i = 0; i < considered.length; i++) { var node = considered[i]; var sum = 0; var total = 0; for (l = 0; l < node.incoming.length; l++) { link = node.incoming[l]; if (upLayer.contains(link.source)) { total++; sum += link.source.gridPosition; } } for (l = 0; l < node.outgoing.length; l++) { link = node.outgoing[l]; if (upLayer.contains(link.target)) { total++; sum += link.target.gridPosition; } } if (total > 0) { node.uBaryCenter = sum / total; node.upstreamLinkCount = total; } else { node.uBaryCenter = i; node.upstreamLinkCount = 0; } } }, calcDownData: function (layer) { if (layer === this.layers.length - 1) { return; } var considered = this.layers[layer], i, l, link; var downLayer = new Set(); var temp = this.layers[layer + 1]; for (i = 0; i < temp.length; i++) { downLayer.add(temp[i]); } for (i = 0; i < considered.length; i++) { var node = considered[i]; var sum = 0; var total = 0; for (l = 0; l < node.incoming.length; l++) { link = node.incoming[l]; if (downLayer.contains(link.source)) { total++; sum += link.source.gridPosition; } } for (l = 0; l < node.outgoing.length; l++) { link = node.outgoing[l]; if (downLayer.contains(link.target)) { total++; sum += link.target.gridPosition; } } if (total > 0) { node.dBaryCenter = sum / total; node.downstreamLinkCount = total; } else { node.dBaryCenter = i; node.downstreamLinkCount = 0; } } }, optimizeLayerCrossings: function (down, layer) { var iconsidered; var considered; if (down) { considered = this.layers[iconsidered = layer + 1]; } else { considered = this.layers[iconsidered = layer - 1]; } var presorted = considered.slice(0); if (down) { this.calcUpData(iconsidered); } else { this.calcDownData(iconsidered); } var that = this; considered.sort(function (n1, n2) { var n1BaryCenter = that.calcBaryCenter(n1), n2BaryCenter = that.calcBaryCenter(n2); if (Math.abs(n1BaryCenter - n2BaryCenter) < 0.0001) { if (n1.degree() === n2.degree()) { return that.compareByIndex(n1, n2); } else if (n1.degree() < n2.degree()) { return 1; } return -1; } var compareValue = (n2BaryCenter - n1BaryCenter) * 1000; if (compareValue > 0) { return -1; } else if (compareValue < 0) { return 1; } return that.compareByIndex(n1, n2); }); var i, moves = 0; for (i = 0; i < considered.length; i++) { if (considered[i] !== presorted[i]) { moves++; } } if (moves > 0) { var inode = 0; for (i = 0; i < considered.length; i++) { var node = considered[i]; node.gridPosition = inode++; } } return moves; }, _swapPairs: function () { var maxIterations = this.options.layeredIterations; var iter = 0; while (true) { if (iter++ > maxIterations) { break; } var downwards = iter % 4 <= 1; var secondPass = iter % 4 === 1; for (var l = downwards ? 0 : this.layers.length - 1; downwards ? l <= this.layers.length - 1 : l >= 0; l += downwards ? 1 : -1) { var layer = this.layers[l]; var hasSwapped = false; var calcCrossings = true; var memCrossings = 0; for (var n = 0; n < layer.length - 1; n++) { var up = 0; var down = 0; var crossBefore = 0; if (calcCrossings) { if (l !== 0) { up = this.countLinksCrossingBetweenTwoLayers(l - 1, l); } if (l !== this.layers.length - 1) { down = this.countLinksCrossingBetweenTwoLayers(l, l + 1); } if (downwards) { up *= 2; } else { down *= 2; } crossBefore = up + down; } else { crossBefore = memCrossings; } if (crossBefore === 0) { continue; } var node1 = layer[n]; var node2 = layer[n + 1]; var node1GridPos = node1.gridPosition; var node2GridPos = node2.gridPosition; layer[n] = node2; layer[n + 1] = node1; node1.gridPosition = node2GridPos; node2.gridPosition = node1GridPos; up = 0; if (l !== 0) { up = this.countLinksCrossingBetweenTwoLayers(l - 1, l); } down = 0; if (l !== this.layers.length - 1) { down = this.countLinksCrossingBetweenTwoLayers(l, l + 1); } if (downwards) { up *= 2; } else { down *= 2; } var crossAfter = up + down; var revert = false; if (secondPass) { revert = crossAfter >= crossBefore; } else { revert = crossAfter > crossBefore; } if (revert) { node1 = layer[n]; node2 = layer[n + 1]; node1GridPos = node1.gridPosition; node2GridPos = node2.gridPosition; layer[n] = node2; layer[n + 1] = node1; node1.gridPosition = node2GridPos; node2.gridPosition = node1GridPos; memCrossings = crossBefore; calcCrossings = false; } else { hasSwapped = true; calcCrossings = true; } } if (hasSwapped) { if (l !== this.layers.length - 1) { this.calcUpData(l + 1); } if (l !== 0) { this.calcDownData(l - 1); } } } } }, countLinksCrossingBetweenTwoLayers: function (ulayer, dlayer) { var links = this.layers[ulayer].linksTo[dlayer]; var link1, link2, n11, n12, n21, n22, l1, l2; var crossings = 0; var length = links.length; for (l1 = 0; l1 < length; l1++) { link1 = links[l1]; for (l2 = l1 + 1; l2 < length; l2++) { link2 = links[l2]; if (link1.target.layer === dlayer) { n11 = link1.source; n12 = link1.target; } else { n11 = link1.target; n12 = link1.source; } if (link2.target.layer === dlayer) { n21 = link2.source; n22 = link2.target; } else { n21 = link2.target; n22 = link2.source; } var n11gp = n11.gridPosition; var n12gp = n12.gridPosition; var n21gp = n21.gridPosition; var n22gp = n22.gridPosition; if ((n11gp - n21gp) * (n12gp - n22gp) < 0) { crossings++; } } } return crossings; }, calcBaryCenter: function (node) { var upstreamLinkCount = node.upstreamLinkCount; var downstreamLinkCount = node.downstreamLinkCount; var uBaryCenter = node.uBaryCenter; var dBaryCenter = node.dBaryCenter; if (upstreamLinkCount > 0 && downstreamLinkCount > 0) { return (uBaryCenter + dBaryCenter) / 2; } if (upstreamLinkCount > 0) { return uBaryCenter; } if (downstreamLinkCount > 0) { return dBaryCenter; } return 0; }, _gridPositionComparer: function (x, y) { if (x.gridPosition < y.gridPosition) { return -1; } if (x.gridPosition > y.gridPosition) { return 1; } return 0; }, _positionAscendingComparer: function (x, y) { return x.k < y.k ? -1 : x.k > y.k ? 1 : 0; }, _positionDescendingComparer: function (x, y) { return x.k < y.k ? 1 : x.k > y.k ? -1 : 0; }, _firstVirtualNode: function (layer) { for (var c = 0; c < layer.length; c++) { if (layer[c].isVirtual) { return c; } } return -1; }, compareByIndex: function (o1, o2) { var i1 = o1.index; var i2 = o2.index; if (i1 < i2) { return 1; } if (i1 > i2) { return -1; } return 0; }, intDiv: function (numerator, denominator) { return (numerator - numerator % denominator) / denominator; }, nextVirtualNode: function (layer, node) { var nodeIndex = node.layerIndex; for (var i = nodeIndex + 1; i < layer.length; ++i) { if (layer[i].isVirtual) { return layer[i]; } } return null; } }); var LayoutState = kendo.Class.extend({ init: function (diagram, graphOrNodes) { if (Utils.isUndefined(diagram)) { throw 'No diagram given'; } this.diagram = diagram; this.nodeMap = new Dictionary(); this.linkMap = new Dictionary(); this.capture(graphOrNodes ? graphOrNodes : diagram); }, capture: function (diagramOrGraphOrNodes) { var node, nodes, shape, i, conn, link, links; if (diagramOrGraphOrNodes instanceof diagram.Graph) { for (i = 0; i < diagramOrGraphOrNodes.nodes.length; i++) { node = diagramOrGraphOrNodes.nodes[i]; shape = node.associatedShape; this.nodeMap.set(shape.visual.id, new Rect(node.x, node.y, node.width, node.height)); } for (i = 0; i < diagramOrGraphOrNodes.links.length; i++) { link = diagramOrGraphOrNodes.links[i]; conn = link.associatedConnection; this.linkMap.set(conn.visual.id, link.points()); } } else if (diagramOrGraphOrNodes instanceof Array) { nodes = diagramOrGraphOrNodes; for (i = 0; i < nodes.length; i++) { node = nodes[i]; shape = node.associatedShape; if (shape) { this.nodeMap.set(shape.visual.id, new Rect(node.x, node.y, node.width, node.height)); } } } else if (diagramOrGraphOrNodes.hasOwnProperty('links') && diagramOrGraphOrNodes.hasOwnProperty('nodes')) { nodes = diagramOrGraphOrNodes.nodes; links = diagramOrGraphOrNodes.links; for (i = 0; i < nodes.length; i++) { node = nodes[i]; shape = node.associatedShape; if (shape) { this.nodeMap.set(shape.visual.id, new Rect(node.x, node.y, node.width, node.height)); } } for (i = 0; i < links.length; i++) { link = links[i]; conn = link.associatedConnection; if (conn) { this.linkMap.set(conn.visual.id, link.points); } } } else { var shapes = this.diagram.shapes; var connections = this.diagram.connections; for (i = 0; i < shapes.length; i++) { shape = shapes[i]; this.nodeMap.set(shape.visual.id, shape.bounds()); } for (i = 0; i < connections.length; i++) { conn = connections[i]; this.linkMap.set(conn.visual.id, conn.points()); } } } }); deepExtend(diagram, { init: function (element) { kendo.init(element, diagram.ui); }, SpringLayout: SpringLayout, TreeLayout: TreeLayout, GraphAdapter: DiagramToHyperTreeAdapter, LayeredLayout: LayeredLayout, LayoutBase: LayoutBase, LayoutState: LayoutState }); }(window.kendo.jQuery)); }, typeof define == 'function' && define.amd ? define : function (a1, a2, a3) { (a3 || a2)(); })); (function (f, define) { define('dataviz/diagram/dom', [ 'kendo.data', 'kendo.draganddrop', 'kendo.toolbar', 'kendo.editable', 'kendo.window', 'kendo.dropdownlist', 'kendo.dataviz.core', 'kendo.dataviz.themes', 'dataviz/diagram/svg', 'dataviz/diagram/services', 'dataviz/diagram/layout' ], f); }(function () { (function ($, undefined) { var dataviz = kendo.dataviz, draw = kendo.drawing, geom = kendo.geometry, diagram = dataviz.diagram, Widget = kendo.ui.Widget, Class = kendo.Class, proxy = $.proxy, deepExtend = kendo.deepExtend, extend = $.extend, HierarchicalDataSource = kendo.data.HierarchicalDataSource, Canvas = diagram.Canvas, Group = diagram.Group, Rectangle = diagram.Rectangle, Circle = diagram.Circle, CompositeTransform = diagram.CompositeTransform, Rect = diagram.Rect, Path = diagram.Path, DeleteShapeUnit = diagram.DeleteShapeUnit, DeleteConnectionUnit = diagram.DeleteConnectionUnit, TextBlock = diagram.TextBlock, Image = diagram.Image, Point = diagram.Point, Intersect = diagram.Intersect, ConnectionEditAdorner = diagram.ConnectionEditAdorner, UndoRedoService = diagram.UndoRedoService, ToolService = diagram.ToolService, Selector = diagram.Selector, ResizingAdorner = diagram.ResizingAdorner, ConnectorsAdorner = diagram.ConnectorsAdorner, Cursors = diagram.Cursors, Utils = diagram.Utils, Observable = kendo.Observable, ToBackUnit = diagram.ToBackUnit, ToFrontUnit = diagram.ToFrontUnit, PolylineRouter = diagram.PolylineRouter, CascadingRouter = diagram.CascadingRouter, isUndefined = Utils.isUndefined, isDefined = Utils.isDefined, defined = kendo.util.defined, isArray = $.isArray, isFunction = kendo.isFunction, isString = Utils.isString, isPlainObject = $.isPlainObject, math = Math; var NS = '.kendoDiagram', CASCADING = 'cascading', ITEMBOUNDSCHANGE = 'itemBoundsChange', CHANGE = 'change', CLICK = 'click', DRAG = 'drag', DRAG_END = 'dragEnd', DRAG_START = 'dragStart', MOUSE_ENTER = 'mouseEnter', MOUSE_LEAVE = 'mouseLeave', ERROR = 'error', AUTO = 'Auto', TOP = 'Top', RIGHT = 'Right', LEFT = 'Left', BOTTOM = 'Bottom', MAXINT = 9007199254740992, SELECT = 'select', ITEMROTATE = 'itemRotate', PAN = 'pan', ZOOM_START = 'zoomStart', ZOOM_END = 'zoomEnd', NONE = 'none', DEFAULT_CANVAS_WIDTH = 600, DEFAULT_CANVAS_HEIGHT = 600, DEFAULT_SHAPE_TYPE = 'rectangle', DEFAULT_SHAPE_WIDTH = 100, DEFAULT_SHAPE_HEIGHT = 100, DEFAULT_SHAPE_MINWIDTH = 20, DEFAULT_SHAPE_MINHEIGHT = 20, DEFAULT_SHAPE_POSITION = 0, DEFAULT_CONNECTION_BACKGROUND = 'Yellow', MAX_VALUE = Number.MAX_VALUE, MIN_VALUE = -Number.MAX_VALUE, ABSOLUTE = 'absolute', TRANSFORMED = 'transformed', ROTATED = 'rotated', TRANSPARENT = 'transparent', WIDTH = 'width', HEIGHT = 'height', X = 'x', Y = 'y', MOUSEWHEEL_NS = 'DOMMouseScroll' + NS + ' mousewheel' + NS, MOBILE_ZOOM_RATE = 0.05, MOBILE_PAN_DISTANCE = 5, BUTTON_TEMPLATE = '#=text#', CONNECTION_CONTENT_OFFSET = 5; diagram.DefaultConnectors = [ { name: TOP }, { name: BOTTOM }, { name: LEFT }, { name: RIGHT }, { name: AUTO, position: function (shape) { return shape.getPosition('center'); } } ]; var defaultButtons = { cancel: { text: 'Cancel', imageClass: 'k-cancel', className: 'k-diagram-cancel', iconClass: 'k-icon' }, update: { text: 'Update', imageClass: 'k-update', className: 'k-diagram-update', iconClass: 'k-icon' } }; diagram.shapeDefaults = function (extra) { var defaults = { type: DEFAULT_SHAPE_TYPE, path: '', autoSize: true, visual: null, x: DEFAULT_SHAPE_POSITION, y: DEFAULT_SHAPE_POSITION, minWidth: DEFAULT_SHAPE_MINWIDTH, minHeight: DEFAULT_SHAPE_MINHEIGHT, width: DEFAULT_SHAPE_WIDTH, height: DEFAULT_SHAPE_HEIGHT, hover: {}, editable: { connect: true, tools: [] }, connectors: diagram.DefaultConnectors, rotation: { angle: 0 } }; Utils.simpleExtend(defaults, extra); return defaults; }; function mwDelta(e) { var origEvent = e.originalEvent, delta = 0; if (origEvent.wheelDelta) { delta = -origEvent.wheelDelta / 40; delta = delta > 0 ? math.ceil(delta) : math.floor(delta); } else if (origEvent.detail) { delta = origEvent.detail; } return delta; } function isAutoConnector(connector) { return connector.options.name.toLowerCase() === AUTO.toLowerCase(); } function closestConnector(point, shape) { var minimumDistance = MAXINT, resCtr, ctrs = shape.connectors; for (var i = 0; i < ctrs.length; i++) { var ctr = ctrs[i]; if (!isAutoConnector(ctr)) { var dist = point.distanceTo(ctr.position()); if (dist < minimumDistance) { minimumDistance = dist; resCtr = ctr; } } } return resCtr; } function indicesOfItems(group, visuals) { var i, indices = [], visual; var children = group.drawingContainer().children; var length = children.length; for (i = 0; i < visuals.length; i++) { visual = visuals[i]; for (var j = 0; j < length; j++) { if (children[j] == visual.drawingContainer()) { indices.push(j); break; } } } return indices; } var DiagramElement = Observable.extend({ init: function (options) { var that = this; that.dataItem = (options || {}).dataItem; Observable.fn.init.call(that); that.options = deepExtend({ id: diagram.randomId() }, that.options, options); that.isSelected = false; that.visual = new Group({ id: that.options.id, autoSize: that.options.autoSize }); that.id = that.options.id; that._template(); }, options: { hover: {}, cursor: Cursors.grip, content: { align: 'center middle' }, selectable: true, serializable: true, enable: true }, _getCursor: function (point) { if (this.adorner) { return this.adorner._getCursor(point); } return this.options.cursor; }, visible: function (value) { if (isUndefined(value)) { return this.visual.visible(); } else { this.visual.visible(value); } }, bounds: function () { }, refresh: function () { this.visual.redraw(); }, position: function (point) { this.options.x = point.x; this.options.y = point.y; this.visual.position(point); }, toString: function () { return this.options.id; }, serialize: function () { var json = deepExtend({}, { options: this.options }); if (this.dataItem) { json.dataItem = this.dataItem.toString(); } return json; }, _content: function (content) { if (content !== undefined) { var options = this.options; if (diagram.Utils.isString(content)) { options.content.text = content; } else { deepExtend(options.content, content); } var contentOptions = options.content; var contentVisual = this._contentVisual; if (!contentVisual) { this._createContentVisual(contentOptions); } else { this._updateContentVisual(contentOptions); } } return this.options.content.text; }, _createContentVisual: function (options) { if (options.text) { this._contentVisual = new TextBlock(options); this._contentVisual._includeInBBox = false; this.visual.append(this._contentVisual); } }, _updateContentVisual: function (options) { this._contentVisual.redraw(options); }, _hitTest: function (point) { var bounds = this.bounds(); return this.visible() && bounds.contains(point) && this.options.enable; }, _template: function () { var that = this; if (that.options.content.template) { var data = that.dataItem || {}, elementTemplate = kendo.template(that.options.content.template, { paramName: 'dataItem' }); that.options.content.text = elementTemplate(data); } }, _canSelect: function () { return this.options.selectable !== false; }, toJSON: function () { return { id: this.options.id }; } }); var Connector = Class.extend({ init: function (shape, options) { this.options = deepExtend({}, this.options, options); this.connections = []; this.shape = shape; }, options: { width: 7, height: 7, fill: { color: DEFAULT_CONNECTION_BACKGROUND }, hover: {} }, position: function () { if (this.options.position) { return this.options.position(this.shape); } else { return this.shape.getPosition(this.options.name); } }, toJSON: function () { return { shapeId: this.shape.toString(), connector: this.options.name }; } }); Connector.parse = function (diagram, str) { var tempStr = str.split(':'), id = tempStr[0], name = tempStr[1] || AUTO; for (var i = 0; i < diagram.shapes.length; i++) { var shape = diagram.shapes[i]; if (shape.options.id == id) { return shape.getConnector(name.trim()); } } }; var Shape = DiagramElement.extend({ init: function (options, diagram) { var that = this; DiagramElement.fn.init.call(that, options); this.diagram = diagram; this.updateOptionsFromModel(); options = that.options; that.connectors = []; that.type = options.type; that.createShapeVisual(); that.updateBounds(); that.content(that.content()); that._createConnectors(); }, options: diagram.shapeDefaults(), _setOptionsFromModel: function (model) { var modelOptions = filterShapeDataItem(model || this.dataItem); this.options = deepExtend({}, this.options, modelOptions); this.redrawVisual(); if (this.options.content) { this._template(); this.content(this.options.content); } }, updateOptionsFromModel: function (model, field) { if (this.diagram && this.diagram._isEditable) { var modelOptions = filterShapeDataItem(model || this.dataItem); if (model && field) { if (!dataviz.inArray(field, [ 'x', 'y', 'width', 'height' ])) { if (this.options.visual) { this.redrawVisual(); } else if (modelOptions.type) { this.options = deepExtend({}, this.options, modelOptions); this.redrawVisual(); } if (this.options.content) { this._template(); this.content(this.options.content); } } else { var bounds = this.bounds(); bounds[field] = model[field]; this.bounds(bounds); } } else { this.options = deepExtend({}, this.options, modelOptions); } } }, redrawVisual: function () { this.visual.clear(); this._contentVisual = null; this.options.dataItem = this.dataItem; this.createShapeVisual(); this.updateBounds(); }, updateModel: function (syncChanges) { var diagram = this.diagram; if (diagram && diagram._isEditable) { var bounds = this._bounds; var model = this.dataItem; if (model) { diagram._suspendModelRefresh(); if (defined(model.x) && bounds.x !== model.x) { model.set('x', bounds.x); } if (defined(model.y) && bounds.y !== model.y) { model.set('y', bounds.y); } if (defined(model.width) && bounds.width !== model.width) { model.set('width', bounds.width); } if (defined(model.height) && bounds.height !== model.height) { model.set('height', bounds.height); } this.dataItem = model; diagram._resumeModelRefresh(); if (syncChanges) { diagram._syncShapeChanges(); } } } }, updateBounds: function () { var bounds = this.visual._measure(true); var options = this.options; this.bounds(new Rect(options.x, options.y, bounds.width, bounds.height)); this._rotate(); this._alignContent(); }, content: function (content) { var result = this._content(content); this._alignContent(); return result; }, _alignContent: function () { var contentOptions = this.options.content || {}; var contentVisual = this._contentVisual; if (contentVisual && contentOptions.align) { var containerRect = this.visual._measure(); var aligner = new diagram.RectAlign(containerRect); var contentBounds = contentVisual.drawingElement.bbox(null); var contentRect = new Rect(0, 0, contentBounds.width(), contentBounds.height()); var alignedBounds = aligner.align(contentRect, contentOptions.align); contentVisual.position(alignedBounds.topLeft()); } }, _createConnectors: function () { var options = this.options, length = options.connectors.length, connectorDefaults = options.connectorDefaults, connector, i; for (i = 0; i < length; i++) { connector = new Connector(this, deepExtend({}, connectorDefaults, options.connectors[i])); this.connectors.push(connector); } }, bounds: function (value) { var bounds; if (value) { if (isString(value)) { switch (value) { case TRANSFORMED: bounds = this._transformedBounds(); break; case ABSOLUTE: bounds = this._transformedBounds(); var pan = this.diagram._pan; bounds.x += pan.x; bounds.y += pan.y; break; case ROTATED: bounds = this._rotatedBounds(); break; default: bounds = this._bounds; } } else { this._setBounds(value); this._triggerBoundsChange(); if (!(this.diagram && this.diagram._layouting)) { this.refreshConnections(); } } } else { bounds = this._bounds; } return bounds; }, _setBounds: function (rect) { var options = this.options; var topLeft = rect.topLeft(); var x = options.x = topLeft.x; var y = options.y = topLeft.y; var width = options.width = math.max(rect.width, options.minWidth); var height = options.height = math.max(rect.height, options.minHeight); this._bounds = new Rect(x, y, width, height); this.visual.redraw({ x: x, y: y, width: width, height: height }); }, position: function (point) { if (point) { this.bounds(new Rect(point.x, point.y, this._bounds.width, this._bounds.height)); } else { return this._bounds.topLeft(); } }, clone: function () { var json = this.serialize(); json.options.id = diagram.randomId(); if (this.diagram && this.diagram._isEditable && defined(this.dataItem)) { json.options.dataItem = cloneDataItem(this.dataItem); } return new Shape(json.options); }, select: function (value) { var diagram = this.diagram, selected, deselected; if (isUndefined(value)) { value = true; } if (this._canSelect()) { if (this.isSelected != value) { selected = []; deselected = []; this.isSelected = value; if (this.isSelected) { diagram._selectedItems.push(this); selected.push(this); } else { Utils.remove(diagram._selectedItems, this); deselected.push(this); } if (!diagram._internalSelection) { diagram._selectionChanged(selected, deselected); } return true; } } }, rotate: function (angle, center, undoable) { var rotate = this.visual.rotate(); if (angle !== undefined) { if (undoable !== false && this.diagram && this.diagram.undoRedoService && angle !== rotate.angle) { this.diagram.undoRedoService.add(new diagram.RotateUnit(this.diagram._resizingAdorner, [this], [rotate.angle]), false); } var b = this.bounds(), sc = new Point(b.width / 2, b.height / 2), deltaAngle, newPosition; if (center) { deltaAngle = angle - rotate.angle; newPosition = b.center().rotate(center, 360 - deltaAngle).minus(sc); this._rotationOffset = this._rotationOffset.plus(newPosition.minus(b.topLeft())); this.position(newPosition); } this.visual.rotate(angle, sc); this.options.rotation.angle = angle; if (this.diagram && this.diagram._connectorsAdorner) { this.diagram._connectorsAdorner.refresh(); } this.refreshConnections(); if (this.diagram) { this.diagram.trigger(ITEMROTATE, { item: this }); } } return rotate; }, connections: function (type) { var result = [], i, j, con, cons, ctr; for (i = 0; i < this.connectors.length; i++) { ctr = this.connectors[i]; cons = ctr.connections; for (j = 0, cons; j < cons.length; j++) { con = cons[j]; if (type == 'out') { var source = con.source(); if (source.shape && source.shape == this) { result.push(con); } } else if (type == 'in') { var target = con.target(); if (target.shape && target.shape == this) { result.push(con); } } else { result.push(con); } } } return result; }, refreshConnections: function () { $.each(this.connections(), function () { this.refresh(); }); }, getConnector: function (nameOrPoint) { var i, ctr; if (isString(nameOrPoint)) { nameOrPoint = nameOrPoint.toLocaleLowerCase(); for (i = 0; i < this.connectors.length; i++) { ctr = this.connectors[i]; if (ctr.options.name.toLocaleLowerCase() == nameOrPoint) { return ctr; } } } else if (nameOrPoint instanceof Point) { return closestConnector(nameOrPoint, this); } else { return this.connectors.length ? this.connectors[0] : null; } }, getPosition: function (side) { var b = this.bounds(), fnName = side.charAt(0).toLowerCase() + side.slice(1); if (isFunction(b[fnName])) { return this._transformPoint(b[fnName]()); } return b.center(); }, redraw: function (options) { if (options) { var shapeOptions = this.options; var boundsChange; this.shapeVisual.redraw(this._visualOptions(options)); if (this._diffNumericOptions(options, [ WIDTH, HEIGHT, X, Y ])) { this.bounds(new Rect(shapeOptions.x, shapeOptions.y, shapeOptions.width, shapeOptions.height)); boundsChange = true; } if (options.connectors) { shapeOptions.connectors = options.connectors; this._updateConnectors(); } shapeOptions = deepExtend(shapeOptions, options); if (options.rotation || boundsChange) { this._rotate(); } if (shapeOptions.content) { this.content(shapeOptions.content); } } }, _updateConnectors: function () { var connections = this.connections(); this.connectors = []; this._createConnectors(); var connection; var source; var target; for (var idx = 0; idx < connections.length; idx++) { connection = connections[idx]; source = connection.source(); target = connection.target(); if (source.shape && source.shape === this) { connection.source(this.getConnector(source.options.name) || null); } else if (target.shape && target.shape === this) { connection.target(this.getConnector(target.options.name) || null); } connection.updateModel(); } }, _diffNumericOptions: diagram.diffNumericOptions, _visualOptions: function (options) { return { data: options.path, source: options.source, hover: options.hover, fill: options.fill, stroke: options.stroke }; }, _triggerBoundsChange: function () { if (this.diagram) { this.diagram.trigger(ITEMBOUNDSCHANGE, { item: this, bounds: this._bounds.clone() }); } }, _transformPoint: function (point) { var rotate = this.rotate(), bounds = this.bounds(), tl = bounds.topLeft(); if (rotate.angle) { point.rotate(rotate.center().plus(tl), 360 - rotate.angle); } return point; }, _transformedBounds: function () { var bounds = this.bounds(), tl = bounds.topLeft(), br = bounds.bottomRight(); return Rect.fromPoints(this.diagram.modelToView(tl), this.diagram.modelToView(br)); }, _rotatedBounds: function () { var bounds = this.bounds().rotatedBounds(this.rotate().angle), tl = bounds.topLeft(), br = bounds.bottomRight(); return Rect.fromPoints(tl, br); }, _rotate: function () { var rotation = this.options.rotation; if (rotation && rotation.angle) { this.rotate(rotation.angle); } this._rotationOffset = new Point(); }, _hover: function (value) { var options = this.options, hover = options.hover, stroke = options.stroke, fill = options.fill; if (value && isDefined(hover.stroke)) { stroke = deepExtend({}, stroke, hover.stroke); } if (value && isDefined(hover.fill)) { fill = hover.fill; } this.shapeVisual.redraw({ stroke: stroke, fill: fill }); if (options.editable && options.editable.connect) { this.diagram._showConnectors(this, value); } }, _hitTest: function (value) { if (this.visible()) { var bounds = this.bounds(), rotatedPoint, angle = this.rotate().angle; if (value.isEmpty && !value.isEmpty()) { return Intersect.rects(value, bounds, angle ? angle : 0); } else { rotatedPoint = value.clone().rotate(bounds.center(), angle); if (bounds.contains(rotatedPoint)) { return this; } } } }, toJSON: function () { return { shapeId: this.options.id }; }, createShapeVisual: function () { var options = this.options; var visualOptions = this._visualOptions(options); var visualTemplate = options.visual; var type = (options.type + '').toLocaleLowerCase(); var shapeVisual; visualOptions.width = options.width; visualOptions.height = options.height; if (isFunction(visualTemplate)) { shapeVisual = visualTemplate.call(this, options); } else if (visualOptions.data) { shapeVisual = new Path(visualOptions); translateToOrigin(shapeVisual); } else if (type == 'rectangle') { shapeVisual = new Rectangle(visualOptions); } else if (type == 'circle') { shapeVisual = new Circle(visualOptions); } else if (type == 'text') { shapeVisual = new TextBlock(visualOptions); } else if (type == 'image') { shapeVisual = new Image(visualOptions); } else { shapeVisual = new Path(visualOptions); } this.shapeVisual = shapeVisual; this.visual.append(this.shapeVisual); } }); var Connection = DiagramElement.extend({ init: function (from, to, options) { var that = this; DiagramElement.fn.init.call(that, options); this.updateOptionsFromModel(); this._initRouter(); that.path = new diagram.Polyline(that.options); that.path.fill(TRANSPARENT); that.visual.append(that.path); that._sourcePoint = that._targetPoint = new Point(); that._setSource(from); that._setTarget(to); that.content(that.options.content); that.definers = []; if (defined(options) && options.points) { that.points(options.points); } }, options: { hover: { stroke: {} }, startCap: NONE, endCap: NONE, points: [], selectable: true, fromConnector: AUTO, toConenctor: AUTO }, _setOptionsFromModel: function (model) { this.updateOptionsFromModel(model || this.dataItem); }, updateOptionsFromModel: function (model) { if (this.diagram && this.diagram._isEditable) { var dataMap = this.diagram._dataMap; var options = filterConnectionDataItem(model || this.dataItem); if (model) { if (defined(options.from)) { var from = dataMap[options.from]; if (from && defined(options.fromConnector)) { from = from.getConnector(options.fromConnector); } this.source(from); } else if (defined(options.fromX) && defined(options.fromY)) { this.source(new Point(options.fromX, options.fromY)); } if (defined(options.to)) { var to = dataMap[options.to]; if (to && defined(options.toConnector)) { to = to.getConnector(options.toConnector); } this.target(to); } else if (defined(options.toX) && defined(options.toY)) { this.target(new Point(options.toX, options.toY)); } if (defined(options.type) && this.type() !== options.type) { this.points([]); this.type(options.type); } this.dataItem = model; this._template(); this.redraw(this.options); } else { this.options = deepExtend({}, options, this.options); } } }, updateModel: function (syncChanges) { if (this.diagram && this.diagram._isEditable) { if (this.diagram.connectionsDataSource) { var model = this.diagram.connectionsDataSource.getByUid(this.dataItem.uid); if (model) { this.diagram._suspendModelRefresh(); if (defined(this.options.fromX) && this.options.fromX !== null) { clearField('from', model); clearField('fromConnector', model); model.set('fromX', this.options.fromX); model.set('fromY', this.options.fromY); } else { model.set('from', this.options.from); if (defined(model.fromConnector)) { model.set('fromConnector', this.sourceConnector ? this.sourceConnector.options.name : null); } clearField('fromX', model); clearField('fromY', model); } if (defined(this.options.toX) && this.options.toX !== null) { clearField('to', model); clearField('toConnector', model); model.set('toX', this.options.toX); model.set('toY', this.options.toY); } else { model.set('to', this.options.to); if (defined(model.toConnector)) { model.set('toConnector', this.targetConnector ? this.targetConnector.options.name : null); } clearField('toX', model); clearField('toY', model); } if (defined(this.options.type) && defined(model.type)) { model.set('type', this.options.type); } this.dataItem = model; this.diagram._resumeModelRefresh(); if (syncChanges) { this.diagram._syncConnectionChanges(); } } } } }, sourcePoint: function () { return this._resolvedSourceConnector ? this._resolvedSourceConnector.position() : this._sourcePoint; }, _setSource: function (source) { var shapeSource = source instanceof Shape; var defaultConnector = this.options.fromConnector || AUTO; var dataItem; if (shapeSource && !source.getConnector(defaultConnector)) { return; } if (source !== undefined) { this.from = source; } this._removeFromSourceConnector(); if (source === null) { if (this.sourceConnector) { this._sourcePoint = (this._resolvedSourceConnector || this.sourceConnector).position(); this._clearSourceConnector(); this._setFromOptions(null, this._sourcePoint); } } else if (source instanceof Connector) { dataItem = source.shape.dataItem; if (dataItem) { this._setFromOptions(dataItem.id); } this.sourceConnector = source; this.sourceConnector.connections.push(this); } else if (source instanceof Point) { this._setFromOptions(null, source); this._sourcePoint = source; if (this.sourceConnector) { this._clearSourceConnector(); } } else if (shapeSource) { dataItem = source.dataItem; if (dataItem) { this._setFromOptions(dataItem.id); } this.sourceConnector = source.getConnector(defaultConnector); this.sourceConnector.connections.push(this); } }, source: function (source, undoable) { if (isDefined(source)) { if (undoable && this.diagram) { this.diagram.undoRedoService.addCompositeItem(new diagram.ConnectionEditUnit(this, source)); } this._setSource(source); this.refresh(); } return this.sourceConnector ? this.sourceConnector : this._sourcePoint; }, _setFromOptions: function (from, fromPoint) { this.options.from = from; if (fromPoint) { this.options.fromX = fromPoint.x; this.options.fromY = fromPoint.y; } else { this.options.fromX = null; this.options.fromY = null; } }, sourceDefiner: function (value) { if (value) { if (value instanceof diagram.PathDefiner) { value.left = null; this._sourceDefiner = value; this.source(value.point); } else { throw 'The sourceDefiner needs to be a PathDefiner.'; } } else { if (!this._sourceDefiner) { this._sourceDefiner = new diagram.PathDefiner(this.sourcePoint(), null, null); } return this._sourceDefiner; } }, targetPoint: function () { return this._resolvedTargetConnector ? this._resolvedTargetConnector.position() : this._targetPoint; }, _setTarget: function (target) { var shapeTarget = target instanceof Shape; var defaultConnector = this.options.toConnector || AUTO; var dataItem; if (shapeTarget && !target.getConnector(defaultConnector)) { return; } if (target !== undefined) { this.to = target; } this._removeFromTargetConnector(); if (target === null) { if (this.targetConnector) { this._targetPoint = (this._resolvedTargetConnector || this.targetConnector).position(); this._clearTargetConnector(); this._setToOptions(null, this._targetPoint); } } else if (target instanceof Connector) { dataItem = target.shape.dataItem; if (dataItem) { this._setToOptions(dataItem.id); } this.targetConnector = target; this.targetConnector.connections.push(this); } else if (target instanceof Point) { this._setToOptions(null, target); this._targetPoint = target; if (this.targetConnector) { this._clearTargetConnector(); } } else if (shapeTarget) { dataItem = target.dataItem; if (dataItem) { this._setToOptions(dataItem.id); } this.targetConnector = target.getConnector(defaultConnector); this.targetConnector.connections.push(this); } }, target: function (target, undoable) { if (isDefined(target)) { if (undoable && this.diagram) { this.diagram.undoRedoService.addCompositeItem(new diagram.ConnectionEditUnit(this, undefined, target)); } this._setTarget(target); this.refresh(); } return this.targetConnector ? this.targetConnector : this._targetPoint; }, _setToOptions: function (to, toPoint) { this.options.to = to; if (toPoint) { this.options.toX = toPoint.x; this.options.toY = toPoint.y; } else { this.options.toX = null; this.options.toY = null; } }, targetDefiner: function (value) { if (value) { if (value instanceof diagram.PathDefiner) { value.right = null; this._targetDefiner = value; this.target(value.point); } else { throw 'The sourceDefiner needs to be a PathDefiner.'; } } else { if (!this._targetDefiner) { this._targetDefiner = new diagram.PathDefiner(this.targetPoint(), null, null); } return this._targetDefiner; } }, _updateConnectors: function () { this._updateConnector(this.source(), 'source'); this._updateConnector(this.target(), 'target'); }, _updateConnector: function (instance, name) { var that = this; var diagram = that.diagram; if (instance instanceof Connector && !diagram.getShapeById(instance.shape.id)) { var dataItem = instance.shape.dataItem; var connectorName = instance.options.name; var setNewTarget = function () { var shape = diagram._dataMap[dataItem.id]; instance = shape.getConnector(connectorName); that[name](instance, false); that.updateModel(); }; if (diagram._dataMap[dataItem.id]) { setNewTarget(); } else { var inactiveItem = diagram._inactiveShapeItems.getByUid(dataItem.uid); if (inactiveItem) { diagram._deferredConnectionUpdates.push(inactiveItem.onActivate(setNewTarget)); } } } else { that[name](instance, false); } }, content: function (content) { var result = this._content(content); if (defined(content)) { this._alignContent(); } return result; }, _createContentVisual: function (options) { var visual; if (isFunction(options.visual)) { visual = options.visual.call(this, options); } else if (options.text) { visual = new TextBlock(options); } if (visual) { this._contentVisual = visual; visual._includeInBBox = false; this.visual.append(visual); } return visual; }, _updateContentVisual: function (options) { if (isFunction(options.visual)) { this.visual.remove(this._contentVisual); this._createContentVisual(options); } else { this._contentVisual.redraw(options); } }, _alignContent: function () { if (this._contentVisual) { var offset = CONNECTION_CONTENT_OFFSET; var points = this.allPoints(); var endIdx = math.floor(points.length / 2); var startIdx = endIdx - 1; while (startIdx > 0 && points[startIdx].equals(points[endIdx])) { startIdx--; endIdx++; } var endPoint = points[endIdx]; var startPoint = points[startIdx]; var boundingBox = this._contentVisual._measure(); var width = boundingBox.width; var height = boundingBox.height; var alignToPath = points.length % 2 === 0; var distance = startPoint.distanceTo(endPoint); if (alignToPath && points.length > 2 && distance > 0 && (startPoint.y === endPoint.y && distance < width || startPoint.x === endPoint.x && distance < height)) { alignToPath = false; offset = 0; } var point; if (alignToPath) { var angle = kendo.util.deg(math.atan2(endPoint.y - startPoint.y, endPoint.x - startPoint.x)); point = new Point((endPoint.x - startPoint.x) / 2 + startPoint.x, (endPoint.y - startPoint.y) / 2 + startPoint.y); if (math.abs(angle) === 90) { point.x += offset; point.y -= height / 2; } else if (angle % 180 === 0) { point.x -= width / 2; point.y -= height + offset; } else if (angle < -90 || 0 < angle && angle < 90) { point.y -= height; } else if (angle < 0 || angle > 90) { point.x -= width; point.y -= height; } } else { var midIdx = math.floor(points.length / 2); point = points[midIdx].clone(); startPoint = points[midIdx - 1]; endPoint = points[midIdx + 1]; var offsetX = startPoint.x <= point.x && endPoint.x <= point.x ? offset : -boundingBox.width - offset; var offsetY = startPoint.y <= point.y && endPoint.y <= point.y ? offset : -boundingBox.height - offset; point.x += offsetX; point.y += offsetY; } this._contentVisual.position(point); } }, select: function (value) { var diagram = this.diagram, selected, deselected; if (this._canSelect()) { if (this.isSelected !== value) { this.isSelected = value; selected = []; deselected = []; if (this.isSelected) { this.adorner = new ConnectionEditAdorner(this, this.options.selection); diagram._adorn(this.adorner, true); diagram._selectedItems.push(this); selected.push(this); } else { if (this.adorner) { diagram._adorn(this.adorner, false); Utils.remove(diagram._selectedItems, this); this.adorner = undefined; deselected.push(this); } } if (this.adorner) { this.adorner.refresh(); } if (!diagram._internalSelection) { diagram._selectionChanged(selected, deselected); } return true; } } }, bounds: function (value) { if (value && !isString(value)) { this._bounds = value; } else { return this._bounds; } }, type: function (value) { var options = this.options; if (value) { if (value !== options.type) { options.type = value; this._initRouter(); this.refresh(); } } else { return options.type; } }, _initRouter: function () { var type = (this.options.type || '').toLowerCase(); if (type == CASCADING) { this._router = new CascadingRouter(this); } else { this._router = new PolylineRouter(this); } }, points: function (value) { if (value) { this.definers = []; for (var i = 0; i < value.length; i++) { var definition = value[i]; if (definition instanceof diagram.Point) { this.definers.push(new diagram.PathDefiner(definition)); } else if (definition.hasOwnProperty('x') && definition.hasOwnProperty('y')) { this.definers.push(new diagram.PathDefiner(new Point(definition.x, definition.y))); } else { throw 'A Connection point needs to be a Point or an object with x and y properties.'; } } } else { var pts = []; if (isDefined(this.definers)) { for (var k = 0; k < this.definers.length; k++) { pts.push(this.definers[k].point); } } return pts; } }, allPoints: function () { var pts = [this.sourcePoint()]; if (this.definers) { for (var k = 0; k < this.definers.length; k++) { pts.push(this.definers[k].point); } } pts.push(this.targetPoint()); return pts; }, refresh: function () { this._resolveConnectors(); this._refreshPath(); this._alignContent(); if (this.adorner) { this.adorner.refresh(); } }, _resolveConnectors: function () { var connection = this, sourcePoint, targetPoint, source = connection.source(), target = connection.target(), autoSourceShape, autoTargetShape; if (source instanceof Point) { sourcePoint = source; } else if (source instanceof Connector) { if (isAutoConnector(source)) { autoSourceShape = source.shape; } else { connection._resolvedSourceConnector = source; sourcePoint = source.position(); } } if (target instanceof Point) { targetPoint = target; } else if (target instanceof Connector) { if (isAutoConnector(target)) { autoTargetShape = target.shape; } else { connection._resolvedTargetConnector = target; targetPoint = target.position(); } } if (sourcePoint) { if (autoTargetShape) { connection._resolvedTargetConnector = closestConnector(sourcePoint, autoTargetShape); } } else if (autoSourceShape) { if (targetPoint) { connection._resolvedSourceConnector = closestConnector(targetPoint, autoSourceShape); } else if (autoTargetShape) { this._resolveAutoConnectors(autoSourceShape, autoTargetShape); } } }, _resolveAutoConnectors: function (autoSourceShape, autoTargetShape) { var minNonConflict = MAXINT; var minDist = MAXINT; var sourceConnectors = autoSourceShape.connectors; var targetConnectors; var minNonConflictSource, minNonConflictTarget; var sourcePoint, targetPoint; var minSource, minTarget; var sourceConnector, targetConnector; var sourceIdx, targetIdx; var dist; for (sourceIdx = 0; sourceIdx < sourceConnectors.length; sourceIdx++) { sourceConnector = sourceConnectors[sourceIdx]; if (!isAutoConnector(sourceConnector)) { sourcePoint = sourceConnector.position(); targetConnectors = autoTargetShape.connectors; for (targetIdx = 0; targetIdx < targetConnectors.length; targetIdx++) { targetConnector = targetConnectors[targetIdx]; if (!isAutoConnector(targetConnector)) { targetPoint = targetConnector.position(); dist = math.round(sourcePoint.distanceTo(targetPoint)); if (dist < minNonConflict && this.diagram && this._testRoutePoints(sourcePoint, targetPoint, sourceConnector, targetConnector)) { minNonConflict = dist; minNonConflictSource = sourceConnector; minNonConflictTarget = targetConnector; } if (dist < minDist) { minSource = sourceConnector; minTarget = targetConnector; minDist = dist; } } } } } if (minNonConflictSource) { minSource = minNonConflictSource; minTarget = minNonConflictTarget; } this._resolvedSourceConnector = minSource; this._resolvedTargetConnector = minTarget; }, _testRoutePoints: function (sourcePoint, targetPoint, sourceConnector, targetConnector) { var router = this._router; var passRoute = true; if (router instanceof CascadingRouter) { var points = router.routePoints(sourcePoint, targetPoint, sourceConnector, targetConnector), start, end, rect; points.unshift(sourcePoint); points.push(targetPoint); for (var idx = 1; idx < points.length; idx++) { start = points[idx - 1]; end = points[idx]; rect = new Rect(math.min(start.x, end.x), math.min(start.y, end.y), math.abs(start.x - end.x), math.abs(start.y - end.y)); if (rect.width > 0) { rect.x++; rect.width -= 2; } if (rect.height > 0) { rect.y++; rect.height -= 2; } if (!rect.isEmpty() && this.diagram._shapesQuadTree.hitTestRect(rect)) { passRoute = false; break; } } } return passRoute; }, redraw: function (options) { if (options) { this.options = deepExtend({}, this.options, options); var points = this.options.points; if (defined(points) && points.length > 0) { this.points(points); this._refreshPath(); } if (options && options.content || options.text) { this.content(options.content); } this.path.redraw({ fill: options.fill, stroke: options.stroke, startCap: options.startCap, endCap: options.endCap }); } }, clone: function () { var json = this.serialize(); if (this.diagram && this.diagram._isEditable && defined(this.dataItem)) { json.options.dataItem = cloneDataItem(this.dataItem); } return new Connection(this.from, this.to, json.options); }, serialize: function () { var from = this.from.toJSON ? this.from.toJSON : this.from.toString(), to = this.to.toJSON ? this.to.toJSON : this.to.toString(); var json = deepExtend({}, { options: this.options, from: from, to: to }); if (defined(this.dataItem)) { json.dataItem = this.dataItem.toString(); } json.options.points = this.points(); return json; }, _hitTest: function (value) { if (this.visible()) { var p = new Point(value.x, value.y), from = this.sourcePoint(), to = this.targetPoint(); if (value.isEmpty && !value.isEmpty() && value.contains(from) && value.contains(to)) { return this; } if (this._router.hitTest(p)) { return this; } } }, _hover: function (value) { var color = (this.options.stroke || {}).color; if (value && isDefined(this.options.hover.stroke.color)) { color = this.options.hover.stroke.color; } this.path.redraw({ stroke: { color: color } }); }, _refreshPath: function () { if (!defined(this.path)) { return; } this._drawPath(); this.bounds(this._router.getBounds()); }, _drawPath: function () { if (this._router) { this._router.route(); } var source = this.sourcePoint(); var target = this.targetPoint(); var points = this.points(); this.path.redraw({ points: [source].concat(points, [target]) }); }, _clearSourceConnector: function () { this.sourceConnector = undefined; this._resolvedSourceConnector = undefined; }, _clearTargetConnector: function () { this.targetConnector = undefined; this._resolvedTargetConnector = undefined; }, _removeFromSourceConnector: function () { if (this.sourceConnector) { Utils.remove(this.sourceConnector.connections, this); } }, _removeFromTargetConnector: function () { if (this.targetConnector) { Utils.remove(this.targetConnector.connections, this); } }, toJSON: function () { var connection = this; var from, to, point; if (connection.from && connection.from.toJSON) { from = connection.from.toJSON(); } else { point = connection._sourcePoint; from = { x: point.x, y: point.y }; } if (connection.to && connection.to.toJSON) { to = connection.to.toJSON(); } else { point = connection._targetPoint; to = { x: point.x, y: point.y }; } return { from: from, to: to }; } }); var Diagram = Widget.extend({ init: function (element, userOptions) { var that = this; kendo.destroy(element); Widget.fn.init.call(that, element, userOptions); that._initTheme(); that._initElements(); that._extendLayoutOptions(that.options); that._initDefaults(userOptions); that._initCanvas(); that.mainLayer = new Group({ id: 'main-layer' }); that.canvas.append(that.mainLayer); that._shapesQuadTree = new ShapesQuadTree(that); that._pan = new Point(); that._adorners = []; that.adornerLayer = new Group({ id: 'adorner-layer' }); that.canvas.append(that.adornerLayer); that._createHandlers(); that._initialize(); that._fetchFreshData(); that._createGlobalToolBar(); that._resizingAdorner = new ResizingAdorner(that, { editable: that.options.editable }); that._connectorsAdorner = new ConnectorsAdorner(that); that._adorn(that._resizingAdorner, true); that._adorn(that._connectorsAdorner, true); that.selector = new Selector(that); that._clipboard = []; that.pauseMouseHandlers = false; that._createOptionElements(); that.zoom(that.options.zoom); that.canvas.draw(); }, options: { name: 'Diagram', theme: 'default', layout: '', zoomRate: 0.1, zoom: 1, zoomMin: 0, zoomMax: 2, dataSource: {}, draggable: true, template: '', autoBind: true, editable: { rotate: {}, resize: {}, text: true, tools: [], drag: { snap: { size: 10, angle: 10 } }, remove: true }, pannable: { key: 'ctrl' }, selectable: { key: 'none' }, tooltip: { enabled: true, format: '{0}' }, copy: { enabled: true, offsetX: 20, offsetY: 20 }, shapeDefaults: diagram.shapeDefaults({ undoable: true }), connectionDefaults: { editable: { tools: [] }, type: CASCADING }, shapes: [], connections: [] }, events: [ ZOOM_END, ZOOM_START, PAN, SELECT, ITEMROTATE, ITEMBOUNDSCHANGE, CHANGE, CLICK, MOUSE_ENTER, MOUSE_LEAVE, 'toolBarClick', 'save', 'cancel', 'edit', 'remove', 'add', 'dataBound', DRAG_START, DRAG, DRAG_END ], items: function () { return $(); }, _createGlobalToolBar: function () { var editable = this.options.editable; if (editable) { var tools = editable.tools; if (this._isEditable && tools !== false && (!tools || tools.length === 0)) { tools = [ 'createShape', 'undo', 'redo', 'rotateClockwise', 'rotateAnticlockwise' ]; } if (tools && tools.length) { this.toolBar = new DiagramToolBar(this, { tools: tools || {}, click: proxy(this._toolBarClick, this), modal: false }); this.toolBar.element.css({ textAlign: 'left' }); this.element.prepend(this.toolBar.element); this._resize(); } } }, createShape: function () { if (this.editor && this.editor.end() || !this.editor) { var dataSource = this.dataSource; var view = dataSource.view() || []; var index = view.length; var model = createModel(dataSource, {}); var shape = this._createShape(model, {}); if (!this.trigger('add', { shape: shape })) { dataSource.insert(index, model); var inactiveItem = this._inactiveShapeItems.getByUid(model.uid); inactiveItem.element = shape; this.edit(shape); } } }, _createShape: function (dataItem, options) { options = deepExtend({}, this.options.shapeDefaults, options); options.dataItem = dataItem; var shape = new Shape(options, this); return shape; }, createConnection: function () { if (this.editor && this.editor.end() || !this.editor) { var connectionsDataSource = this.connectionsDataSource; var view = connectionsDataSource.view() || []; var index = view.length; var model = createModel(connectionsDataSource, {}); var connection = this._createConnection(model); if (!this.trigger('add', { connection: connection })) { this._connectionsDataMap[model.uid] = connection; connectionsDataSource.insert(index, model); this.addConnection(connection, false); this.edit(connection); } } }, _createConnection: function (dataItem, source, target) { var options = deepExtend({}, this.options.connectionDefaults); options.dataItem = dataItem; var connection = new Connection(source || new Point(), target || new Point(), options); return connection; }, editModel: function (dataItem, editorType) { this.cancelEdit(); var editors, template; var editable = this.options.editable; if (editorType == 'shape') { editors = editable.shapeEditors; template = editable.shapeTemplate; } else if (editorType == 'connection') { var connectionSelectorHandler = proxy(connectionSelector, this); editors = deepExtend({}, { from: connectionSelectorHandler, to: connectionSelectorHandler }, editable.connectionEditors); template = editable.connectionTemplate; } else { return; } this.editor = new PopupEditor(this.element, { update: proxy(this._update, this), cancel: proxy(this._cancel, this), model: dataItem, type: editorType, target: this, editors: editors, template: template }); this.trigger('edit', this._editArgs()); }, edit: function (item) { if (item.dataItem) { var editorType = item instanceof Shape ? 'shape' : 'connection'; this.editModel(item.dataItem, editorType); } }, cancelEdit: function () { if (this.editor) { this._getEditDataSource().cancelChanges(this.editor.model); this._destroyEditor(); } }, saveEdit: function () { if (this.editor && this.editor.end() && !this.trigger('save', this._editArgs())) { this._getEditDataSource().sync(); } }, _update: function () { if (this.editor && this.editor.end() && !this.trigger('save', this._editArgs())) { this._getEditDataSource().sync(); this._destroyEditor(); } }, _cancel: function () { if (this.editor && !this.trigger('cancel', this._editArgs())) { var model = this.editor.model; this._getEditDataSource().cancelChanges(model); var element = this._connectionsDataMap[model.uid] || this._dataMap[model.id]; if (element) { element._setOptionsFromModel(model); } this._destroyEditor(); } }, _getEditDataSource: function () { return this.editor.options.type === 'shape' ? this.dataSource : this.connectionsDataSource; }, _editArgs: function () { var result = { container: this.editor.wrapper }; result[this.editor.options.type] = this.editor.model; return result; }, _destroyEditor: function () { if (this.editor) { this.editor.close(); this.editor = null; } }, _initElements: function () { this.wrapper = this.element.empty().css('position', 'relative').attr('tabindex', 0).addClass('k-widget k-diagram'); this.scrollable = $('').appendTo(this.element); }, _initDefaults: function (userOptions) { var options = this.options; var editable = options.editable; var shapeDefaults = options.shapeDefaults; var connectionDefaults = options.connectionDefaults; var userShapeDefaults = (userOptions || {}).shapeDefaults; if (editable === false) { shapeDefaults.editable = false; connectionDefaults.editable = false; } else { copyDefaultOptions(editable, shapeDefaults.editable, [ 'drag', 'remove', 'connect' ]); copyDefaultOptions(editable, connectionDefaults.editable, [ 'drag', 'remove' ]); } if (userShapeDefaults && userShapeDefaults.connectors) { options.shapeDefaults.connectors = userShapeDefaults.connectors; } }, _initCanvas: function () { var canvasContainer = $('').appendTo(this.scrollable)[0]; var viewPort = this.viewport(); this.canvas = new Canvas(canvasContainer, { width: viewPort.width || DEFAULT_CANVAS_WIDTH, height: viewPort.height || DEFAULT_CANVAS_HEIGHT }); }, _createHandlers: function () { var that = this; var element = that.element; element.on(MOUSEWHEEL_NS, proxy(that._wheel, that)); if (!kendo.support.touch && !kendo.support.mobileOS) { that.toolService = new ToolService(that); this.scroller.wrapper.on('mousemove' + NS, proxy(that._mouseMove, that)).on('mouseup' + NS, proxy(that._mouseUp, that)).on('mousedown' + NS, proxy(that._mouseDown, that)).on('mouseover' + NS, proxy(that._mouseover, that)).on('mouseout' + NS, proxy(that._mouseout, that)); element.on('keydown' + NS, proxy(that._keydown, that)); } else { that._userEvents = new kendo.UserEvents(element, { multiTouch: true, tap: proxy(that._tap, that) }); that._userEvents.bind([ 'gesturestart', 'gesturechange', 'gestureend' ], { gesturestart: proxy(that._gestureStart, that), gesturechange: proxy(that._gestureChange, that), gestureend: proxy(that._gestureEnd, that) }); that.toolService = new ToolService(that); if (that.options.pannable !== false) { that.scroller.enable(); } } this._syncHandler = proxy(that._syncChanges, that); that._resizeHandler = proxy(that.resize, that, false); kendo.onResize(that._resizeHandler); this.bind(ZOOM_START, proxy(that._destroyToolBar, that)); this.bind(PAN, proxy(that._destroyToolBar, that)); }, _tap: function (e) { var toolService = this.toolService; var p = this._caculateMobilePosition(e); toolService._updateHoveredItem(p); if (toolService.hoveredItem) { var item = toolService.hoveredItem; if (this.options.selectable !== false) { this._destroyToolBar(); if (item.isSelected) { item.select(false); } else { this.select(item, { addToSelection: true }); } this._createToolBar(); } this.trigger('click', { item: item, point: p }); } }, _caculateMobilePosition: function (e) { return this.documentToModel(Point(e.x.location, e.y.location)); }, _gestureStart: function (e) { this._destroyToolBar(); this.scroller.disable(); var initialCenter = this.documentToModel(new Point(e.center.x, e.center.y)); var eventArgs = { point: initialCenter, zoom: this.zoom() }; if (this.trigger(ZOOM_START, eventArgs)) { return; } this._gesture = e; this._initialCenter = initialCenter; }, _gestureChange: function (e) { var previousGesture = this._gesture; var initialCenter = this._initialCenter; var center = this.documentToView(new Point(e.center.x, e.center.y)); var scaleDelta = e.distance / previousGesture.distance; var zoom = this._zoom; var updateZoom = false; if (math.abs(scaleDelta - 1) >= MOBILE_ZOOM_RATE) { this._zoom = zoom = this._getValidZoom(zoom * scaleDelta); this.options.zoom = zoom; this._gesture = e; updateZoom = true; } var zoomedPoint = initialCenter.times(zoom); var pan = center.minus(zoomedPoint); if (updateZoom || this._pan.distanceTo(pan) >= MOBILE_PAN_DISTANCE) { this._panTransform(pan); this._updateAdorners(); } e.preventDefault(); }, _gestureEnd: function () { if (this.options.pannable !== false) { this.scroller.enable(); } this.trigger(ZOOM_END, { point: this._initialCenter, zoom: this.zoom() }); }, _resize: function () { var viewport = this.viewport(); if (this.canvas) { this.canvas.size(viewport); } if (this.scrollable && this.toolBar) { this.scrollable.height(viewport.height); } }, _mouseover: function (e) { var node = e.target._kendoNode; if (node && node.srcElement._hover) { node.srcElement._hover(true, node.srcElement); } }, _mouseout: function (e) { var node = e.target._kendoNode; if (node && node.srcElement._hover) { node.srcElement._hover(false, node.srcElement); } }, _initTheme: function () { var that = this, themes = dataviz.ui.themes || {}, themeName = ((that.options || {}).theme || '').toLowerCase(), themeOptions = (themes[themeName] || {}).diagram; that.options = deepExtend({}, themeOptions, that.options); if (that.options.editable === true) { deepExtend(that.options, { editable: (themeOptions || {}).editable }); } }, _createOptionElements: function () { var options = this.options; var shapesLength = options.shapes.length; if (shapesLength) { this._createShapes(); } if (options.connections.length) { this._createConnections(); } if (shapesLength && options.layout) { this.layout(options.layout); } }, _createShapes: function () { var that = this, options = that.options, shapes = options.shapes, shape, i; for (i = 0; i < shapes.length; i++) { shape = shapes[i]; that.addShape(shape); } }, _createConnections: function () { var diagram = this, options = diagram.options, defaults = options.connectionDefaults, connections = options.connections, conn, source, target, i; for (i = 0; i < connections.length; i++) { conn = connections[i]; source = diagram._findConnectionTarget(conn.from); target = diagram._findConnectionTarget(conn.to); diagram.connect(source, target, deepExtend({}, defaults, conn)); } }, _findConnectionTarget: function (options) { var diagram = this; var shapeId = isString(options) ? options : options.shapeId || options.id; var target; if (shapeId) { target = diagram.getShapeById(shapeId); if (options.connector) { target = target.getConnector(options.connector); } } else { target = new Point(options.x || 0, options.y || 0); } return target; }, destroy: function () { var that = this; Widget.fn.destroy.call(that); if (this._userEvents) { this._userEvents.destroy(); } kendo.unbindResize(that._resizeHandler); that.clear(); that.element.off(NS); that.scroller.wrapper.off(NS); that.canvas.destroy(true); that.canvas = undefined; that._destroyEditor(); that.destroyScroller(); that._destroyGlobalToolBar(); that._destroyToolBar(); }, destroyScroller: function () { var scroller = this.scroller; if (!scroller) { return; } scroller.destroy(); scroller.element.remove(); this.scroller = null; }, save: function () { var json = { shapes: [], connections: [] }; var i, connection, shape; for (i = 0; i < this.shapes.length; i++) { shape = this.shapes[i]; if (shape.options.serializable) { json.shapes.push(shape.options); } } for (i = 0; i < this.connections.length; i++) { connection = this.connections[i]; json.connections.push(deepExtend({}, connection.options, connection.toJSON())); } return json; }, focus: function () { if (!this.element.is(kendo._activeElement())) { var element = this.element, scrollContainer = element[0], containers = [], offsets = [], documentElement = document.documentElement, i; do { scrollContainer = scrollContainer.parentNode; if (scrollContainer.scrollHeight > scrollContainer.clientHeight) { containers.push(scrollContainer); offsets.push(scrollContainer.scrollTop); } } while (scrollContainer != documentElement); element.focus(); for (i = 0; i < containers.length; i++) { containers[i].scrollTop = offsets[i]; } } }, load: function (options) { this.clear(); this.setOptions(options); this._createShapes(); this._createConnections(); }, setOptions: function (options) { deepExtend(this.options, options); }, clear: function () { var that = this; that.select(false); that.mainLayer.clear(); that._shapesQuadTree.clear(); that._initialize(); }, connect: function (source, target, options) { var connection; if (this.connectionsDataSource && this._isEditable) { var dataItem = this.connectionsDataSource.add({}); connection = this._connectionsDataMap[dataItem.uid]; connection.source(source); connection.target(target); connection.redraw(options); connection.updateModel(); } else { connection = new Connection(source, target, deepExtend({}, this.options.connectionDefaults, options)); this.addConnection(connection); } return connection; }, connected: function (source, target) { for (var i = 0; i < this.connections.length; i++) { var c = this.connections[i]; if (c.from == source && c.to == target) { return true; } } return false; }, addConnection: function (connection, undoable) { if (undoable !== false) { this.undoRedoService.add(new diagram.AddConnectionUnit(connection, this), false); } connection.diagram = this; connection._setOptionsFromModel(); connection.refresh(); this.mainLayer.append(connection.visual); this.connections.push(connection); this.trigger(CHANGE, { added: [connection], removed: [] }); return connection; }, _addConnection: function (connection, undoable) { var connectionsDataSource = this.connectionsDataSource; var dataItem; if (connectionsDataSource && this._isEditable) { dataItem = createModel(connectionsDataSource, cloneDataItem(connection.dataItem)); connection.dataItem = dataItem; connection.updateModel(); if (!this.trigger('add', { connection: connection })) { this._connectionsDataMap[dataItem.uid] = connection; connectionsDataSource.add(dataItem); this.addConnection(connection, undoable); connection._updateConnectors(); return connection; } } else if (!this.trigger('add', { connection: connection })) { this.addConnection(connection, undoable); connection._updateConnectors(); return connection; } }, addShape: function (item, undoable) { var shape, shapeDefaults = this.options.shapeDefaults; if (item instanceof Shape) { shape = item; } else if (!(item instanceof kendo.Class)) { shapeDefaults = deepExtend({}, shapeDefaults, item || {}); shape = new Shape(shapeDefaults, this); } else { return; } if (undoable !== false) { this.undoRedoService.add(new diagram.AddShapeUnit(shape, this), false); } this.shapes.push(shape); if (shape.diagram !== this) { this._shapesQuadTree.insert(shape); shape.diagram = this; } this.mainLayer.append(shape.visual); this.trigger(CHANGE, { added: [shape], removed: [] }); return shape; }, _addShape: function (shape, undoable) { var that = this; var dataSource = that.dataSource; var dataItem; if (dataSource && this._isEditable) { dataItem = createModel(dataSource, cloneDataItem(shape.dataItem)); shape.dataItem = dataItem; shape.updateModel(); if (!this.trigger('add', { shape: shape })) { this.dataSource.add(dataItem); var inactiveItem = this._inactiveShapeItems.getByUid(dataItem.uid); inactiveItem.element = shape; inactiveItem.undoable = undoable; return shape; } } else if (!this.trigger('add', { shape: shape })) { return this.addShape(shape, undoable); } }, remove: function (items, undoable) { items = isArray(items) ? items.slice(0) : [items]; var elements = splitDiagramElements(items); var shapes = elements.shapes; var connections = elements.connections; var i; if (!defined(undoable)) { undoable = true; } if (undoable) { this.undoRedoService.begin(); } this._suspendModelRefresh(); for (i = shapes.length - 1; i >= 0; i--) { this._removeItem(shapes[i], undoable, connections); } for (i = connections.length - 1; i >= 0; i--) { this._removeItem(connections[i], undoable); } this._resumeModelRefresh(); if (undoable) { this.undoRedoService.commit(false); } this.trigger(CHANGE, { added: [], removed: items }); }, _removeShapeDataItem: function (item) { if (this._isEditable) { this.dataSource.remove(item.dataItem); delete this._dataMap[item.dataItem.id]; } }, _removeConnectionDataItem: function (item) { if (this._isEditable) { this.connectionsDataSource.remove(item.dataItem); delete this._connectionsDataMap[item.dataItem.uid]; } }, _triggerRemove: function (items) { var toRemove = []; var item, args, editable; for (var idx = 0; idx < items.length; idx++) { item = items[idx]; editable = item.options.editable; if (item instanceof Shape) { args = { shape: item }; } else { args = { connection: item }; } if (editable && editable.remove !== false && !this.trigger('remove', args)) { toRemove.push(item); } } return toRemove; }, undo: function () { this.undoRedoService.undo(); }, redo: function () { this.undoRedoService.redo(); }, select: function (item, options) { if (isDefined(item)) { options = deepExtend({ addToSelection: false }, options); var addToSelection = options.addToSelection, items = [], selected = [], i, element; if (!addToSelection) { this.deselect(); } this._internalSelection = true; if (item instanceof Array) { items = item; } else if (item instanceof DiagramElement) { items = [item]; } for (i = 0; i < items.length; i++) { element = items[i]; if (element.select(true)) { selected.push(element); } } this._selectionChanged(selected, []); this._internalSelection = false; } else { return this._selectedItems; } }, selectAll: function () { this.select(this.shapes.concat(this.connections)); }, selectArea: function (rect) { var i, items, item; this._internalSelection = true; var selected = []; if (rect instanceof Rect) { items = this.shapes.concat(this.connections); for (i = 0; i < items.length; i++) { item = items[i]; if ((!rect || item._hitTest(rect)) && item.options.enable) { if (item.select(true)) { selected.push(item); } } } } this._selectionChanged(selected, []); this._internalSelection = false; }, deselect: function (item) { this._internalSelection = true; var deselected = [], items = [], element, i; if (item instanceof Array) { items = item; } else if (item instanceof DiagramElement) { items.push(item); } else if (!isDefined(item)) { items = this._selectedItems.slice(0); } for (i = 0; i < items.length; i++) { element = items[i]; if (element.select(false)) { deselected.push(element); } } this._selectionChanged([], deselected); this._internalSelection = false; }, toFront: function (items, undoable) { if (!items) { items = this._selectedItems.slice(); } var result = this._getDiagramItems(items), indices; if (!defined(undoable) || undoable) { indices = indicesOfItems(this.mainLayer, result.visuals); var unit = new ToFrontUnit(this, items, indices); this.undoRedoService.add(unit); } else { this.mainLayer.toFront(result.visuals); this._fixOrdering(result, true); } }, toBack: function (items, undoable) { if (!items) { items = this._selectedItems.slice(); } var result = this._getDiagramItems(items), indices; if (!defined(undoable) || undoable) { indices = indicesOfItems(this.mainLayer, result.visuals); var unit = new ToBackUnit(this, items, indices); this.undoRedoService.add(unit); } else { this.mainLayer.toBack(result.visuals); this._fixOrdering(result, false); } }, bringIntoView: function (item, options) { var viewport = this.viewport(); var aligner = new diagram.RectAlign(viewport); var current, rect, original, newPan; if (viewport.width === 0 || viewport.height === 0) { return; } options = deepExtend({ animate: false, align: 'center middle' }, options); if (options.align == 'none') { options.align = 'center middle'; } if (item instanceof DiagramElement) { rect = item.bounds(TRANSFORMED); } else if (isArray(item)) { rect = this.boundingBox(item); } else if (item instanceof Rect) { rect = item.clone(); } original = rect.clone(); rect.zoom(this._zoom); if (rect.width > viewport.width || rect.height > viewport.height) { this._zoom = this._getValidZoom(math.min(viewport.width / original.width, viewport.height / original.height)); rect = original.clone().zoom(this._zoom); } this._zoomMainLayer(); current = rect.clone(); aligner.align(rect, options.align); newPan = rect.topLeft().minus(current.topLeft()); this.pan(newPan.times(-1), options.animate); }, alignShapes: function (direction) { if (isUndefined(direction)) { direction = 'Left'; } var items = this.select(), val, item, i; if (items.length === 0) { return; } switch (direction.toLowerCase()) { case 'left': case 'top': val = MAX_VALUE; break; case 'right': case 'bottom': val = MIN_VALUE; break; } for (i = 0; i < items.length; i++) { item = items[i]; if (item instanceof Shape) { switch (direction.toLowerCase()) { case 'left': val = math.min(val, item.options.x); break; case 'top': val = math.min(val, item.options.y); break; case 'right': val = math.max(val, item.options.x); break; case 'bottom': val = math.max(val, item.options.y); break; } } } var undoStates = []; var shapes = []; for (i = 0; i < items.length; i++) { item = items[i]; if (item instanceof Shape) { shapes.push(item); undoStates.push(item.bounds()); switch (direction.toLowerCase()) { case 'left': case 'right': item.position(new Point(val, item.options.y)); break; case 'top': case 'bottom': item.position(new Point(item.options.x, val)); break; } } } var unit = new diagram.TransformUnit(shapes, undoStates); this.undoRedoService.add(unit, false); }, zoom: function (zoom, options) { if (zoom) { var staticPoint = options ? options.point : new diagram.Point(0, 0); zoom = this._zoom = this._getValidZoom(zoom); if (!isUndefined(staticPoint)) { staticPoint = new diagram.Point(math.round(staticPoint.x), math.round(staticPoint.y)); var zoomedPoint = staticPoint.times(zoom); var viewportVector = this.modelToView(staticPoint); var raw = viewportVector.minus(zoomedPoint); this._storePan(new diagram.Point(math.round(raw.x), math.round(raw.y))); } if (options) { options.zoom = zoom; } this._panTransform(); this._updateAdorners(); } return this._zoom; }, _getPan: function (pan) { var canvas = this.canvas; if (!canvas.translate) { pan = pan.plus(this._pan); } return pan; }, pan: function (pan, animate) { if (pan instanceof Point) { var that = this; var scroller = that.scroller; pan = that._getPan(pan); pan = pan.times(-1); if (animate) { scroller.animatedScrollTo(pan.x, pan.y, function () { that._updateAdorners(); }); } else { scroller.scrollTo(pan.x, pan.y); that._updateAdorners(); } } }, viewport: function () { var element = this.element; var width = element.width(); var height = element.height(); if (this.toolBar) { height -= this.toolBar.element.outerHeight(); } return new Rect(0, 0, width, height); }, copy: function () { if (this.options.copy.enabled) { this._clipboard = []; this._copyOffset = 1; for (var i = 0; i < this._selectedItems.length; i++) { var item = this._selectedItems[i]; this._clipboard.push(item); } } }, cut: function () { if (this.options.copy.enabled) { this._clipboard = []; this._copyOffset = 0; for (var i = 0; i < this._selectedItems.length; i++) { var item = this._selectedItems[i]; this._clipboard.push(item); } this.remove(this._clipboard, true); } }, paste: function () { if (this._clipboard.length > 0) { var item, copied, i; var mapping = {}; var elements = splitDiagramElements(this._clipboard); var connections = elements.connections; var shapes = elements.shapes; var offset = { x: this._copyOffset * this.options.copy.offsetX, y: this._copyOffset * this.options.copy.offsetY }; this.deselect(); for (i = 0; i < shapes.length; i++) { item = shapes[i]; copied = item.clone(); mapping[item.id] = copied; copied.position(new Point(item.options.x + offset.x, item.options.y + offset.y)); copied.diagram = this; copied = this._addShape(copied); if (copied) { copied.select(); } } for (i = 0; i < connections.length; i++) { item = connections[i]; copied = this._addConnection(item.clone()); if (copied) { this._updateCopiedConnection(copied, item, 'source', mapping, offset); this._updateCopiedConnection(copied, item, 'target', mapping, offset); copied.select(true); copied.updateModel(); } } this._syncChanges(); this._copyOffset += 1; } }, _updateCopiedConnection: function (connection, sourceConnection, connectorName, mapping, offset) { var onActivate, inactiveItem, targetShape; var target = sourceConnection[connectorName](); var diagram = this; if (target instanceof Connector && mapping[target.shape.id]) { targetShape = mapping[target.shape.id]; if (diagram.getShapeById(targetShape.id)) { connection[connectorName](targetShape.getConnector(target.options.name)); } else { inactiveItem = diagram._inactiveShapeItems.getByUid(targetShape.dataItem.uid); if (inactiveItem) { onActivate = function (item) { targetShape = diagram._dataMap[item.id]; connection[connectorName](targetShape.getConnector(target.options.name)); connection.updateModel(); }; diagram._deferredConnectionUpdates.push(inactiveItem.onActivate(onActivate)); } } } else { connection[connectorName](new Point(sourceConnection[connectorName + 'Point']().x + offset.x, sourceConnection[connectorName + 'Point']().y + offset.y)); } }, boundingBox: function (items, origin) { var rect = Rect.empty(), temp, di = isDefined(items) ? this._getDiagramItems(items) : { shapes: this.shapes }; if (di.shapes.length > 0) { var item = di.shapes[0]; rect = item.bounds(ROTATED); for (var i = 1; i < di.shapes.length; i++) { item = di.shapes[i]; temp = item.bounds(ROTATED); if (origin === true) { temp.x -= item._rotationOffset.x; temp.y -= item._rotationOffset.y; } rect = rect.union(temp); } } return rect; }, _containerOffset: function () { var containerOffset = this.element.offset(); if (this.toolBar) { containerOffset.top += this.toolBar.element.outerHeight(); } return containerOffset; }, documentToView: function (point) { var containerOffset = this._containerOffset(); return new Point(point.x - containerOffset.left, point.y - containerOffset.top); }, viewToDocument: function (point) { var containerOffset = this._containerOffset(); return new Point(point.x + containerOffset.left, point.y + containerOffset.top); }, viewToModel: function (point) { return this._transformWithMatrix(point, this._matrixInvert); }, modelToView: function (point) { return this._transformWithMatrix(point, this._matrix); }, modelToLayer: function (point) { return this._transformWithMatrix(point, this._layerMatrix); }, layerToModel: function (point) { return this._transformWithMatrix(point, this._layerMatrixInvert); }, documentToModel: function (point) { var viewPoint = this.documentToView(point); if (!this.canvas.translate) { viewPoint.x = viewPoint.x + this.scroller.scrollLeft; viewPoint.y = viewPoint.y + this.scroller.scrollTop; } return this.viewToModel(viewPoint); }, modelToDocument: function (point) { return this.viewToDocument(this.modelToView(point)); }, _transformWithMatrix: function (point, matrix) { var result = point; if (point instanceof Point) { if (matrix) { result = matrix.apply(point); } } else { var tl = this._transformWithMatrix(point.topLeft(), matrix), br = this._transformWithMatrix(point.bottomRight(), matrix); result = Rect.fromPoints(tl, br); } return result; }, setDataSource: function (dataSource) { this.options.dataSource = dataSource; this._dataSource(); if (this.options.autoBind) { this.dataSource.fetch(); } }, setConnectionsDataSource: function (dataSource) { this.options.connectionsDataSource = dataSource; this._connectionDataSource(); if (this.options.autoBind) { this.connectionsDataSource.fetch(); } }, layout: function (options) { this._layouting = true; var type; if (isUndefined(options)) { options = this.options.layout; } if (isUndefined(options) || isUndefined(options.type)) { type = 'Tree'; } else { type = options.type; } var l; switch (type.toLowerCase()) { case 'tree': l = new diagram.TreeLayout(this); break; case 'layered': l = new diagram.LayeredLayout(this); break; case 'forcedirected': case 'force': case 'spring': case 'springembedder': l = new diagram.SpringLayout(this); break; default: throw 'Layout algorithm \'' + type + '\' is not supported.'; } var initialState = new diagram.LayoutState(this); var finalState = l.layout(options); if (finalState) { var unit = new diagram.LayoutUndoUnit(initialState, finalState, options ? options.animate : null); this.undoRedoService.add(unit); } this._layouting = false; this._redrawConnections(); }, getShapeById: function (id) { var found; found = Utils.first(this.shapes, function (s) { return s.visual.id === id; }); if (found) { return found; } found = Utils.first(this.connections, function (c) { return c.visual.id === id; }); return found; }, _extendLayoutOptions: function (options) { if (options.layout) { options.layout = deepExtend(diagram.LayoutBase.fn.defaultOptions || {}, options.layout); } }, _selectionChanged: function (selected, deselected) { if (selected.length || deselected.length) { this.trigger(SELECT, { selected: selected, deselected: deselected }); } }, _getValidZoom: function (zoom) { return math.min(math.max(zoom, this.options.zoomMin), this.options.zoomMax); }, _panTransform: function (pos) { var diagram = this, pan = pos || diagram._pan; if (diagram.canvas.translate) { diagram.scroller.scrollTo(pan.x, pan.y); diagram._zoomMainLayer(); } else { diagram._storePan(pan); diagram._transformMainLayer(); } }, _finishPan: function () { this.trigger(PAN, { total: this._pan, delta: Number.NaN }); }, _storePan: function (pan) { this._pan = pan; this._storeViewMatrix(); }, _zoomMainLayer: function () { var zoom = this._zoom; var transform = new CompositeTransform(0, 0, zoom, zoom); transform.render(this.mainLayer); this._storeLayerMatrix(transform); this._storeViewMatrix(); }, _transformMainLayer: function () { var pan = this._pan, zoom = this._zoom; var transform = new CompositeTransform(pan.x, pan.y, zoom, zoom); transform.render(this.mainLayer); this._storeLayerMatrix(transform); this._storeViewMatrix(); }, _storeLayerMatrix: function (canvasTransform) { this._layerMatrix = canvasTransform.toMatrix(); this._layerMatrixInvert = canvasTransform.invert().toMatrix(); }, _storeViewMatrix: function () { var pan = this._pan, zoom = this._zoom; var transform = new CompositeTransform(pan.x, pan.y, zoom, zoom); this._matrix = transform.toMatrix(); this._matrixInvert = transform.invert().toMatrix(); }, _toIndex: function (items, indices) { var result = this._getDiagramItems(items); this.mainLayer.toIndex(result.visuals, indices); this._fixOrdering(result, false); }, _fixOrdering: function (result, toFront) { var shapePos = toFront ? this.shapes.length - 1 : 0, conPos = toFront ? this.connections.length - 1 : 0, i, item; for (i = 0; i < result.shapes.length; i++) { item = result.shapes[i]; Utils.remove(this.shapes, item); Utils.insert(this.shapes, item, shapePos); } for (i = 0; i < result.cons.length; i++) { item = result.cons[i]; Utils.remove(this.connections, item); Utils.insert(this.connections, item, conPos); } }, _getDiagramItems: function (items) { var i, result = {}, args = items; result.visuals = []; result.shapes = []; result.cons = []; if (!items) { args = this._selectedItems.slice(); } else if (!isArray(items)) { args = [items]; } for (i = 0; i < args.length; i++) { var item = args[i]; if (item instanceof Shape) { result.shapes.push(item); result.visuals.push(item.visual); } else if (item instanceof Connection) { result.cons.push(item); result.visuals.push(item.visual); } } return result; }, _removeItem: function (item, undoable, removedConnections) { item.select(false); if (item instanceof Shape) { this._removeShapeDataItem(item); this._removeShape(item, undoable, removedConnections); } else if (item instanceof Connection) { this._removeConnectionDataItem(item); this._removeConnection(item, undoable); } this.mainLayer.remove(item.visual); }, _removeShape: function (shape, undoable, removedConnections) { var i, connection, connector, sources = [], targets = []; this.toolService._removeHover(); if (undoable) { this.undoRedoService.addCompositeItem(new DeleteShapeUnit(shape)); } Utils.remove(this.shapes, shape); this._shapesQuadTree.remove(shape); for (i = 0; i < shape.connectors.length; i++) { connector = shape.connectors[i]; for (var j = 0; j < connector.connections.length; j++) { connection = connector.connections[j]; if (!removedConnections || !dataviz.inArray(connection, removedConnections)) { if (connection.sourceConnector == connector) { sources.push(connection); } else if (connection.targetConnector == connector) { targets.push(connection); } } } } for (i = 0; i < sources.length; i++) { sources[i].source(null, undoable); sources[i].updateModel(); } for (i = 0; i < targets.length; i++) { targets[i].target(null, undoable); targets[i].updateModel(); } }, _removeConnection: function (connection, undoable) { if (connection.sourceConnector) { Utils.remove(connection.sourceConnector.connections, connection); } if (connection.targetConnector) { Utils.remove(connection.targetConnector.connections, connection); } if (undoable) { this.undoRedoService.addCompositeItem(new DeleteConnectionUnit(connection)); } Utils.remove(this.connections, connection); }, _removeDataItems: function (items, recursive) { var item, children, shape, idx; items = isArray(items) ? items : [items]; while (items.length) { item = items.shift(); shape = this._dataMap[item.uid]; if (shape) { this._removeShapeConnections(shape); this._removeItem(shape, false); delete this._dataMap[item.uid]; if (recursive && item.hasChildren && item.loaded()) { children = item.children.data(); for (idx = 0; idx < children.length; idx++) { items.push(children[idx]); } } } } }, _removeShapeConnections: function (shape) { var connections = shape.connections(); var idx; if (connections) { for (idx = 0; idx < connections.length; idx++) { this._removeItem(connections[idx], false); } } }, _addDataItem: function (dataItem, undoable) { if (!defined(dataItem)) { return; } var shape = this._dataMap[dataItem.id]; if (shape) { return shape; } var options = deepExtend({}, this.options.shapeDefaults); options.dataItem = dataItem; shape = new Shape(options, this); this.addShape(shape, undoable !== false); this._dataMap[dataItem.id] = shape; return shape; }, _addDataItemByUid: function (dataItem) { if (!defined(dataItem)) { return; } var shape = this._dataMap[dataItem.uid]; if (shape) { return shape; } var options = deepExtend({}, this.options.shapeDefaults); options.dataItem = dataItem; shape = new Shape(options, this); this.addShape(shape); this._dataMap[dataItem.uid] = shape; return shape; }, _addDataItems: function (items, parent) { var item, idx, shape, parentShape, connection; for (idx = 0; idx < items.length; idx++) { item = items[idx]; shape = this._addDataItemByUid(item); parentShape = this._addDataItemByUid(parent); if (parentShape && !this.connected(parentShape, shape)) { connection = this.connect(parentShape, shape); } } }, _refreshSource: function (e) { var that = this, node = e.node, action = e.action, items = e.items, options = that.options, idx, dataBound; if (e.field) { return; } if (action == 'remove') { this._removeDataItems(e.items, true); } else { if ((!action || action === 'itemloaded') && !this._bindingRoots) { this._bindingRoots = true; dataBound = true; } if (!action && !node) { that.clear(); } this._addDataItems(items, node); for (idx = 0; idx < items.length; idx++) { items[idx].load(); } } if (options.layout && (dataBound || action == 'remove' || action == 'add')) { that.layout(options.layout); } if (dataBound) { this.trigger('dataBound'); this._bindingRoots = false; } }, _mouseDown: function (e) { var p = this._calculatePosition(e); if (e.which == 1 && this.toolService.start(p, this._meta(e))) { this._destroyToolBar(); e.preventDefault(); } }, _addItem: function (item) { if (item instanceof Shape) { this.addShape(item); } else if (item instanceof Connection) { this.addConnection(item); } }, _mouseUp: function (e) { var p = this._calculatePosition(e); if (e.which == 1 && this.toolService.end(p, this._meta(e))) { this._createToolBar(); e.preventDefault(); } }, _createToolBar: function () { var diagram = this.toolService.diagram; if (!this.singleToolBar && diagram.select().length === 1) { var element = diagram.select()[0]; if (element && element.options.editable !== false) { var editable = element.options.editable; var tools = editable.tools; if (this._isEditable && tools.length === 0) { if (element instanceof Shape) { tools = [ 'edit', 'rotateClockwise', 'rotateAnticlockwise' ]; } else if (element instanceof Connection) { tools = ['edit']; } if (editable && editable.remove !== false) { tools.push('delete'); } } if (tools && tools.length) { var padding = 20; var point; this.singleToolBar = new DiagramToolBar(diagram, { tools: tools, click: proxy(this._toolBarClick, this), modal: true }); var popupWidth = this.singleToolBar._popup.element.outerWidth(); var popupHeight = this.singleToolBar._popup.element.outerHeight(); if (element instanceof Shape) { var shapeBounds = this.modelToView(element.bounds(ROTATED)); point = Point(shapeBounds.x, shapeBounds.y).minus(Point((popupWidth - shapeBounds.width) / 2, popupHeight + padding)); } else if (element instanceof Connection) { var connectionBounds = this.modelToView(element.bounds()); point = Point(connectionBounds.x, connectionBounds.y).minus(Point((popupWidth - connectionBounds.width - 20) / 2, popupHeight + padding)); } if (point) { if (!this.canvas.translate) { point = point.minus(Point(this.scroller.scrollLeft, this.scroller.scrollTop)); } point = this.viewToDocument(point); point = Point(math.max(point.x, 0), math.max(point.y, 0)); this.singleToolBar.showAt(point); } else { this._destroyToolBar(); } } } } }, _toolBarClick: function (e) { this.trigger('toolBarClick', e); this._destroyToolBar(); }, _mouseMove: function (e) { if (this.pauseMouseHandlers) { return; } var p = this._calculatePosition(e); if ((e.which === 0 || e.which == 1) && this.toolService.move(p, this._meta(e))) { e.preventDefault(); } }, _keydown: function (e) { if (this.toolService.keyDown(e.keyCode, this._meta(e))) { e.preventDefault(); } }, _wheel: function (e) { var delta = mwDelta(e), p = this._calculatePosition(e), meta = deepExtend(this._meta(e), { delta: delta }); if (this.toolService.wheel(p, meta)) { e.preventDefault(); } }, _meta: function (e) { return { ctrlKey: e.ctrlKey, metaKey: e.metaKey, altKey: e.altKey, shiftKey: e.shiftKey }; }, _calculatePosition: function (e) { var pointEvent = e.pageX === undefined ? e.originalEvent : e, point = new Point(pointEvent.pageX, pointEvent.pageY), offset = this.documentToModel(point); return offset; }, _normalizePointZoom: function (point) { return point.times(1 / this.zoom()); }, _initialize: function () { this.shapes = []; this._selectedItems = []; this.connections = []; this._dataMap = {}; this._connectionsDataMap = {}; this._inactiveShapeItems = new InactiveItemsCollection(); this._deferredConnectionUpdates = []; this.undoRedoService = new UndoRedoService({ undone: this._syncHandler, redone: this._syncHandler }); this.id = diagram.randomId(); }, _fetchFreshData: function () { var that = this; that._dataSource(); if (that._isEditable) { that._connectionDataSource(); } if (that.options.autoBind) { if (that._isEditable) { this._loadingShapes = true; this._loadingConnections = true; that.dataSource.fetch(); that.connectionsDataSource.fetch(); } else { that.dataSource.fetch(); } } }, _dataSource: function () { if (defined(this.options.connectionsDataSource)) { this._isEditable = true; var dsOptions = this.options.dataSource || {}; var ds = isArray(dsOptions) ? { data: dsOptions } : dsOptions; if (this.dataSource && this._shapesRefreshHandler) { this.dataSource.unbind('change', this._shapesRefreshHandler).unbind('requestStart', this._shapesRequestStartHandler).unbind('error', this._shapesErrorHandler); } else { this._shapesRefreshHandler = proxy(this._refreshShapes, this); this._shapesRequestStartHandler = proxy(this._shapesRequestStart, this); this._shapesErrorHandler = proxy(this._error, this); } this.dataSource = kendo.data.DataSource.create(ds).bind('change', this._shapesRefreshHandler).bind('requestStart', this._shapesRequestStartHandler).bind('error', this._shapesErrorHandler); } else { this._treeDataSource(); this._isEditable = false; } }, _connectionDataSource: function () { var dsOptions = this.options.connectionsDataSource; if (dsOptions) { var ds = isArray(dsOptions) ? { data: dsOptions } : dsOptions; if (this.connectionsDataSource && this._connectionsRefreshHandler) { this.connectionsDataSource.unbind('change', this._connectionsRefreshHandler).unbind('requestStart', this._connectionsRequestStartHandler).unbind('error', this._connectionsErrorHandler); } else { this._connectionsRefreshHandler = proxy(this._refreshConnections, this); this._connectionsRequestStartHandler = proxy(this._connectionsRequestStart, this); this._connectionsErrorHandler = proxy(this._connectionsError, this); } this.connectionsDataSource = kendo.data.DataSource.create(ds).bind('change', this._connectionsRefreshHandler).bind('requestStart', this._connectionsRequestStartHandler).bind('error', this._connectionsErrorHandler); } }, _shapesRequestStart: function (e) { if (e.type == 'read') { this._loadingShapes = true; } }, _connectionsRequestStart: function (e) { if (e.type == 'read') { this._loadingConnections = true; } }, _error: function () { this._loadingShapes = false; }, _connectionsError: function () { this._loadingConnections = false; }, _refreshShapes: function (e) { if (e.action === 'remove') { if (this._shouldRefresh()) { this._removeShapes(e.items); } } else if (e.action === 'itemchange') { if (this._shouldRefresh()) { this._updateShapes(e.items, e.field); } } else if (e.action === 'add') { this._inactiveShapeItems.add(e.items); } else if (e.action === 'sync') { this._syncShapes(e.items); } else { this.refresh(); } }, _shouldRefresh: function () { return !this._suspended; }, _suspendModelRefresh: function () { this._suspended = (this._suspended || 0) + 1; }, _resumeModelRefresh: function () { this._suspended = math.max((this._suspended || 0) - 1, 0); }, refresh: function () { this._loadingShapes = false; if (!this._loadingConnections) { this._rebindShapesAndConnections(); } }, _rebindShapesAndConnections: function () { this.clear(); this._addShapes(this.dataSource.view()); if (this.connectionsDataSource) { this._addConnections(this.connectionsDataSource.view(), false); } if (this.options.layout) { this.layout(this.options.layout); } else { this._redrawConnections(); } this.trigger('dataBound'); }, refreshConnections: function () { this._loadingConnections = false; if (!this._loadingShapes) { this._rebindShapesAndConnections(); } }, _redrawConnections: function () { var connections = this.connections; for (var idx = 0; idx < connections.length; idx++) { connections[idx].refresh(); } }, _removeShapes: function (items) { var dataMap = this._dataMap; var item, i; for (i = 0; i < items.length; i++) { item = items[i]; if (dataMap[item.id]) { this.remove(dataMap[item.id], false); dataMap[item.id] = null; } } }, _syncShapes: function () { var diagram = this; var inactiveItems = diagram._inactiveShapeItems; inactiveItems.forEach(function (inactiveItem) { var dataItem = inactiveItem.dataItem; var shape = inactiveItem.element; if (!dataItem.isNew()) { if (shape) { shape._setOptionsFromModel(); diagram.addShape(shape, inactiveItem.undoable); diagram._dataMap[dataItem.id] = shape; } else { diagram._addDataItem(dataItem); } inactiveItem.activate(); inactiveItems.remove(dataItem); } }); }, _updateShapes: function (items, field) { for (var i = 0; i < items.length; i++) { var dataItem = items[i]; var shape = this._dataMap[dataItem.id]; if (shape) { shape.updateOptionsFromModel(dataItem, field); } } }, _addShapes: function (dataItems) { for (var i = 0; i < dataItems.length; i++) { this._addDataItem(dataItems[i], false); } }, _refreshConnections: function (e) { if (e.action === 'remove') { if (this._shouldRefresh()) { this._removeConnections(e.items); } } else if (e.action === 'add') { this._addConnections(e.items); } else if (e.action === 'sync') { } else if (e.action === 'itemchange') { if (this._shouldRefresh()) { this._updateConnections(e.items); } } else { this.refreshConnections(); } }, _removeConnections: function (items) { for (var i = 0; i < items.length; i++) { this.remove(this._connectionsDataMap[items[i].uid], false); this._connectionsDataMap[items[i].uid] = null; } }, _updateConnections: function (items) { for (var i = 0; i < items.length; i++) { var dataItem = items[i]; var connection = this._connectionsDataMap[dataItem.uid]; connection.updateOptionsFromModel(dataItem); } }, _addConnections: function (connections, undoable) { var length = connections.length; for (var i = 0; i < length; i++) { var dataItem = connections[i]; this._addConnectionDataItem(dataItem, undoable); } }, _addConnectionDataItem: function (dataItem, undoable) { if (!this._connectionsDataMap[dataItem.uid]) { var from = this._validateConnector(dataItem.from); if (!defined(from) || from === null) { from = new Point(dataItem.fromX, dataItem.fromY); } var to = this._validateConnector(dataItem.to); if (!defined(to) || to === null) { to = new Point(dataItem.toX, dataItem.toY); } if (defined(from) && defined(to)) { var options = deepExtend({}, this.options.connectionDefaults); options.dataItem = dataItem; var connection = new Connection(from, to, options); this._connectionsDataMap[dataItem.uid] = connection; this.addConnection(connection, undoable); } } }, _validateConnector: function (value) { var connector; if (defined(value) && value !== null) { connector = this._dataMap[value]; } return connector; }, _treeDataSource: function () { var that = this, options = that.options, dataSource = options.dataSource; dataSource = isArray(dataSource) ? { data: dataSource } : dataSource; if (!dataSource.fields) { dataSource.fields = [ { field: 'text' }, { field: 'url' }, { field: 'spriteCssClass' }, { field: 'imageUrl' } ]; } if (that.dataSource && that._refreshHandler) { that._unbindDataSource(); } that._refreshHandler = proxy(that._refreshSource, that); that._errorHandler = proxy(that._error, that); that.dataSource = HierarchicalDataSource.create(dataSource).bind(CHANGE, that._refreshHandler).bind(ERROR, that._errorHandler); }, _unbindDataSource: function () { var that = this; that.dataSource.unbind(CHANGE, that._refreshHandler).unbind(ERROR, that._errorHandler); }, _adorn: function (adorner, isActive) { if (isActive !== undefined && adorner) { if (isActive) { this._adorners.push(adorner); this.adornerLayer.append(adorner.visual); } else { Utils.remove(this._adorners, adorner); this.adornerLayer.remove(adorner.visual); } } }, _showConnectors: function (shape, value) { if (value) { this._connectorsAdorner.show(shape); } else { this._connectorsAdorner.destroy(); } }, _updateAdorners: function () { var adorners = this._adorners; for (var i = 0; i < adorners.length; i++) { var adorner = adorners[i]; if (adorner.refreshBounds) { adorner.refreshBounds(); } adorner.refresh(); } }, _refresh: function () { for (var i = 0; i < this.connections.length; i++) { this.connections[i].refresh(); } }, _destroyToolBar: function () { if (this.singleToolBar) { this.singleToolBar.hide(); this.singleToolBar.destroy(); this.singleToolBar = null; } }, _destroyGlobalToolBar: function () { if (this.toolBar) { this.toolBar.hide(); this.toolBar.destroy(); this.toolBar = null; } }, exportDOMVisual: function () { var viewBox = this.canvas._viewBox; var scrollOffset = geom.transform().translate(-viewBox.x, -viewBox.y); var viewRect = new geom.Rect([ 0, 0 ], [ viewBox.width, viewBox.height ]); var clipPath = draw.Path.fromRect(viewRect); var wrap = new draw.Group({ transform: scrollOffset }); var clipWrap = new draw.Group({ clip: clipPath }); var root = this.canvas.drawingElement.children[0]; clipWrap.append(wrap); wrap.children.push(root); return clipWrap; }, exportVisual: function () { var scale = geom.transform().scale(1 / this._zoom); var wrap = new draw.Group({ transform: scale }); var root = this.mainLayer.drawingElement; wrap.children.push(root); return wrap; }, _syncChanges: function () { this._syncShapeChanges(); this._syncConnectionChanges(); }, _syncShapeChanges: function () { if (this.dataSource && this._isEditable) { this.dataSource.sync(); } }, _syncConnectionChanges: function () { var that = this; if (that.connectionsDataSource && that._isEditable) { $.when.apply($, that._deferredConnectionUpdates).then(function () { that.connectionsDataSource.sync(); }); that.deferredConnectionUpdates = []; } } }); dataviz.ExportMixin.extend(Diagram.fn, true); if (kendo.PDFMixin) { kendo.PDFMixin.extend(Diagram.fn); } function filterShapeDataItem(dataItem) { var result = {}; dataItem = dataItem || {}; if (defined(dataItem.text) && dataItem.text !== null) { result.text = dataItem.text; } if (defined(dataItem.x) && dataItem.x !== null) { result.x = dataItem.x; } if (defined(dataItem.y) && dataItem.y !== null) { result.y = dataItem.y; } if (defined(dataItem.width) && dataItem.width !== null) { result.width = dataItem.width; } if (defined(dataItem.height) && dataItem.height !== null) { result.height = dataItem.height; } if (defined(dataItem.type) && dataItem.type !== null) { result.type = dataItem.type; } return result; } function filterConnectionDataItem(dataItem) { var result = {}; dataItem = dataItem || {}; if (defined(dataItem.text) && dataItem.text !== null) { result.content = dataItem.text; } if (defined(dataItem.type) && dataItem.type !== null) { result.type = dataItem.type; } if (defined(dataItem.from) && dataItem.from !== null) { result.from = dataItem.from; } if (defined(dataItem.fromConnector) && dataItem.fromConnector !== null) { result.fromConnector = dataItem.fromConnector; } if (defined(dataItem.fromX) && dataItem.fromX !== null) { result.fromX = dataItem.fromX; } if (defined(dataItem.fromY) && dataItem.fromY !== null) { result.fromY = dataItem.fromY; } if (defined(dataItem.to) && dataItem.to !== null) { result.to = dataItem.to; } if (defined(dataItem.toConnector) && dataItem.toConnector !== null) { result.toConnector = dataItem.toConnector; } if (defined(dataItem.toX) && dataItem.toX !== null) { result.toX = dataItem.toX; } if (defined(dataItem.toY) && dataItem.toY !== null) { result.toY = dataItem.toY; } return result; } var DiagramToolBar = kendo.Observable.extend({ init: function (diagram, options) { kendo.Observable.fn.init.call(this); this.diagram = diagram; this.options = deepExtend({}, this.options, options); this._tools = []; this.createToolBar(); this.createTools(); this.appendTools(); if (this.options.modal) { this.createPopup(); } this.bind(this.events, options); }, events: ['click'], createPopup: function () { this.container = $('').append(this.element); this._popup = this.container.kendoPopup({}).getKendoPopup(); }, appendTools: function () { for (var i = 0; i < this._tools.length; i++) { var tool = this._tools[i]; if (tool.buttons && tool.buttons.length || !defined(tool.buttons)) { this._toolBar.add(tool); } } }, createToolBar: function () { this.element = $(''); this._toolBar = this.element.kendoToolBar({ click: proxy(this.click, this), resizable: false }).getKendoToolBar(); this.element.css('border', 'none'); }, createTools: function () { for (var i = 0; i < this.options.tools.length; i++) { this.createTool(this.options.tools[i]); } }, createTool: function (tool) { var toolName = (isPlainObject(tool) ? tool.name : tool) + 'Tool'; if (this[toolName]) { this[toolName](tool); } else { this._tools.push(tool); } }, showAt: function (point) { if (this._popup) { this._popup.open(point.x, point.y); } }, hide: function () { if (this._popup) { this._popup.close(); } }, newGroup: function () { return { type: 'buttonGroup', buttons: [] }; }, editTool: function () { this._tools.push({ spriteCssClass: 'k-icon k-i-pencil', showText: 'overflow', type: 'button', text: 'Edit', attributes: this._setAttributes({ action: 'edit' }) }); }, deleteTool: function () { this._tools.push({ spriteCssClass: 'k-icon k-i-close', showText: 'overflow', type: 'button', text: 'Delete', attributes: this._setAttributes({ action: 'delete' }) }); }, rotateAnticlockwiseTool: function (options) { this._appendGroup('rotate'); this._rotateGroup.buttons.push({ spriteCssClass: 'k-icon k-i-rotateccw', showText: 'overflow', text: 'RotateAnticlockwise', group: 'rotate', attributes: this._setAttributes({ action: 'rotateAnticlockwise', step: options.step }) }); }, rotateClockwiseTool: function (options) { this._appendGroup('rotate'); this._rotateGroup.buttons.push({ spriteCssClass: 'k-icon k-i-rotatecw', attributes: this._setAttributes({ action: 'rotateClockwise', step: options.step }), showText: 'overflow', text: 'RotateClockwise', group: 'rotate' }); }, createShapeTool: function () { this._appendGroup('create'); this._createGroup.buttons.push({ spriteCssClass: 'k-icon k-i-shape', showText: 'overflow', text: 'CreateShape', group: 'create', attributes: this._setAttributes({ action: 'createShape' }) }); }, createConnectionTool: function () { this._appendGroup('create'); this._createGroup.buttons.push({ spriteCssClass: 'k-icon k-i-connector', showText: 'overflow', text: 'CreateConnection', group: 'create', attributes: this._setAttributes({ action: 'createConnection' }) }); }, undoTool: function () { this._appendGroup('history'); this._historyGroup.buttons.push({ spriteCssClass: 'k-icon k-i-undo', showText: 'overflow', text: 'Undo', group: 'history', attributes: this._setAttributes({ action: 'undo' }) }); }, redoTool: function () { this._appendGroup('history'); this._historyGroup.buttons.push({ spriteCssClass: 'k-icon k-i-redo', showText: 'overflow', text: 'Redo', group: 'history', attributes: this._setAttributes({ action: 'redo' }) }); }, _appendGroup: function (name) { var prop = '_' + name + 'Group'; if (!this[prop]) { this[prop] = this.newGroup(); this._tools.push(this[prop]); } }, _setAttributes: function (attributes) { var attr = {}; if (attributes.action) { attr[kendo.attr('action')] = attributes.action; } if (attributes.step) { attr[kendo.attr('step')] = attributes.step; } return attr; }, _getAttributes: function (element) { var attr = {}; var action = element.attr(kendo.attr('action')); if (action) { attr.action = action; } var step = element.attr(kendo.attr('step')); if (step) { attr.step = step; } return attr; }, click: function (e) { var attributes = this._getAttributes($(e.target)); var action = attributes.action; if (action) { this[action](attributes); } this.trigger('click', this.eventData(action)); }, eventData: function (action) { var element = this.selectedElements(), shapes = [], connections = []; if (element instanceof Shape) { shapes.push(element); } else { connections.push(element); } return { shapes: shapes, connections: connections, action: action }; }, 'delete': function () { var diagram = this.diagram; var toRemove = diagram._triggerRemove(this.selectedElements()); if (toRemove.length) { this.diagram.remove(toRemove, true); this.diagram._syncChanges(); } }, edit: function () { this.diagram.edit(this.selectedElements()[0]); }, rotateClockwise: function (options) { var angle = parseFloat(options.step || 90); this._rotate(angle); }, rotateAnticlockwise: function (options) { var angle = parseFloat(options.step || 90); this._rotate(-angle); }, _rotate: function (angle) { var adorner = this.diagram._resizingAdorner; adorner.angle(adorner.angle() + angle); adorner.rotate(); }, selectedElements: function () { return this.diagram.select(); }, createShape: function () { this.diagram.createShape(); }, createConnection: function () { this.diagram.createConnection(); }, undo: function () { this.diagram.undo(); }, redo: function () { this.diagram.redo(); }, destroy: function () { this.diagram = null; this.element = null; this.options = null; if (this._toolBar) { this._toolBar.destroy(); } if (this._popup) { this._popup.destroy(); } } }); var Editor = kendo.Observable.extend({ init: function (element, options) { kendo.Observable.fn.init.call(this); this.options = extend(true, {}, this.options, options); this.element = element; this.model = this.options.model; this.fields = this._getFields(); this._initContainer(); this.createEditable(); }, options: { editors: {} }, _initContainer: function () { this.wrapper = this.element; }, createEditable: function () { var options = this.options; this.editable = new kendo.ui.Editable(this.wrapper, { fields: this.fields, target: options.target, clearContainer: false, model: this.model }); }, _isEditable: function (field) { return this.model.editable && this.model.editable(field); }, _getFields: function () { var fields = []; var modelFields = this.model.fields; for (var field in modelFields) { var result = {}; if (this._isEditable(field)) { var editor = this.options.editors[field]; if (editor) { result.editor = editor; } result.field = field; fields.push(result); } } return fields; }, end: function () { return this.editable.end(); }, destroy: function () { this.editable.destroy(); this.editable.element.find('[' + kendo.attr('container-for') + ']').empty(); this.model = this.wrapper = this.element = this.columns = this.editable = null; } }); var PopupEditor = Editor.extend({ init: function (element, options) { Editor.fn.init.call(this, element, options); this.bind(this.events, this.options); this.open(); }, events: [ 'update', 'cancel' ], options: { window: { modal: true, resizable: false, draggable: true, title: 'Edit', visible: false } }, _initContainer: function () { var that = this; this.wrapper = $('').attr(kendo.attr('uid'), this.model.uid); var formContent = ''; if (this.options.template) { formContent += this._renderTemplate(); this.fields = []; } else { formContent += this._renderFields(); } formContent += this._renderButtons(); this.wrapper.append($('').append(formContent)); this.window = new kendo.ui.Window(this.wrapper.appendTo(this.element), this.options.window); this.window.bind('close', function (e) { if (e.userTriggered) { e.sender.element.focus(); that._cancelClick(e); } }); this._attachButtonEvents(); }, _renderTemplate: function () { var template = this.options.template; if (typeof template === 'string') { template = window.unescape(template); } template = kendo.template(template)(this.model); return template; }, _renderFields: function () { var form = ''; for (var i = 0; i < this.fields.length; i++) { var field = this.fields[i]; form += ''; if (this._isEditable(field.field)) { form += ''; } } return form; }, _renderButtons: function () { var form = ''; return form; }, _createButton: function (name) { return kendo.template(BUTTON_TEMPLATE)(defaultButtons[name]); }, _attachButtonEvents: function () { this._cancelClickHandler = proxy(this._cancelClick, this); this.window.element.on(CLICK + NS, 'a.k-diagram-cancel', this._cancelClickHandler); this._updateClickHandler = proxy(this._updateClick, this); this.window.element.on(CLICK + NS, 'a.k-diagram-update', this._updateClickHandler); }, _updateClick: function (e) { e.preventDefault(); this.trigger('update'); }, _cancelClick: function (e) { e.preventDefault(); this.trigger('cancel'); }, open: function () { this.window.center().open(); }, close: function () { this.window.bind('deactivate', proxy(this.destroy, this)).close(); }, destroy: function () { this.window.close().destroy(); this.window.element.off(CLICK + NS, 'a.k-diagram-cancel', this._cancelClickHandler); this.window.element.off(CLICK + NS, 'a.k-diagram-update', this._updateClickHandler); this._cancelClickHandler = null; this._editUpdateClickHandler = null; this.window = null; Editor.fn.destroy.call(this); } }); function connectionSelector(container, options) { var model = this.dataSource.reader.model; if (model) { var textField = model.fn.fields.text ? 'text' : model.idField; $('').appendTo(container).kendoDropDownList({ dataValueField: model.idField, dataTextField: textField, dataSource: this.dataSource.data().toJSON(), optionLabel: ' ', valuePrimitive: true }); } } function InactiveItem(dataItem) { this.dataItem = dataItem; this.callbacks = []; } InactiveItem.fn = InactiveItem.prototype = { onActivate: function (callback) { var deffered = $.Deferred(); this.callbacks.push({ callback: callback, deferred: deffered }); return deffered; }, activate: function () { var callbacks = this.callbacks; var item; for (var idx = 0; idx < callbacks.length; idx++) { item = this.callbacks[idx]; item.callback(this.dataItem); item.deferred.resolve(); } this.callbacks = []; } }; function InactiveItemsCollection() { this.items = {}; } InactiveItemsCollection.fn = InactiveItemsCollection.prototype = { add: function (items) { for (var idx = 0; idx < items.length; idx++) { this.items[items[idx].uid] = new InactiveItem(items[idx]); } }, forEach: function (callback) { for (var uid in this.items) { callback(this.items[uid]); } }, getByUid: function (uid) { return this.items[uid]; }, remove: function (item) { delete this.items[item.uid]; } }; var QuadRoot = Class.extend({ init: function () { this.shapes = []; }, _add: function (shape, bounds) { this.shapes.push({ bounds: bounds, shape: shape }); shape._quadNode = this; }, insert: function (shape, bounds) { this._add(shape, bounds); }, remove: function (shape) { var shapes = this.shapes; var length = shapes.length; for (var idx = 0; idx < length; idx++) { if (shapes[idx].shape === shape) { shapes.splice(idx, 1); break; } } }, hitTestRect: function (rect) { var shapes = this.shapes; var length = shapes.length; for (var i = 0; i < length; i++) { if (this._testRect(shapes[i].shape, rect)) { return true; } } }, _testRect: function (shape, rect) { var angle = shape.rotate().angle; var bounds = shape.bounds(); var hit; if (!angle) { hit = bounds.overlaps(rect); } else { hit = Intersect.rects(rect, bounds, -angle); } return hit; } }); var QuadNode = QuadRoot.extend({ init: function (rect) { QuadRoot.fn.init.call(this); this.children = []; this.rect = rect; }, inBounds: function (rect) { var nodeRect = this.rect; var nodeBottomRight = nodeRect.bottomRight(); var bottomRight = rect.bottomRight(); var inBounds = nodeRect.x <= rect.x && nodeRect.y <= rect.y && bottomRight.x <= nodeBottomRight.x && bottomRight.y <= nodeBottomRight.y; return inBounds; }, overlapsBounds: function (rect) { return this.rect.overlaps(rect); }, insert: function (shape, bounds) { var inserted = false; var children = this.children; var length = children.length; if (this.inBounds(bounds)) { if (!length && this.shapes.length < 4) { this._add(shape, bounds); } else { if (!length) { this._initChildren(); } for (var idx = 0; idx < children.length; idx++) { if (children[idx].insert(shape, bounds)) { inserted = true; break; } } if (!inserted) { this._add(shape, bounds); } } inserted = true; } return inserted; }, _initChildren: function () { var rect = this.rect, children = this.children, shapes = this.shapes, center = rect.center(), halfWidth = rect.width / 2, halfHeight = rect.height / 2, childIdx, shapeIdx; children.push(new QuadNode(new Rect(rect.x, rect.y, halfWidth, halfHeight)), new QuadNode(new Rect(center.x, rect.y, halfWidth, halfHeight)), new QuadNode(new Rect(rect.x, center.y, halfWidth, halfHeight)), new QuadNode(new Rect(center.x, center.y, halfWidth, halfHeight))); for (shapeIdx = shapes.length - 1; shapeIdx >= 0; shapeIdx--) { for (childIdx = 0; childIdx < children.length; childIdx++) { if (children[childIdx].insert(shapes[shapeIdx].shape, shapes[shapeIdx].bounds)) { shapes.splice(shapeIdx, 1); break; } } } }, hitTestRect: function (rect) { var idx; var children = this.children; var length = children.length; var hit = false; if (this.overlapsBounds(rect)) { if (QuadRoot.fn.hitTestRect.call(this, rect)) { hit = true; } else { for (idx = 0; idx < length; idx++) { if (children[idx].hitTestRect(rect)) { hit = true; break; } } } } return hit; } }); var ShapesQuadTree = Class.extend({ ROOT_SIZE: 1000, init: function (diagram) { var boundsChangeHandler = proxy(this._boundsChange, this); diagram.bind(ITEMBOUNDSCHANGE, boundsChangeHandler); diagram.bind(ITEMROTATE, boundsChangeHandler); this.initRoots(); }, initRoots: function () { this.rootMap = {}; this.root = new QuadRoot(); }, clear: function () { this.initRoots(); }, _boundsChange: function (e) { if (e.item._quadNode) { e.item._quadNode.remove(e.item); } this.insert(e.item); }, insert: function (shape) { var bounds = shape.bounds(ROTATED); var rootSize = this.ROOT_SIZE; var sectors = this.getSectors(bounds); var x = sectors[0][0]; var y = sectors[1][0]; if (this.inRoot(sectors)) { this.root.insert(shape, bounds); } else { if (!this.rootMap[x]) { this.rootMap[x] = {}; } if (!this.rootMap[x][y]) { this.rootMap[x][y] = new QuadNode(new Rect(x * rootSize, y * rootSize, rootSize, rootSize)); } this.rootMap[x][y].insert(shape, bounds); } }, remove: function (shape) { if (shape._quadNode) { shape._quadNode.remove(shape); } }, inRoot: function (sectors) { return sectors[0].length > 1 || sectors[1].length > 1; }, getSectors: function (rect) { var rootSize = this.ROOT_SIZE; var bottomRight = rect.bottomRight(); var bottomX = math.floor(bottomRight.x / rootSize); var bottomY = math.floor(bottomRight.y / rootSize); var sectors = [ [], [] ]; for (var x = math.floor(rect.x / rootSize); x <= bottomX; x++) { sectors[0].push(x); } for (var y = math.floor(rect.y / rootSize); y <= bottomY; y++) { sectors[1].push(y); } return sectors; }, hitTestRect: function (rect) { var sectors = this.getSectors(rect); var xIdx, yIdx, x, y; var root; if (this.root.hitTestRect(rect)) { return true; } for (xIdx = 0; xIdx < sectors[0].length; xIdx++) { x = sectors[0][xIdx]; for (yIdx = 0; yIdx < sectors[1].length; yIdx++) { y = sectors[1][yIdx]; root = (this.rootMap[x] || {})[y]; if (root && root.hitTestRect(rect)) { return true; } } } return false; } }); function cloneDataItem(dataItem) { var result = dataItem; if (dataItem instanceof kendo.data.Model) { result = dataItem.toJSON(); result[dataItem.idField] = dataItem._defaultId; } return result; } function splitDiagramElements(elements) { var connections = []; var shapes = []; var element, idx; for (idx = 0; idx < elements.length; idx++) { element = elements[idx]; if (element instanceof Shape) { shapes.push(element); } else { connections.push(element); } } return { shapes: shapes, connections: connections }; } function createModel(dataSource, model) { if (dataSource.reader.model) { return new dataSource.reader.model(model); } return new kendo.data.ObservableObject(model); } function clearField(field, model) { if (defined(model[field])) { model.set(field, null); } } function copyDefaultOptions(mainOptions, elementOptions, fields) { var field; for (var idx = 0; idx < fields.length; idx++) { field = fields[idx]; if (elementOptions && !defined(elementOptions[field])) { elementOptions[field] = mainOptions[field]; } } } function translateToOrigin(visual) { var bbox = visual.drawingContainer().clippedBBox(null); if (bbox.origin.x !== 0 || bbox.origin.y !== 0) { visual.position(-bbox.origin.x, -bbox.origin.y); } } dataviz.ui.plugin(Diagram); deepExtend(diagram, { Shape: Shape, Connection: Connection, Connector: Connector, DiagramToolBar: DiagramToolBar, QuadNode: QuadNode, QuadRoot: QuadRoot, ShapesQuadTree: ShapesQuadTree, PopupEditor: PopupEditor }); }(window.kendo.jQuery)); }, typeof define == 'function' && define.amd ? define : function (a1, a2, a3) { (a3 || a2)(); })); (function (f, define) { define('kendo.dataviz.diagram', [ 'kendo.data', 'kendo.draganddrop', 'kendo.userevents', 'kendo.mobile.scroller', 'kendo.drawing', 'dataviz/diagram/utils', 'dataviz/diagram/math', 'dataviz/diagram/svg', 'dataviz/diagram/services', 'dataviz/diagram/layout', 'dataviz/diagram/dom' ], f); }(function () { var __meta__ = { id: 'dataviz.diagram', name: 'Diagram', category: 'dataviz', description: 'The Kendo DataViz Diagram ', depends: [ 'data', 'userevents', 'mobile.scroller', 'draganddrop', 'drawing', 'dataviz.core', 'dataviz.themes', 'toolbar' ], features: [ { id: 'dataviz.diagram-pdf-export', name: 'PDF export', description: 'Export Diagram as PDF', depends: ['pdf'] }, { id: 'dataviz.diagram-editing', name: 'Editing', description: 'Support for model editing', depends: [ 'editable', 'window', 'dropdownlist' ] } ] }; }, typeof define == 'function' && define.amd ? define : function (a1, a2, a3) { (a3 || a2)(); }));