add the Schematic function

main
ColsonZhang 2021-01-26 22:51:08 +08:00
parent 10a8ce7c7a
commit 443922f90a
177 changed files with 187768 additions and 10381 deletions

View File

@ -1,10 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="jquery-1.11.2.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div class="jade"></div>
</body>
</html>

10346
MyJade/jquery-1.11.2.js vendored

File diff suppressed because it is too large Load Diff

View File

@ -1,22 +0,0 @@
$(document).ready(function () {
$('.jade').each(function(index, div) {
// var j = new jade_defs.jade();
// jade_defs.services(j);
// j.setup(div);
});
});
class Jade{
constructor(){
this.top_level = $( '<div class="jade-top-level">' +
' <div class="jade-tabs-div">'+
'</div>');
}
}

View File

View File

@ -6,9 +6,9 @@
## 项目框架 ## 项目框架
### MyJade ### Schematic
能够绘制电路原理图的web前端代码主要参考MIT的Jade项目 能够绘制电路原理图的web前端代码主要采用mxGraph代码构建
### Server ### Server
@ -42,6 +42,12 @@
## 更新日志 ## 更新日志
* 2021年1月26日重大更新增加了前端电路绘制功能demo
* 改变技术方案放弃采用MIT的Jade电路绘制前端改为使用mxGraph设计电路绘制功能
* 当前已经可以实现绘制简单的电路原理图并将拓扑关系提取到xml格式文件中后续可设计程序将xml转换为spice文件
* 当前已经增加了几个简单的N_Mosfet P_Mosfet Vdd Gnd Resistor等元件的图表
* 但是,还有很多细节需要继续填充
* 2021年1月26日修复了一些小bug增加了一些配置信息 * 2021年1月26日修复了一些小bug增加了一些配置信息
* 增加了README的内容 * 增加了README的内容

91957
Schematic/mxClient.js Normal file

File diff suppressed because it is too large Load Diff

1881
Schematic/mxClient.min.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
## 有用的mxgraph's demo # 有用的mxgraph's demo
* dynamictoolbar.html * dynamictoolbar.html
* permissions.html * permissions.html

73
Schematic/server.py Normal file
View File

@ -0,0 +1,73 @@
#!/usr/bin/env python
import logging
import os
import sys
import time
import tornado.httpserver
import tornado.ioloop
import tornado.log
import tornado.web
class IndexHandler(tornado.web.RequestHandler):
def get(self, path):
path = os.path.normpath(path)
if os.path.join(path, '').startswith(os.path.join('..', '')) or os.path.isabs(path):
raise tornado.web.HTTPError(403)
if not os.path.isdir(path):
raise tornado.web.HTTPError(404)
self.write('<html>\n<head><title>Index of /')
escaped_title = tornado.web.escape.xhtml_escape(path if path != '.' else '')
self.write(escaped_title)
self.write('</title></head>\n<body>\n<h1>Index of /')
self.write(escaped_title)
self.write('</h1>\n<hr />\n<table>\n')
for f in (['..'] if path != '.' else []) + sorted([i for i in os.listdir(path) if not i.startswith('.')]):
self.write('<tr><td><a href="')
absf = os.path.join(path, f)
self.write(tornado.web.escape.url_escape(f).replace('+', '%20'))
if os.path.isdir(absf):
f += '/'
self.write('/')
self.write('">')
self.write(tornado.web.escape.xhtml_escape(f))
self.write('</a></td><td>')
lstat_result = os.lstat(absf)
self.write(tornado.web.escape.xhtml_escape(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(lstat_result.st_mtime))))
self.write('</td><td align="right">')
self.write(tornado.web.escape.xhtml_escape(str(lstat_result.st_size)))
self.write('</td></tr>\n')
self.finish('</table>\n<hr />\n<div>Powered by <a href="https://github.com/m13253/tornado-simple-http-server" target="_blank">TornadoSimpleHTTPServer</a></div></body>\n</html>\n')
def main():
listen_address = ''
listen_port = 8000
try:
if len(sys.argv) == 2:
listen_port = int(sys.argv[1])
elif len(sys.argv) == 3:
listen_address = sys.argv[1]
listen_port = int(sys.argv[2])
assert 0 <= listen_port <= 65535
except (AssertionError, ValueError) as e:
raise ValueError('port must be a number between 0 and 65535')
tornado.log.enable_pretty_logging()
application = tornado.web.Application(
[
('/(.*/)', IndexHandler),
('/()', IndexHandler),
('/(.*)', tornado.web.StaticFileHandler, {'path': '.', 'default_filename': ''}),
],
gzip=False,
static_hash_cache=False,
)
http_server = tornado.httpserver.HTTPServer(application)
http_server.listen(listen_port, listen_address)
logging.info('Listening on %s:%s' % (listen_address or '[::]' if ':' not in listen_address else '[%s]' % listen_address, listen_port))
tornado.ioloop.IOLoop.instance().start()
if __name__ == '__main__':
main()

View File

@ -0,0 +1,166 @@
div.mxRubberband {
position: absolute;
overflow: hidden;
border-style: solid;
border-width: 1px;
border-color: #0000FF;
background: #0077FF;
}
.mxCellEditor {
background: url(data:image/gif;base64,R0lGODlhMAAwAIAAAP///wAAACH5BAEAAAAALAAAAAAwADAAAAIxhI+py+0Po5y02ouz3rz7D4biSJbmiabqyrbuC8fyTNf2jef6zvf+DwwKh8Si8egpAAA7);
_background: url('../images/transparent.gif');
border-color: transparent;
border-style: solid;
display: inline-block;
position: absolute;
overflow: visible;
word-wrap: normal;
border-width: 0;
min-width: 1px;
resize: none;
padding: 0px;
margin: 0px;
}
.mxPlainTextEditor * {
padding: 0px;
margin: 0px;
}
div.mxWindow {
-webkit-box-shadow: 3px 3px 12px #C0C0C0;
-moz-box-shadow: 3px 3px 12px #C0C0C0;
box-shadow: 3px 3px 12px #C0C0C0;
background: url(data:image/gif;base64,R0lGODlhGgAUAIAAAOzs7PDw8CH5BAAAAAAALAAAAAAaABQAAAIijI+py70Ao5y02lud3lzhD4ZUR5aPiKajyZbqq7YyB9dhAQA7);
_background: url('../images/window.gif');
border:1px solid #c3c3c3;
position: absolute;
overflow: hidden;
z-index: 1;
}
table.mxWindow {
border-collapse: collapse;
table-layout: fixed;
font-family: Arial;
font-size: 8pt;
}
td.mxWindowTitle {
background: url(data:image/gif;base64,R0lGODlhFwAXAMQAANfX18rKyuHh4c7OzsDAwMHBwc/Pz+Li4uTk5NHR0dvb2+jo6O/v79/f3/n5+dnZ2dbW1uPj44yMjNPT0+Dg4N3d3ebm5szMzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAAAAAAALAAAAAAXABcAAAWQICESxWiW5Ck6bOu+MMvMdG3f86LvfO/rlqBwSCwaj8ikUohoOp/QaDNCrVqvWKpgezhsv+AwmEIum89ocmPNbrvf64p8Tq/b5Yq8fs/v5x+AgYKDhIAAh4iJiouHEI6PkJGSjhOVlpeYmZUJnJ2en6CcBqMDpaanqKgXq6ytrq+rAbKztLW2shK5uru8vbkhADs=) repeat-x;
_background: url('../images/window-title.gif') repeat-x;
text-overflow: ellipsis;
white-space: nowrap;
text-align: center;
font-weight: bold;
overflow: hidden;
height: 13px;
padding: 2px;
padding-top: 4px;
padding-bottom: 6px;
color: black;
}
td.mxWindowPane {
vertical-align: top;
padding: 0px;
}
div.mxWindowPane {
overflow: hidden;
position: relative;
}
td.mxWindowPane td {
font-family: Arial;
font-size: 8pt;
}
td.mxWindowPane input, td.mxWindowPane select, td.mxWindowPane textarea, td.mxWindowPane radio {
border-color: #8C8C8C;
border-style: solid;
border-width: 1px;
font-family: Arial;
font-size: 8pt;
padding: 1px;
}
td.mxWindowPane button {
background: url(data:image/gif;base64,R0lGODlhCgATALMAAP7+/t7e3vj4+Ojo6OXl5e/v7/n5+fb29vPz8/39/e3t7fHx8e7u7v///wAAAAAAACH5BAAAAAAALAAAAAAKABMAAAQ2MMlJhb0Y6c2X/2AhjiRjnqiirizqMkEsz0Rt30Ou7y8K/ouDcEg0GI9IgHLJbDif0Kh06owAADs=) repeat-x;
_background: url('../images/button.gif') repeat-x;
font-family: Arial;
font-size: 8pt;
padding: 2px;
float: left;
}
img.mxToolbarItem {
margin-right: 6px;
margin-bottom: 6px;
border-width: 1px;
}
select.mxToolbarCombo {
vertical-align: top;
border-style: inset;
border-width: 2px;
}
div.mxToolbarComboContainer {
padding: 2px;
}
img.mxToolbarMode {
margin: 2px;
margin-right: 4px;
margin-bottom: 4px;
border-width: 0px;
}
img.mxToolbarModeSelected {
margin: 0px;
margin-right: 2px;
margin-bottom: 2px;
border-width: 2px;
border-style: inset;
}
div.mxTooltip {
-webkit-box-shadow: 3px 3px 12px #C0C0C0;
-moz-box-shadow: 3px 3px 12px #C0C0C0;
box-shadow: 3px 3px 12px #C0C0C0;
background: #FFFFCC;
border-style: solid;
border-width: 1px;
border-color: black;
font-family: Arial;
font-size: 8pt;
position: absolute;
cursor: default;
padding: 4px;
color: black;
}
div.mxPopupMenu {
-webkit-box-shadow: 3px 3px 12px #C0C0C0;
-moz-box-shadow: 3px 3px 12px #C0C0C0;
box-shadow: 3px 3px 12px #C0C0C0;
background: url(data:image/gif;base64,R0lGODlhGgAUAIAAAOzs7PDw8CH5BAAAAAAALAAAAAAaABQAAAIijI+py70Ao5y02lud3lzhD4ZUR5aPiKajyZbqq7YyB9dhAQA7);
_background: url('../images/window.gif');
position: absolute;
border-style: solid;
border-width: 1px;
border-color: black;
}
table.mxPopupMenu {
border-collapse: collapse;
margin-top: 1px;
margin-bottom: 1px;
}
tr.mxPopupMenuItem {
color: black;
cursor: pointer;
}
tr.mxPopupMenuItemHover {
background-color: #000066;
color: #FFFFFF;
cursor: pointer;
}
td.mxPopupMenuItem {
padding: 2px 30px 2px 10px;
white-space: nowrap;
font-family: Arial;
font-size: 8pt;
}
td.mxPopupMenuIcon {
background-color: #D0D0D0;
padding: 2px 4px 2px 4px;
}
.mxDisabled {
opacity: 0.2 !important;
cursor:default !important;
}

View File

@ -0,0 +1,18 @@
div.mxTooltip {
filter:progid:DXImageTransform.Microsoft.DropShadow(OffX=4, OffY=4,
Color='#A2A2A2', Positive='true');
}
div.mxPopupMenu {
filter:progid:DXImageTransform.Microsoft.DropShadow(OffX=4, OffY=4,
Color='#C0C0C0', Positive='true');
}
div.mxWindow {
_filter:progid:DXImageTransform.Microsoft.DropShadow(OffX=4, OffY=4,
Color='#C0C0C0', Positive='true');
}
td.mxWindowTitle {
_height: 23px;
}
.mxDisabled {
filter:alpha(opacity=20) !important;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 877 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 907 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 878 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 843 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 845 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 425 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 B

View File

@ -0,0 +1,126 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxDefaultKeyHandler
*
* Binds keycodes to actionnames in an editor. This aggregates an internal
* <handler> and extends the implementation of <mxKeyHandler.escape> to not
* only cancel the editing, but also hide the properties dialog and fire an
* <mxEditor.escape> event via <editor>. An instance of this class is created
* by <mxEditor> and stored in <mxEditor.keyHandler>.
*
* Example:
*
* Bind the delete key to the delete action in an existing editor.
*
* (code)
* var keyHandler = new mxDefaultKeyHandler(editor);
* keyHandler.bindAction(46, 'delete');
* (end)
*
* Codec:
*
* This class uses the <mxDefaultKeyHandlerCodec> to read configuration
* data into an existing instance. See <mxDefaultKeyHandlerCodec> for a
* description of the configuration format.
*
* Keycodes:
*
* See <mxKeyHandler>.
*
* An <mxEvent.ESCAPE> event is fired via the editor if the escape key is
* pressed.
*
* Constructor: mxDefaultKeyHandler
*
* Constructs a new default key handler for the <mxEditor.graph> in the
* given <mxEditor>. (The editor may be null if a prototypical instance for
* a <mxDefaultKeyHandlerCodec> is created.)
*
* Parameters:
*
* editor - Reference to the enclosing <mxEditor>.
*/
function mxDefaultKeyHandler(editor)
{
if (editor != null)
{
this.editor = editor;
this.handler = new mxKeyHandler(editor.graph);
// Extends the escape function of the internal key
// handle to hide the properties dialog and fire
// the escape event via the editor instance
var old = this.handler.escape;
this.handler.escape = function(evt)
{
old.apply(this, arguments);
editor.hideProperties();
editor.fireEvent(new mxEventObject(mxEvent.ESCAPE, 'event', evt));
};
}
};
/**
* Variable: editor
*
* Reference to the enclosing <mxEditor>.
*/
mxDefaultKeyHandler.prototype.editor = null;
/**
* Variable: handler
*
* Holds the <mxKeyHandler> for key event handling.
*/
mxDefaultKeyHandler.prototype.handler = null;
/**
* Function: bindAction
*
* Binds the specified keycode to the given action in <editor>. The
* optional control flag specifies if the control key must be pressed
* to trigger the action.
*
* Parameters:
*
* code - Integer that specifies the keycode.
* action - Name of the action to execute in <editor>.
* control - Optional boolean that specifies if control must be pressed.
* Default is false.
*/
mxDefaultKeyHandler.prototype.bindAction = function (code, action, control)
{
var keyHandler = mxUtils.bind(this, function()
{
this.editor.execute(action);
});
// Binds the function to control-down keycode
if (control)
{
this.handler.bindControlKey(code, keyHandler);
}
// Binds the function to the normal keycode
else
{
this.handler.bindKey(code, keyHandler);
}
};
/**
* Function: destroy
*
* Destroys the <handler> associated with this object. This does normally
* not need to be called, the <handler> is destroyed automatically when the
* window unloads (in IE) by <mxEditor>.
*/
mxDefaultKeyHandler.prototype.destroy = function ()
{
this.handler.destroy();
this.handler = null;
};

View File

@ -0,0 +1,306 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxDefaultPopupMenu
*
* Creates popupmenus for mouse events. This object holds an XML node
* which is a description of the popup menu to be created. In
* <createMenu>, the configuration is applied to the context and
* the resulting menu items are added to the menu dynamically. See
* <createMenu> for a description of the configuration format.
*
* This class does not create the DOM nodes required for the popup menu, it
* only parses an XML description to invoke the respective methods on an
* <mxPopupMenu> each time the menu is displayed.
*
* Codec:
*
* This class uses the <mxDefaultPopupMenuCodec> to read configuration
* data into an existing instance, however, the actual parsing is done
* by this class during program execution, so the format is described
* below.
*
* Constructor: mxDefaultPopupMenu
*
* Constructs a new popupmenu-factory based on given configuration.
*
* Paramaters:
*
* config - XML node that contains the configuration data.
*/
function mxDefaultPopupMenu(config)
{
this.config = config;
};
/**
* Variable: imageBasePath
*
* Base path for all icon attributes in the config. Default is null.
*/
mxDefaultPopupMenu.prototype.imageBasePath = null;
/**
* Variable: config
*
* XML node used as the description of new menu items. This node is
* used in <createMenu> to dynamically create the menu items if their
* respective conditions evaluate to true for the given arguments.
*/
mxDefaultPopupMenu.prototype.config = null;
/**
* Function: createMenu
*
* This function is called from <mxEditor> to add items to the
* given menu based on <config>. The config is a sequence of
* the following nodes and attributes.
*
* Child Nodes:
*
* add - Adds a new menu item. See below for attributes.
* separator - Adds a separator. No attributes.
* condition - Adds a custom condition. Name attribute.
*
* The add-node may have a child node that defines a function to be invoked
* before the action is executed (or instead of an action to be executed).
*
* Attributes:
*
* as - Resource key for the label (needs entry in property file).
* action - Name of the action to execute in enclosing editor.
* icon - Optional icon (relative/absolute URL).
* iconCls - Optional CSS class for the icon.
* if - Optional name of condition that must be true (see below).
* enabled-if - Optional name of condition that specifies if the menu item
* should be enabled.
* name - Name of custom condition. Only for condition nodes.
*
* Conditions:
*
* nocell - No cell under the mouse.
* ncells - More than one cell selected.
* notRoot - Drilling position is other than home.
* cell - Cell under the mouse.
* notEmpty - Exactly one cell with children under mouse.
* expandable - Exactly one expandable cell under mouse.
* collapsable - Exactly one collapsable cell under mouse.
* validRoot - Exactly one cell which is a possible root under mouse.
* swimlane - Exactly one cell which is a swimlane under mouse.
*
* Example:
*
* To add a new item for a given action to the popupmenu:
*
* (code)
* <mxDefaultPopupMenu as="popupHandler">
* <add as="delete" action="delete" icon="images/delete.gif" if="cell"/>
* </mxDefaultPopupMenu>
* (end)
*
* To add a new item for a custom function:
*
* (code)
* <mxDefaultPopupMenu as="popupHandler">
* <add as="action1"><![CDATA[
* function (editor, cell, evt)
* {
* editor.execute('action1', cell, 'myArg');
* }
* ]]></add>
* </mxDefaultPopupMenu>
* (end)
*
* The above example invokes action1 with an additional third argument via
* the editor instance. The third argument is passed to the function that
* defines action1. If the add-node has no action-attribute, then only the
* function defined in the text content is executed, otherwise first the
* function and then the action defined in the action-attribute is
* executed. The function in the text content has 3 arguments, namely the
* <mxEditor> instance, the <mxCell> instance under the mouse, and the
* native mouse event.
*
* Custom Conditions:
*
* To add a new condition for popupmenu items:
*
* (code)
* <condition name="condition1"><![CDATA[
* function (editor, cell, evt)
* {
* return cell != null;
* }
* ]]></condition>
* (end)
*
* The new condition can then be used in any item as follows:
*
* (code)
* <add as="action1" action="action1" icon="action1.gif" if="condition1"/>
* (end)
*
* The order in which the items and conditions appear is not significant as
* all connditions are evaluated before any items are created.
*
* Parameters:
*
* editor - Enclosing <mxEditor> instance.
* menu - <mxPopupMenu> that is used for adding items and separators.
* cell - Optional <mxCell> which is under the mousepointer.
* evt - Optional mouse event which triggered the menu.
*/
mxDefaultPopupMenu.prototype.createMenu = function(editor, menu, cell, evt)
{
if (this.config != null)
{
var conditions = this.createConditions(editor, cell, evt);
var item = this.config.firstChild;
this.addItems(editor, menu, cell, evt, conditions, item, null);
}
};
/**
* Function: addItems
*
* Recursively adds the given items and all of its children into the given menu.
*
* Parameters:
*
* editor - Enclosing <mxEditor> instance.
* menu - <mxPopupMenu> that is used for adding items and separators.
* cell - Optional <mxCell> which is under the mousepointer.
* evt - Optional mouse event which triggered the menu.
* conditions - Array of names boolean conditions.
* item - XML node that represents the current menu item.
* parent - DOM node that represents the parent menu item.
*/
mxDefaultPopupMenu.prototype.addItems = function(editor, menu, cell, evt, conditions, item, parent)
{
var addSeparator = false;
while (item != null)
{
if (item.nodeName == 'add')
{
var condition = item.getAttribute('if');
if (condition == null || conditions[condition])
{
var as = item.getAttribute('as');
as = mxResources.get(as) || as;
var funct = mxUtils.eval(mxUtils.getTextContent(item));
var action = item.getAttribute('action');
var icon = item.getAttribute('icon');
var iconCls = item.getAttribute('iconCls');
var enabledCond = item.getAttribute('enabled-if');
var enabled = enabledCond == null || conditions[enabledCond];
if (addSeparator)
{
menu.addSeparator(parent);
addSeparator = false;
}
if (icon != null && this.imageBasePath)
{
icon = this.imageBasePath + icon;
}
var row = this.addAction(menu, editor, as, icon, funct, action, cell, parent, iconCls, enabled);
this.addItems(editor, menu, cell, evt, conditions, item.firstChild, row);
}
}
else if (item.nodeName == 'separator')
{
addSeparator = true;
}
item = item.nextSibling;
}
};
/**
* Function: addAction
*
* Helper method to bind an action to a new menu item.
*
* Parameters:
*
* menu - <mxPopupMenu> that is used for adding items and separators.
* editor - Enclosing <mxEditor> instance.
* lab - String that represents the label of the menu item.
* icon - Optional URL that represents the icon of the menu item.
* action - Optional name of the action to execute in the given editor.
* funct - Optional function to execute before the optional action. The
* function takes an <mxEditor>, the <mxCell> under the mouse and the
* mouse event that triggered the call.
* cell - Optional <mxCell> to use as an argument for the action.
* parent - DOM node that represents the parent menu item.
* iconCls - Optional CSS class for the menu icon.
* enabled - Optional boolean that specifies if the menu item is enabled.
* Default is true.
*/
mxDefaultPopupMenu.prototype.addAction = function(menu, editor, lab, icon, funct, action, cell, parent, iconCls, enabled)
{
var clickHandler = function(evt)
{
if (typeof(funct) == 'function')
{
funct.call(editor, editor, cell, evt);
}
if (action != null)
{
editor.execute(action, cell, evt);
}
};
return menu.addItem(lab, icon, clickHandler, parent, iconCls, enabled);
};
/**
* Function: createConditions
*
* Evaluates the default conditions for the given context.
*/
mxDefaultPopupMenu.prototype.createConditions = function(editor, cell, evt)
{
// Creates array with conditions
var model = editor.graph.getModel();
var childCount = model.getChildCount(cell);
// Adds some frequently used conditions
var conditions = [];
conditions['nocell'] = cell == null;
conditions['ncells'] = editor.graph.getSelectionCount() > 1;
conditions['notRoot'] = model.getRoot() !=
model.getParent(editor.graph.getDefaultParent());
conditions['cell'] = cell != null;
var isCell = cell != null && editor.graph.getSelectionCount() == 1;
conditions['nonEmpty'] = isCell && childCount > 0;
conditions['expandable'] = isCell && editor.graph.isCellFoldable(cell, false);
conditions['collapsable'] = isCell && editor.graph.isCellFoldable(cell, true);
conditions['validRoot'] = isCell && editor.graph.isValidRoot(cell);
conditions['emptyValidRoot'] = conditions['validRoot'] && childCount == 0;
conditions['swimlane'] = isCell && editor.graph.isSwimlane(cell);
// Evaluates dynamic conditions from config file
var condNodes = this.config.getElementsByTagName('condition');
for (var i=0; i<condNodes.length; i++)
{
var funct = mxUtils.eval(mxUtils.getTextContent(condNodes[i]));
var name = condNodes[i].getAttribute('name');
if (name != null && typeof(funct) == 'function')
{
conditions[name] = funct(editor, cell, evt);
}
}
return conditions;
};

View File

@ -0,0 +1,564 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxDefaultToolbar
*
* Toolbar for the editor. This modifies the state of the graph
* or inserts new cells upon mouse clicks.
*
* Example:
*
* Create a toolbar with a button to copy the selection into the clipboard,
* and a combo box with one action to paste the selection from the clipboard
* into the graph.
*
* (code)
* var toolbar = new mxDefaultToolbar(container, editor);
* toolbar.addItem('Copy', null, 'copy');
*
* var combo = toolbar.addActionCombo('More actions...');
* toolbar.addActionOption(combo, 'Paste', 'paste');
* (end)
*
* Codec:
*
* This class uses the <mxDefaultToolbarCodec> to read configuration
* data into an existing instance. See <mxDefaultToolbarCodec> for a
* description of the configuration format.
*
* Constructor: mxDefaultToolbar
*
* Constructs a new toolbar for the given container and editor. The
* container and editor may be null if a prototypical instance for a
* <mxDefaultKeyHandlerCodec> is created.
*
* Parameters:
*
* container - DOM node that contains the toolbar.
* editor - Reference to the enclosing <mxEditor>.
*/
function mxDefaultToolbar(container, editor)
{
this.editor = editor;
if (container != null && editor != null)
{
this.init(container);
}
};
/**
* Variable: editor
*
* Reference to the enclosing <mxEditor>.
*/
mxDefaultToolbar.prototype.editor = null;
/**
* Variable: toolbar
*
* Holds the internal <mxToolbar>.
*/
mxDefaultToolbar.prototype.toolbar = null;
/**
* Variable: resetHandler
*
* Reference to the function used to reset the <toolbar>.
*/
mxDefaultToolbar.prototype.resetHandler = null;
/**
* Variable: spacing
*
* Defines the spacing between existing and new vertices in
* gridSize units when a new vertex is dropped on an existing
* cell. Default is 4 (40 pixels).
*/
mxDefaultToolbar.prototype.spacing = 4;
/**
* Variable: connectOnDrop
*
* Specifies if elements should be connected if new cells are dropped onto
* connectable elements. Default is false.
*/
mxDefaultToolbar.prototype.connectOnDrop = false;
/**
* Function: init
*
* Constructs the <toolbar> for the given container and installs a listener
* that updates the <mxEditor.insertFunction> on <editor> if an item is
* selected in the toolbar. This assumes that <editor> is not null.
*
* Parameters:
*
* container - DOM node that contains the toolbar.
*/
mxDefaultToolbar.prototype.init = function(container)
{
if (container != null)
{
this.toolbar = new mxToolbar(container);
// Installs the insert function in the editor if an item is
// selected in the toolbar
this.toolbar.addListener(mxEvent.SELECT, mxUtils.bind(this, function(sender, evt)
{
var funct = evt.getProperty('function');
if (funct != null)
{
this.editor.insertFunction = mxUtils.bind(this, function()
{
funct.apply(this, arguments);
this.toolbar.resetMode();
});
}
else
{
this.editor.insertFunction = null;
}
}));
// Resets the selected tool after a doubleclick or escape keystroke
this.resetHandler = mxUtils.bind(this, function()
{
if (this.toolbar != null)
{
this.toolbar.resetMode(true);
}
});
this.editor.graph.addListener(mxEvent.DOUBLE_CLICK, this.resetHandler);
this.editor.addListener(mxEvent.ESCAPE, this.resetHandler);
}
};
/**
* Function: addItem
*
* Adds a new item that executes the given action in <editor>. The title,
* icon and pressedIcon are used to display the toolbar item.
*
* Parameters:
*
* title - String that represents the title (tooltip) for the item.
* icon - URL of the icon to be used for displaying the item.
* action - Name of the action to execute when the item is clicked.
* pressed - Optional URL of the icon for the pressed state.
*/
mxDefaultToolbar.prototype.addItem = function(title, icon, action, pressed)
{
var clickHandler = mxUtils.bind(this, function()
{
if (action != null && action.length > 0)
{
this.editor.execute(action);
}
});
return this.toolbar.addItem(title, icon, clickHandler, pressed);
};
/**
* Function: addSeparator
*
* Adds a vertical separator using the optional icon.
*
* Parameters:
*
* icon - Optional URL of the icon that represents the vertical separator.
* Default is <mxClient.imageBasePath> + '/separator.gif'.
*/
mxDefaultToolbar.prototype.addSeparator = function(icon)
{
icon = icon || mxClient.imageBasePath + '/separator.gif';
this.toolbar.addSeparator(icon);
};
/**
* Function: addCombo
*
* Helper method to invoke <mxToolbar.addCombo> on <toolbar> and return the
* resulting DOM node.
*/
mxDefaultToolbar.prototype.addCombo = function()
{
return this.toolbar.addCombo();
};
/**
* Function: addActionCombo
*
* Helper method to invoke <mxToolbar.addActionCombo> on <toolbar> using
* the given title and return the resulting DOM node.
*
* Parameters:
*
* title - String that represents the title of the combo.
*/
mxDefaultToolbar.prototype.addActionCombo = function(title)
{
return this.toolbar.addActionCombo(title);
};
/**
* Function: addActionOption
*
* Binds the given action to a option with the specified label in the
* given combo. Combo is an object returned from an earlier call to
* <addCombo> or <addActionCombo>.
*
* Parameters:
*
* combo - DOM node that represents the combo box.
* title - String that represents the title of the combo.
* action - Name of the action to execute in <editor>.
*/
mxDefaultToolbar.prototype.addActionOption = function(combo, title, action)
{
var clickHandler = mxUtils.bind(this, function()
{
this.editor.execute(action);
});
this.addOption(combo, title, clickHandler);
};
/**
* Function: addOption
*
* Helper method to invoke <mxToolbar.addOption> on <toolbar> and return
* the resulting DOM node that represents the option.
*
* Parameters:
*
* combo - DOM node that represents the combo box.
* title - String that represents the title of the combo.
* value - Object that represents the value of the option.
*/
mxDefaultToolbar.prototype.addOption = function(combo, title, value)
{
return this.toolbar.addOption(combo, title, value);
};
/**
* Function: addMode
*
* Creates an item for selecting the given mode in the <editor>'s graph.
* Supported modenames are select, connect and pan.
*
* Parameters:
*
* title - String that represents the title of the item.
* icon - URL of the icon that represents the item.
* mode - String that represents the mode name to be used in
* <mxEditor.setMode>.
* pressed - Optional URL of the icon that represents the pressed state.
* funct - Optional JavaScript function that takes the <mxEditor> as the
* first and only argument that is executed after the mode has been
* selected.
*/
mxDefaultToolbar.prototype.addMode = function(title, icon, mode, pressed, funct)
{
var clickHandler = mxUtils.bind(this, function()
{
this.editor.setMode(mode);
if (funct != null)
{
funct(this.editor);
}
});
return this.toolbar.addSwitchMode(title, icon, clickHandler, pressed);
};
/**
* Function: addPrototype
*
* Creates an item for inserting a clone of the specified prototype cell into
* the <editor>'s graph. The ptype may either be a cell or a function that
* returns a cell.
*
* Parameters:
*
* title - String that represents the title of the item.
* icon - URL of the icon that represents the item.
* ptype - Function or object that represents the prototype cell. If ptype
* is a function then it is invoked with no arguments to create new
* instances.
* pressed - Optional URL of the icon that represents the pressed state.
* insert - Optional JavaScript function that handles an insert of the new
* cell. This function takes the <mxEditor>, new cell to be inserted, mouse
* event and optional <mxCell> under the mouse pointer as arguments.
* toggle - Optional boolean that specifies if the item can be toggled.
* Default is true.
*/
mxDefaultToolbar.prototype.addPrototype = function(title, icon, ptype, pressed, insert, toggle)
{
// Creates a wrapper function that is in charge of constructing
// the new cell instance to be inserted into the graph
var factory = mxUtils.bind(this, function()
{
if (typeof(ptype) == 'function')
{
return ptype();
}
else if (ptype != null)
{
return this.editor.graph.cloneCell(ptype);
}
return null;
});
// Defines the function for a click event on the graph
// after this item has been selected in the toolbar
var clickHandler = mxUtils.bind(this, function(evt, cell)
{
if (typeof(insert) == 'function')
{
insert(this.editor, factory(), evt, cell);
}
else
{
this.drop(factory(), evt, cell);
}
this.toolbar.resetMode();
mxEvent.consume(evt);
});
var img = this.toolbar.addMode(title, icon, clickHandler, pressed, null, toggle);
// Creates a wrapper function that calls the click handler without
// the graph argument
var dropHandler = function(graph, evt, cell)
{
clickHandler(evt, cell);
};
this.installDropHandler(img, dropHandler);
return img;
};
/**
* Function: drop
*
* Handles a drop from a toolbar item to the graph. The given vertex
* represents the new cell to be inserted. This invokes <insert> or
* <connect> depending on the given target cell.
*
* Parameters:
*
* vertex - <mxCell> to be inserted.
* evt - Mouse event that represents the drop.
* target - Optional <mxCell> that represents the drop target.
*/
mxDefaultToolbar.prototype.drop = function(vertex, evt, target)
{
var graph = this.editor.graph;
var model = graph.getModel();
if (target == null ||
model.isEdge(target) ||
!this.connectOnDrop ||
!graph.isCellConnectable(target))
{
while (target != null &&
!graph.isValidDropTarget(target, [vertex], evt))
{
target = model.getParent(target);
}
this.insert(vertex, evt, target);
}
else
{
this.connect(vertex, evt, target);
}
};
/**
* Function: insert
*
* Handles a drop by inserting the given vertex into the given parent cell
* or the default parent if no parent is specified.
*
* Parameters:
*
* vertex - <mxCell> to be inserted.
* evt - Mouse event that represents the drop.
* parent - Optional <mxCell> that represents the parent.
*/
mxDefaultToolbar.prototype.insert = function(vertex, evt, target)
{
var graph = this.editor.graph;
if (graph.canImportCell(vertex))
{
var x = mxEvent.getClientX(evt);
var y = mxEvent.getClientY(evt);
var pt = mxUtils.convertPoint(graph.container, x, y);
// Splits the target edge or inserts into target group
if (graph.isSplitEnabled() &&
graph.isSplitTarget(target, [vertex], evt))
{
return graph.splitEdge(target, [vertex], null, pt.x, pt.y);
}
else
{
return this.editor.addVertex(target, vertex, pt.x, pt.y);
}
}
return null;
};
/**
* Function: connect
*
* Handles a drop by connecting the given vertex to the given source cell.
*
* vertex - <mxCell> to be inserted.
* evt - Mouse event that represents the drop.
* source - Optional <mxCell> that represents the source terminal.
*/
mxDefaultToolbar.prototype.connect = function(vertex, evt, source)
{
var graph = this.editor.graph;
var model = graph.getModel();
if (source != null &&
graph.isCellConnectable(vertex) &&
graph.isEdgeValid(null, source, vertex))
{
var edge = null;
model.beginUpdate();
try
{
var geo = model.getGeometry(source);
var g = model.getGeometry(vertex).clone();
// Moves the vertex away from the drop target that will
// be used as the source for the new connection
g.x = geo.x + (geo.width - g.width) / 2;
g.y = geo.y + (geo.height - g.height) / 2;
var step = this.spacing * graph.gridSize;
var dist = model.getDirectedEdgeCount(source, true) * 20;
if (this.editor.horizontalFlow)
{
g.x += (g.width + geo.width) / 2 + step + dist;
}
else
{
g.y += (g.height + geo.height) / 2 + step + dist;
}
vertex.setGeometry(g);
// Fires two add-events with the code below - should be fixed
// to only fire one add event for both inserts
var parent = model.getParent(source);
graph.addCell(vertex, parent);
graph.constrainChild(vertex);
// Creates the edge using the editor instance and calls
// the second function that fires an add event
edge = this.editor.createEdge(source, vertex);
if (model.getGeometry(edge) == null)
{
var edgeGeometry = new mxGeometry();
edgeGeometry.relative = true;
model.setGeometry(edge, edgeGeometry);
}
graph.addEdge(edge, parent, source, vertex);
}
finally
{
model.endUpdate();
}
graph.setSelectionCells([vertex, edge]);
graph.scrollCellToVisible(vertex);
}
};
/**
* Function: installDropHandler
*
* Makes the given img draggable using the given function for handling a
* drop event.
*
* Parameters:
*
* img - DOM node that represents the image.
* dropHandler - Function that handles a drop of the image.
*/
mxDefaultToolbar.prototype.installDropHandler = function (img, dropHandler)
{
var sprite = document.createElement('img');
sprite.setAttribute('src', img.getAttribute('src'));
// Handles delayed loading of the images
var loader = mxUtils.bind(this, function(evt)
{
// Preview uses the image node with double size. Later this can be
// changed to use a separate preview and guides, but for this the
// dropHandler must use the additional x- and y-arguments and the
// dragsource which makeDraggable returns much be configured to
// use guides via mxDragSource.isGuidesEnabled.
sprite.style.width = (2 * img.offsetWidth) + 'px';
sprite.style.height = (2 * img.offsetHeight) + 'px';
mxUtils.makeDraggable(img, this.editor.graph, dropHandler,
sprite);
mxEvent.removeListener(sprite, 'load', loader);
});
if (mxClient.IS_IE)
{
loader();
}
else
{
mxEvent.addListener(sprite, 'load', loader);
}
};
/**
* Function: destroy
*
* Destroys the <toolbar> associated with this object and removes all
* installed listeners. This does normally not need to be called, the
* <toolbar> is destroyed automatically when the window unloads (in IE) by
* <mxEditor>.
*/
mxDefaultToolbar.prototype.destroy = function ()
{
if (this.resetHandler != null)
{
this.editor.graph.removeListener('dblclick', this.resetHandler);
this.editor.removeListener('escape', this.resetHandler);
this.resetHandler = null;
}
if (this.toolbar != null)
{
this.toolbar.destroy();
this.toolbar = null;
}
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,314 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxCellHighlight
*
* A helper class to highlight cells. Here is an example for a given cell.
*
* (code)
* var highlight = new mxCellHighlight(graph, '#ff0000', 2);
* highlight.highlight(graph.view.getState(cell)));
* (end)
*
* Constructor: mxCellHighlight
*
* Constructs a cell highlight.
*/
function mxCellHighlight(graph, highlightColor, strokeWidth, dashed)
{
if (graph != null)
{
this.graph = graph;
this.highlightColor = (highlightColor != null) ? highlightColor : mxConstants.DEFAULT_VALID_COLOR;
this.strokeWidth = (strokeWidth != null) ? strokeWidth : mxConstants.HIGHLIGHT_STROKEWIDTH;
this.dashed = (dashed != null) ? dashed : false;
this.opacity = mxConstants.HIGHLIGHT_OPACITY;
// Updates the marker if the graph changes
this.repaintHandler = mxUtils.bind(this, function()
{
// Updates reference to state
if (this.state != null)
{
var tmp = this.graph.view.getState(this.state.cell);
if (tmp == null)
{
this.hide();
}
else
{
this.state = tmp;
this.repaint();
}
}
});
this.graph.getView().addListener(mxEvent.SCALE, this.repaintHandler);
this.graph.getView().addListener(mxEvent.TRANSLATE, this.repaintHandler);
this.graph.getView().addListener(mxEvent.SCALE_AND_TRANSLATE, this.repaintHandler);
this.graph.getModel().addListener(mxEvent.CHANGE, this.repaintHandler);
// Hides the marker if the current root changes
this.resetHandler = mxUtils.bind(this, function()
{
this.hide();
});
this.graph.getView().addListener(mxEvent.DOWN, this.resetHandler);
this.graph.getView().addListener(mxEvent.UP, this.resetHandler);
}
};
/**
* Variable: keepOnTop
*
* Specifies if the highlights should appear on top of everything
* else in the overlay pane. Default is false.
*/
mxCellHighlight.prototype.keepOnTop = false;
/**
* Variable: graph
*
* Reference to the enclosing <mxGraph>.
*/
mxCellHighlight.prototype.graph = null;
/**
* Variable: state
*
* Reference to the <mxCellState>.
*/
mxCellHighlight.prototype.state = null;
/**
* Variable: spacing
*
* Specifies the spacing between the highlight for vertices and the vertex.
* Default is 2.
*/
mxCellHighlight.prototype.spacing = 2;
/**
* Variable: resetHandler
*
* Holds the handler that automatically invokes reset if the highlight
* should be hidden.
*/
mxCellHighlight.prototype.resetHandler = null;
/**
* Function: setHighlightColor
*
* Sets the color of the rectangle used to highlight drop targets.
*
* Parameters:
*
* color - String that represents the new highlight color.
*/
mxCellHighlight.prototype.setHighlightColor = function(color)
{
this.highlightColor = color;
if (this.shape != null)
{
this.shape.stroke = color;
}
};
/**
* Function: drawHighlight
*
* Creates and returns the highlight shape for the given state.
*/
mxCellHighlight.prototype.drawHighlight = function()
{
this.shape = this.createShape();
this.repaint();
if (!this.keepOnTop && this.shape.node.parentNode.firstChild != this.shape.node)
{
this.shape.node.parentNode.insertBefore(this.shape.node, this.shape.node.parentNode.firstChild);
}
};
/**
* Function: createShape
*
* Creates and returns the highlight shape for the given state.
*/
mxCellHighlight.prototype.createShape = function()
{
var shape = this.graph.cellRenderer.createShape(this.state);
shape.svgStrokeTolerance = this.graph.tolerance;
shape.points = this.state.absolutePoints;
shape.apply(this.state);
shape.stroke = this.highlightColor;
shape.opacity = this.opacity;
shape.isDashed = this.dashed;
shape.isShadow = false;
shape.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ? mxConstants.DIALECT_VML : mxConstants.DIALECT_SVG;
shape.init(this.graph.getView().getOverlayPane());
mxEvent.redirectMouseEvents(shape.node, this.graph, this.state);
if (this.graph.dialect != mxConstants.DIALECT_SVG)
{
shape.pointerEvents = false;
}
else
{
shape.svgPointerEvents = 'stroke';
}
return shape;
};
/**
* Function: getStrokeWidth
*
* Returns the stroke width.
*/
mxCellHighlight.prototype.getStrokeWidth = function(state)
{
return this.strokeWidth;
};
/**
* Function: repaint
*
* Updates the highlight after a change of the model or view.
*/
mxCellHighlight.prototype.repaint = function()
{
if (this.state != null && this.shape != null)
{
this.shape.scale = this.state.view.scale;
if (this.graph.model.isEdge(this.state.cell))
{
this.shape.strokewidth = this.getStrokeWidth();
this.shape.points = this.state.absolutePoints;
this.shape.outline = false;
}
else
{
this.shape.bounds = new mxRectangle(this.state.x - this.spacing, this.state.y - this.spacing,
this.state.width + 2 * this.spacing, this.state.height + 2 * this.spacing);
this.shape.rotation = Number(this.state.style[mxConstants.STYLE_ROTATION] || '0');
this.shape.strokewidth = this.getStrokeWidth() / this.state.view.scale;
this.shape.outline = true;
}
// Uses cursor from shape in highlight
if (this.state.shape != null)
{
this.shape.setCursor(this.state.shape.getCursor());
}
// Workaround for event transparency in VML with transparent color
// is to use a non-transparent color with near zero opacity
if (mxClient.IS_QUIRKS || document.documentMode == 8)
{
if (this.shape.stroke == 'transparent')
{
// KNOWN: Quirks mode does not seem to catch events if
// we do not force an update of the DOM via a change such
// as mxLog.debug. Since IE6 is EOL we do not add a fix.
this.shape.stroke = 'white';
this.shape.opacity = 1;
}
else
{
this.shape.opacity = this.opacity;
}
}
this.shape.redraw();
}
};
/**
* Function: hide
*
* Resets the state of the cell marker.
*/
mxCellHighlight.prototype.hide = function()
{
this.highlight(null);
};
/**
* Function: mark
*
* Marks the <markedState> and fires a <mark> event.
*/
mxCellHighlight.prototype.highlight = function(state)
{
if (this.state != state)
{
if (this.shape != null)
{
this.shape.destroy();
this.shape = null;
}
this.state = state;
if (this.state != null)
{
this.drawHighlight();
}
}
};
/**
* Function: isHighlightAt
*
* Returns true if this highlight is at the given position.
*/
mxCellHighlight.prototype.isHighlightAt = function(x, y)
{
var hit = false;
// Quirks mode is currently not supported as it used a different coordinate system
if (this.shape != null && document.elementFromPoint != null && !mxClient.IS_QUIRKS)
{
var elt = document.elementFromPoint(x, y);
while (elt != null)
{
if (elt == this.shape.node)
{
hit = true;
break;
}
elt = elt.parentNode;
}
}
return hit;
};
/**
* Function: destroy
*
* Destroys the handler and all its resources and DOM nodes.
*/
mxCellHighlight.prototype.destroy = function()
{
this.graph.getView().removeListener(this.resetHandler);
this.graph.getView().removeListener(this.repaintHandler);
this.graph.getModel().removeListener(this.repaintHandler);
if (this.shape != null)
{
this.shape.destroy();
this.shape = null;
}
};

View File

@ -0,0 +1,430 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxCellMarker
*
* A helper class to process mouse locations and highlight cells.
*
* Helper class to highlight cells. To add a cell marker to an existing graph
* for highlighting all cells, the following code is used:
*
* (code)
* var marker = new mxCellMarker(graph);
* graph.addMouseListener({
* mouseDown: function() {},
* mouseMove: function(sender, me)
* {
* marker.process(me);
* },
* mouseUp: function() {}
* });
* (end)
*
* Event: mxEvent.MARK
*
* Fires after a cell has been marked or unmarked. The <code>state</code>
* property contains the marked <mxCellState> or null if no state is marked.
*
* Constructor: mxCellMarker
*
* Constructs a new cell marker.
*
* Parameters:
*
* graph - Reference to the enclosing <mxGraph>.
* validColor - Optional marker color for valid states. Default is
* <mxConstants.DEFAULT_VALID_COLOR>.
* invalidColor - Optional marker color for invalid states. Default is
* <mxConstants.DEFAULT_INVALID_COLOR>.
* hotspot - Portion of the width and hight where a state intersects a
* given coordinate pair. A value of 0 means always highlight. Default is
* <mxConstants.DEFAULT_HOTSPOT>.
*/
function mxCellMarker(graph, validColor, invalidColor, hotspot)
{
mxEventSource.call(this);
if (graph != null)
{
this.graph = graph;
this.validColor = (validColor != null) ? validColor : mxConstants.DEFAULT_VALID_COLOR;
this.invalidColor = (invalidColor != null) ? invalidColor : mxConstants.DEFAULT_INVALID_COLOR;
this.hotspot = (hotspot != null) ? hotspot : mxConstants.DEFAULT_HOTSPOT;
this.highlight = new mxCellHighlight(graph);
}
};
/**
* Extends mxEventSource.
*/
mxUtils.extend(mxCellMarker, mxEventSource);
/**
* Variable: graph
*
* Reference to the enclosing <mxGraph>.
*/
mxCellMarker.prototype.graph = null;
/**
* Variable: enabled
*
* Specifies if the marker is enabled. Default is true.
*/
mxCellMarker.prototype.enabled = true;
/**
* Variable: hotspot
*
* Specifies the portion of the width and height that should trigger
* a highlight. The area around the center of the cell to be marked is used
* as the hotspot. Possible values are between 0 and 1. Default is
* mxConstants.DEFAULT_HOTSPOT.
*/
mxCellMarker.prototype.hotspot = mxConstants.DEFAULT_HOTSPOT;
/**
* Variable: hotspotEnabled
*
* Specifies if the hotspot is enabled. Default is false.
*/
mxCellMarker.prototype.hotspotEnabled = false;
/**
* Variable: validColor
*
* Holds the valid marker color.
*/
mxCellMarker.prototype.validColor = null;
/**
* Variable: invalidColor
*
* Holds the invalid marker color.
*/
mxCellMarker.prototype.invalidColor = null;
/**
* Variable: currentColor
*
* Holds the current marker color.
*/
mxCellMarker.prototype.currentColor = null;
/**
* Variable: validState
*
* Holds the marked <mxCellState> if it is valid.
*/
mxCellMarker.prototype.validState = null;
/**
* Variable: markedState
*
* Holds the marked <mxCellState>.
*/
mxCellMarker.prototype.markedState = null;
/**
* Function: setEnabled
*
* Enables or disables event handling. This implementation
* updates <enabled>.
*
* Parameters:
*
* enabled - Boolean that specifies the new enabled state.
*/
mxCellMarker.prototype.setEnabled = function(enabled)
{
this.enabled = enabled;
};
/**
* Function: isEnabled
*
* Returns true if events are handled. This implementation
* returns <enabled>.
*/
mxCellMarker.prototype.isEnabled = function()
{
return this.enabled;
};
/**
* Function: setHotspot
*
* Sets the <hotspot>.
*/
mxCellMarker.prototype.setHotspot = function(hotspot)
{
this.hotspot = hotspot;
};
/**
* Function: getHotspot
*
* Returns the <hotspot>.
*/
mxCellMarker.prototype.getHotspot = function()
{
return this.hotspot;
};
/**
* Function: setHotspotEnabled
*
* Specifies whether the hotspot should be used in <intersects>.
*/
mxCellMarker.prototype.setHotspotEnabled = function(enabled)
{
this.hotspotEnabled = enabled;
};
/**
* Function: isHotspotEnabled
*
* Returns true if hotspot is used in <intersects>.
*/
mxCellMarker.prototype.isHotspotEnabled = function()
{
return this.hotspotEnabled;
};
/**
* Function: hasValidState
*
* Returns true if <validState> is not null.
*/
mxCellMarker.prototype.hasValidState = function()
{
return this.validState != null;
};
/**
* Function: getValidState
*
* Returns the <validState>.
*/
mxCellMarker.prototype.getValidState = function()
{
return this.validState;
};
/**
* Function: getMarkedState
*
* Returns the <markedState>.
*/
mxCellMarker.prototype.getMarkedState = function()
{
return this.markedState;
};
/**
* Function: reset
*
* Resets the state of the cell marker.
*/
mxCellMarker.prototype.reset = function()
{
this.validState = null;
if (this.markedState != null)
{
this.markedState = null;
this.unmark();
}
};
/**
* Function: process
*
* Processes the given event and cell and marks the state returned by
* <getState> with the color returned by <getMarkerColor>. If the
* markerColor is not null, then the state is stored in <markedState>. If
* <isValidState> returns true, then the state is stored in <validState>
* regardless of the marker color. The state is returned regardless of the
* marker color and valid state.
*/
mxCellMarker.prototype.process = function(me)
{
var state = null;
if (this.isEnabled())
{
state = this.getState(me);
this.setCurrentState(state, me);
}
return state;
};
/**
* Function: setCurrentState
*
* Sets and marks the current valid state.
*/
mxCellMarker.prototype.setCurrentState = function(state, me, color)
{
var isValid = (state != null) ? this.isValidState(state) : false;
color = (color != null) ? color : this.getMarkerColor(me.getEvent(), state, isValid);
if (isValid)
{
this.validState = state;
}
else
{
this.validState = null;
}
if (state != this.markedState || color != this.currentColor)
{
this.currentColor = color;
if (state != null && this.currentColor != null)
{
this.markedState = state;
this.mark();
}
else if (this.markedState != null)
{
this.markedState = null;
this.unmark();
}
}
};
/**
* Function: markCell
*
* Marks the given cell using the given color, or <validColor> if no color is specified.
*/
mxCellMarker.prototype.markCell = function(cell, color)
{
var state = this.graph.getView().getState(cell);
if (state != null)
{
this.currentColor = (color != null) ? color : this.validColor;
this.markedState = state;
this.mark();
}
};
/**
* Function: mark
*
* Marks the <markedState> and fires a <mark> event.
*/
mxCellMarker.prototype.mark = function()
{
this.highlight.setHighlightColor(this.currentColor);
this.highlight.highlight(this.markedState);
this.fireEvent(new mxEventObject(mxEvent.MARK, 'state', this.markedState));
};
/**
* Function: unmark
*
* Hides the marker and fires a <mark> event.
*/
mxCellMarker.prototype.unmark = function()
{
this.mark();
};
/**
* Function: isValidState
*
* Returns true if the given <mxCellState> is a valid state. If this
* returns true, then the state is stored in <validState>. The return value
* of this method is used as the argument for <getMarkerColor>.
*/
mxCellMarker.prototype.isValidState = function(state)
{
return true;
};
/**
* Function: getMarkerColor
*
* Returns the valid- or invalidColor depending on the value of isValid.
* The given <mxCellState> is ignored by this implementation.
*/
mxCellMarker.prototype.getMarkerColor = function(evt, state, isValid)
{
return (isValid) ? this.validColor : this.invalidColor;
};
/**
* Function: getState
*
* Uses <getCell>, <getStateToMark> and <intersects> to return the
* <mxCellState> for the given <mxMouseEvent>.
*/
mxCellMarker.prototype.getState = function(me)
{
var view = this.graph.getView();
var cell = this.getCell(me);
var state = this.getStateToMark(view.getState(cell));
return (state != null && this.intersects(state, me)) ? state : null;
};
/**
* Function: getCell
*
* Returns the <mxCell> for the given event and cell. This returns the
* given cell.
*/
mxCellMarker.prototype.getCell = function(me)
{
return me.getCell();
};
/**
* Function: getStateToMark
*
* Returns the <mxCellState> to be marked for the given <mxCellState> under
* the mouse. This returns the given state.
*/
mxCellMarker.prototype.getStateToMark = function(state)
{
return state;
};
/**
* Function: intersects
*
* Returns true if the given coordinate pair intersects the given state.
* This returns true if the <hotspot> is 0 or the coordinates are inside
* the hotspot for the given cell state.
*/
mxCellMarker.prototype.intersects = function(state, me)
{
if (this.hotspotEnabled)
{
return mxUtils.intersectsHotspot(state, me.getGraphX(), me.getGraphY(),
this.hotspot, mxConstants.MIN_HOTSPOT_SIZE,
mxConstants.MAX_HOTSPOT_SIZE);
}
return true;
};
/**
* Function: destroy
*
* Destroys the handler and all its resources and DOM nodes.
*/
mxCellMarker.prototype.destroy = function()
{
this.graph.getView().removeListener(this.resetHandler);
this.graph.getModel().removeListener(this.resetHandler);
this.highlight.destroy();
};

View File

@ -0,0 +1,145 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxCellTracker
*
* Event handler that highlights cells. Inherits from <mxCellMarker>.
*
* Example:
*
* (code)
* new mxCellTracker(graph, '#00FF00');
* (end)
*
* For detecting dragEnter, dragOver and dragLeave on cells, the following
* code can be used:
*
* (code)
* graph.addMouseListener(
* {
* cell: null,
* mouseDown: function(sender, me) { },
* mouseMove: function(sender, me)
* {
* var tmp = me.getCell();
*
* if (tmp != this.cell)
* {
* if (this.cell != null)
* {
* this.dragLeave(me.getEvent(), this.cell);
* }
*
* this.cell = tmp;
*
* if (this.cell != null)
* {
* this.dragEnter(me.getEvent(), this.cell);
* }
* }
*
* if (this.cell != null)
* {
* this.dragOver(me.getEvent(), this.cell);
* }
* },
* mouseUp: function(sender, me) { },
* dragEnter: function(evt, cell)
* {
* mxLog.debug('dragEnter', cell.value);
* },
* dragOver: function(evt, cell)
* {
* mxLog.debug('dragOver', cell.value);
* },
* dragLeave: function(evt, cell)
* {
* mxLog.debug('dragLeave', cell.value);
* }
* });
* (end)
*
* Constructor: mxCellTracker
*
* Constructs an event handler that highlights cells.
*
* Parameters:
*
* graph - Reference to the enclosing <mxGraph>.
* color - Color of the highlight. Default is blue.
* funct - Optional JavaScript function that is used to override
* <mxCellMarker.getCell>.
*/
function mxCellTracker(graph, color, funct)
{
mxCellMarker.call(this, graph, color);
this.graph.addMouseListener(this);
if (funct != null)
{
this.getCell = funct;
}
// Automatic deallocation of memory
if (mxClient.IS_IE)
{
mxEvent.addListener(window, 'unload', mxUtils.bind(this, function()
{
this.destroy();
}));
}
};
/**
* Extends mxCellMarker.
*/
mxUtils.extend(mxCellTracker, mxCellMarker);
/**
* Function: mouseDown
*
* Ignores the event. The event is not consumed.
*/
mxCellTracker.prototype.mouseDown = function(sender, me) { };
/**
* Function: mouseMove
*
* Handles the event by highlighting the cell under the mousepointer if it
* is over the hotspot region of the cell.
*/
mxCellTracker.prototype.mouseMove = function(sender, me)
{
if (this.isEnabled())
{
this.process(me);
}
};
/**
* Function: mouseUp
*
* Handles the event by reseting the highlight.
*/
mxCellTracker.prototype.mouseUp = function(sender, me) { };
/**
* Function: destroy
*
* Destroys the object and all its resources and DOM nodes. This doesn't
* normally need to be called. It is called automatically when the window
* unloads.
*/
mxCellTracker.prototype.destroy = function()
{
if (!this.destroyed)
{
this.destroyed = true;
this.graph.removeMouseListener(this);
mxCellMarker.prototype.destroy.apply(this);
}
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,517 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxConstraintHandler
*
* Handles constraints on connection targets. This class is in charge of
* showing fixed points when the mouse is over a vertex and handles constraints
* to establish new connections.
*
* Constructor: mxConstraintHandler
*
* Constructs an new constraint handler.
*
* Parameters:
*
* graph - Reference to the enclosing <mxGraph>.
* factoryMethod - Optional function to create the edge. The function takes
* the source and target <mxCell> as the first and second argument and
* returns the <mxCell> that represents the new edge.
*/
function mxConstraintHandler(graph)
{
this.graph = graph;
// Adds a graph model listener to update the current focus on changes
this.resetHandler = mxUtils.bind(this, function(sender, evt)
{
if (this.currentFocus != null && this.graph.view.getState(this.currentFocus.cell) == null)
{
this.reset();
}
else
{
this.redraw();
}
});
this.graph.model.addListener(mxEvent.CHANGE, this.resetHandler);
this.graph.view.addListener(mxEvent.SCALE_AND_TRANSLATE, this.resetHandler);
this.graph.view.addListener(mxEvent.TRANSLATE, this.resetHandler);
this.graph.view.addListener(mxEvent.SCALE, this.resetHandler);
this.graph.addListener(mxEvent.ROOT, this.resetHandler);
};
/**
* Variable: pointImage
*
* <mxImage> to be used as the image for fixed connection points.
*/
mxConstraintHandler.prototype.pointImage = new mxImage(mxClient.imageBasePath + '/point.gif', 5, 5);
/**
* Variable: graph
*
* Reference to the enclosing <mxGraph>.
*/
mxConstraintHandler.prototype.graph = null;
/**
* Variable: enabled
*
* Specifies if events are handled. Default is true.
*/
mxConstraintHandler.prototype.enabled = true;
/**
* Variable: highlightColor
*
* Specifies the color for the highlight. Default is <mxConstants.DEFAULT_VALID_COLOR>.
*/
mxConstraintHandler.prototype.highlightColor = mxConstants.DEFAULT_VALID_COLOR;
/**
* Function: isEnabled
*
* Returns true if events are handled. This implementation
* returns <enabled>.
*/
mxConstraintHandler.prototype.isEnabled = function()
{
return this.enabled;
};
/**
* Function: setEnabled
*
* Enables or disables event handling. This implementation
* updates <enabled>.
*
* Parameters:
*
* enabled - Boolean that specifies the new enabled state.
*/
mxConstraintHandler.prototype.setEnabled = function(enabled)
{
this.enabled = enabled;
};
/**
* Function: reset
*
* Resets the state of this handler.
*/
mxConstraintHandler.prototype.reset = function()
{
if (this.focusIcons != null)
{
for (var i = 0; i < this.focusIcons.length; i++)
{
this.focusIcons[i].destroy();
}
this.focusIcons = null;
}
if (this.focusHighlight != null)
{
this.focusHighlight.destroy();
this.focusHighlight = null;
}
this.currentConstraint = null;
this.currentFocusArea = null;
this.currentPoint = null;
this.currentFocus = null;
this.focusPoints = null;
};
/**
* Function: getTolerance
*
* Returns the tolerance to be used for intersecting connection points. This
* implementation returns <mxGraph.tolerance>.
*
* Parameters:
*
* me - <mxMouseEvent> whose tolerance should be returned.
*/
mxConstraintHandler.prototype.getTolerance = function(me)
{
return this.graph.getTolerance();
};
/**
* Function: getImageForConstraint
*
* Returns the tolerance to be used for intersecting connection points.
*/
mxConstraintHandler.prototype.getImageForConstraint = function(state, constraint, point)
{
return this.pointImage;
};
/**
* Function: isEventIgnored
*
* Returns true if the given <mxMouseEvent> should be ignored in <update>. This
* implementation always returns false.
*/
mxConstraintHandler.prototype.isEventIgnored = function(me, source)
{
return false;
};
/**
* Function: isStateIgnored
*
* Returns true if the given state should be ignored. This always returns false.
*/
mxConstraintHandler.prototype.isStateIgnored = function(state, source)
{
return false;
};
/**
* Function: destroyIcons
*
* Destroys the <focusIcons> if they exist.
*/
mxConstraintHandler.prototype.destroyIcons = function()
{
if (this.focusIcons != null)
{
for (var i = 0; i < this.focusIcons.length; i++)
{
this.focusIcons[i].destroy();
}
this.focusIcons = null;
this.focusPoints = null;
}
};
/**
* Function: destroyFocusHighlight
*
* Destroys the <focusHighlight> if one exists.
*/
mxConstraintHandler.prototype.destroyFocusHighlight = function()
{
if (this.focusHighlight != null)
{
this.focusHighlight.destroy();
this.focusHighlight = null;
}
};
/**
* Function: isKeepFocusEvent
*
* Returns true if the current focused state should not be changed for the given event.
* This returns true if shift and alt are pressed.
*/
mxConstraintHandler.prototype.isKeepFocusEvent = function(me)
{
return mxEvent.isShiftDown(me.getEvent());
};
/**
* Function: getCellForEvent
*
* Returns the cell for the given event.
*/
mxConstraintHandler.prototype.getCellForEvent = function(me, point)
{
var cell = me.getCell();
// Gets cell under actual point if different from event location
if (cell == null && point != null && (me.getGraphX() != point.x || me.getGraphY() != point.y))
{
cell = this.graph.getCellAt(point.x, point.y);
}
// Uses connectable parent vertex if one exists
if (cell != null && !this.graph.isCellConnectable(cell))
{
var parent = this.graph.getModel().getParent(cell);
if (this.graph.getModel().isVertex(parent) && this.graph.isCellConnectable(parent))
{
cell = parent;
}
}
return (this.graph.isCellLocked(cell)) ? null : cell;
};
/**
* Function: update
*
* Updates the state of this handler based on the given <mxMouseEvent>.
* Source is a boolean indicating if the cell is a source or target.
*/
mxConstraintHandler.prototype.update = function(me, source, existingEdge, point)
{
if (this.isEnabled() && !this.isEventIgnored(me))
{
// Lazy installation of mouseleave handler
if (this.mouseleaveHandler == null && this.graph.container != null)
{
this.mouseleaveHandler = mxUtils.bind(this, function()
{
this.reset();
});
mxEvent.addListener(this.graph.container, 'mouseleave', this.resetHandler);
}
var tol = this.getTolerance(me);
var x = (point != null) ? point.x : me.getGraphX();
var y = (point != null) ? point.y : me.getGraphY();
var grid = new mxRectangle(x - tol, y - tol, 2 * tol, 2 * tol);
var mouse = new mxRectangle(me.getGraphX() - tol, me.getGraphY() - tol, 2 * tol, 2 * tol);
var state = this.graph.view.getState(this.getCellForEvent(me, point));
// Keeps focus icons visible while over vertex bounds and no other cell under mouse or shift is pressed
if (!this.isKeepFocusEvent(me) && (this.currentFocusArea == null || this.currentFocus == null ||
(state != null) || !this.graph.getModel().isVertex(this.currentFocus.cell) ||
!mxUtils.intersects(this.currentFocusArea, mouse)) && (state != this.currentFocus))
{
this.currentFocusArea = null;
this.currentFocus = null;
this.setFocus(me, state, source);
}
this.currentConstraint = null;
this.currentPoint = null;
var minDistSq = null;
if (this.focusIcons != null && this.constraints != null &&
(state == null || this.currentFocus == state))
{
var cx = mouse.getCenterX();
var cy = mouse.getCenterY();
for (var i = 0; i < this.focusIcons.length; i++)
{
var dx = cx - this.focusIcons[i].bounds.getCenterX();
var dy = cy - this.focusIcons[i].bounds.getCenterY();
var tmp = dx * dx + dy * dy;
if ((this.intersects(this.focusIcons[i], mouse, source, existingEdge) || (point != null &&
this.intersects(this.focusIcons[i], grid, source, existingEdge))) &&
(minDistSq == null || tmp < minDistSq))
{
this.currentConstraint = this.constraints[i];
this.currentPoint = this.focusPoints[i];
minDistSq = tmp;
var tmp = this.focusIcons[i].bounds.clone();
tmp.grow(mxConstants.HIGHLIGHT_SIZE + 1);
tmp.width -= 1;
tmp.height -= 1;
if (this.focusHighlight == null)
{
var hl = this.createHighlightShape();
hl.dialect = (this.graph.dialect == mxConstants.DIALECT_SVG) ?
mxConstants.DIALECT_SVG : mxConstants.DIALECT_VML;
hl.pointerEvents = false;
hl.init(this.graph.getView().getOverlayPane());
this.focusHighlight = hl;
var getState = mxUtils.bind(this, function()
{
return (this.currentFocus != null) ? this.currentFocus : state;
});
mxEvent.redirectMouseEvents(hl.node, this.graph, getState);
}
this.focusHighlight.bounds = tmp;
this.focusHighlight.redraw();
}
}
}
if (this.currentConstraint == null)
{
this.destroyFocusHighlight();
}
}
else
{
this.currentConstraint = null;
this.currentFocus = null;
this.currentPoint = null;
}
};
/**
* Function: redraw
*
* Transfers the focus to the given state as a source or target terminal. If
* the handler is not enabled then the outline is painted, but the constraints
* are ignored.
*/
mxConstraintHandler.prototype.redraw = function()
{
if (this.currentFocus != null && this.constraints != null && this.focusIcons != null)
{
var state = this.graph.view.getState(this.currentFocus.cell);
this.currentFocus = state;
this.currentFocusArea = new mxRectangle(state.x, state.y, state.width, state.height);
for (var i = 0; i < this.constraints.length; i++)
{
var cp = this.graph.getConnectionPoint(state, this.constraints[i]);
var img = this.getImageForConstraint(state, this.constraints[i], cp);
var bounds = new mxRectangle(Math.round(cp.x - img.width / 2),
Math.round(cp.y - img.height / 2), img.width, img.height);
this.focusIcons[i].bounds = bounds;
this.focusIcons[i].redraw();
this.currentFocusArea.add(this.focusIcons[i].bounds);
this.focusPoints[i] = cp;
}
}
};
/**
* Function: setFocus
*
* Transfers the focus to the given state as a source or target terminal. If
* the handler is not enabled then the outline is painted, but the constraints
* are ignored.
*/
mxConstraintHandler.prototype.setFocus = function(me, state, source)
{
this.constraints = (state != null && !this.isStateIgnored(state, source) &&
this.graph.isCellConnectable(state.cell)) ? ((this.isEnabled()) ?
(this.graph.getAllConnectionConstraints(state, source) || []) : []) : null;
// Only uses cells which have constraints
if (this.constraints != null)
{
this.currentFocus = state;
this.currentFocusArea = new mxRectangle(state.x, state.y, state.width, state.height);
if (this.focusIcons != null)
{
for (var i = 0; i < this.focusIcons.length; i++)
{
this.focusIcons[i].destroy();
}
this.focusIcons = null;
this.focusPoints = null;
}
this.focusPoints = [];
this.focusIcons = [];
for (var i = 0; i < this.constraints.length; i++)
{
var cp = this.graph.getConnectionPoint(state, this.constraints[i]);
var img = this.getImageForConstraint(state, this.constraints[i], cp);
var src = img.src;
var bounds = new mxRectangle(Math.round(cp.x - img.width / 2),
Math.round(cp.y - img.height / 2), img.width, img.height);
var icon = new mxImageShape(bounds, src);
icon.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ?
mxConstants.DIALECT_MIXEDHTML : mxConstants.DIALECT_SVG;
icon.preserveImageAspect = false;
icon.init(this.graph.getView().getDecoratorPane());
// Fixes lost event tracking for images in quirks / IE8 standards
if (mxClient.IS_QUIRKS || document.documentMode == 8)
{
mxEvent.addListener(icon.node, 'dragstart', function(evt)
{
mxEvent.consume(evt);
return false;
});
}
// Move the icon behind all other overlays
if (icon.node.previousSibling != null)
{
icon.node.parentNode.insertBefore(icon.node, icon.node.parentNode.firstChild);
}
var getState = mxUtils.bind(this, function()
{
return (this.currentFocus != null) ? this.currentFocus : state;
});
icon.redraw();
mxEvent.redirectMouseEvents(icon.node, this.graph, getState);
this.currentFocusArea.add(icon.bounds);
this.focusIcons.push(icon);
this.focusPoints.push(cp);
}
this.currentFocusArea.grow(this.getTolerance(me));
}
else
{
this.destroyIcons();
this.destroyFocusHighlight();
}
};
/**
* Function: createHighlightShape
*
* Create the shape used to paint the highlight.
*
* Returns true if the given icon intersects the given point.
*/
mxConstraintHandler.prototype.createHighlightShape = function()
{
var hl = new mxRectangleShape(null, this.highlightColor, this.highlightColor, mxConstants.HIGHLIGHT_STROKEWIDTH);
hl.opacity = mxConstants.HIGHLIGHT_OPACITY;
return hl;
};
/**
* Function: intersects
*
* Returns true if the given icon intersects the given rectangle.
*/
mxConstraintHandler.prototype.intersects = function(icon, mouse, source, existingEdge)
{
return mxUtils.intersects(icon.bounds, mouse);
};
/**
* Function: destroy
*
* Destroy this handler.
*/
mxConstraintHandler.prototype.destroy = function()
{
this.reset();
if (this.resetHandler != null)
{
this.graph.model.removeListener(this.resetHandler);
this.graph.view.removeListener(this.resetHandler);
this.graph.removeListener(this.resetHandler);
this.resetHandler = null;
}
if (this.mouseleaveHandler != null && this.graph.container != null)
{
mxEvent.removeListener(this.graph.container, 'mouseleave', this.mouseleaveHandler);
this.mouseleaveHandler = null;
}
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,413 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
function mxEdgeSegmentHandler(state)
{
mxEdgeHandler.call(this, state);
};
/**
* Extends mxEdgeHandler.
*/
mxUtils.extend(mxEdgeSegmentHandler, mxElbowEdgeHandler);
/**
* Function: getCurrentPoints
*
* Returns the current absolute points.
*/
mxEdgeSegmentHandler.prototype.getCurrentPoints = function()
{
var pts = this.state.absolutePoints;
if (pts != null)
{
// Special case for straight edges where we add a virtual middle handle for moving the edge
var tol = Math.max(1, this.graph.view.scale);
if (pts.length == 2 || (pts.length == 3 &&
(Math.abs(pts[0].x - pts[1].x) < tol && Math.abs(pts[1].x - pts[2].x) < tol ||
Math.abs(pts[0].y - pts[1].y) < tol && Math.abs(pts[1].y - pts[2].y) < tol)))
{
var cx = pts[0].x + (pts[pts.length - 1].x - pts[0].x) / 2;
var cy = pts[0].y + (pts[pts.length - 1].y - pts[0].y) / 2;
pts = [pts[0], new mxPoint(cx, cy), new mxPoint(cx, cy), pts[pts.length - 1]];
}
}
return pts;
};
/**
* Function: getPreviewPoints
*
* Updates the given preview state taking into account the state of the constraint handler.
*/
mxEdgeSegmentHandler.prototype.getPreviewPoints = function(point)
{
if (this.isSource || this.isTarget)
{
return mxElbowEdgeHandler.prototype.getPreviewPoints.apply(this, arguments);
}
else
{
var pts = this.getCurrentPoints();
var last = this.convertPoint(pts[0].clone(), false);
point = this.convertPoint(point.clone(), false);
var result = [];
for (var i = 1; i < pts.length; i++)
{
var pt = this.convertPoint(pts[i].clone(), false);
if (i == this.index)
{
if (Math.round(last.x - pt.x) == 0)
{
last.x = point.x;
pt.x = point.x;
}
if (Math.round(last.y - pt.y) == 0)
{
last.y = point.y;
pt.y = point.y;
}
}
if (i < pts.length - 1)
{
result.push(pt);
}
last = pt;
}
// Replaces single point that intersects with source or target
if (result.length == 1)
{
var source = this.state.getVisibleTerminalState(true);
var target = this.state.getVisibleTerminalState(false);
var scale = this.state.view.getScale();
var tr = this.state.view.getTranslate();
var x = result[0].x * scale + tr.x;
var y = result[0].y * scale + tr.y;
if ((source != null && mxUtils.contains(source, x, y)) ||
(target != null && mxUtils.contains(target, x, y)))
{
result = [point, point];
}
}
return result;
}
};
/**
* Function: updatePreviewState
*
* Overridden to perform optimization of the edge style result.
*/
mxEdgeSegmentHandler.prototype.updatePreviewState = function(edge, point, terminalState, me)
{
mxEdgeHandler.prototype.updatePreviewState.apply(this, arguments);
// Checks and corrects preview by running edge style again
if (!this.isSource && !this.isTarget)
{
point = this.convertPoint(point.clone(), false);
var pts = edge.absolutePoints;
var pt0 = pts[0];
var pt1 = pts[1];
var result = [];
for (var i = 2; i < pts.length; i++)
{
var pt2 = pts[i];
// Merges adjacent segments only if more than 2 to allow for straight edges
if ((Math.round(pt0.x - pt1.x) != 0 || Math.round(pt1.x - pt2.x) != 0) &&
(Math.round(pt0.y - pt1.y) != 0 || Math.round(pt1.y - pt2.y) != 0))
{
result.push(this.convertPoint(pt1.clone(), false));
}
pt0 = pt1;
pt1 = pt2;
}
var source = this.state.getVisibleTerminalState(true);
var target = this.state.getVisibleTerminalState(false);
var rpts = this.state.absolutePoints;
// A straight line is represented by 3 handles
if (result.length == 0 && (Math.round(pts[0].x - pts[pts.length - 1].x) == 0 ||
Math.round(pts[0].y - pts[pts.length - 1].y) == 0))
{
result = [point, point];
}
// Handles special case of transitions from straight vertical to routed
else if (pts.length == 5 && result.length == 2 && source != null && target != null &&
rpts != null && Math.round(rpts[0].x - rpts[rpts.length - 1].x) == 0)
{
var view = this.graph.getView();
var scale = view.getScale();
var tr = view.getTranslate();
var y0 = view.getRoutingCenterY(source) / scale - tr.y;
// Use fixed connection point y-coordinate if one exists
var sc = this.graph.getConnectionConstraint(edge, source, true);
if (sc != null)
{
var pt = this.graph.getConnectionPoint(source, sc);
if (pt != null)
{
this.convertPoint(pt, false);
y0 = pt.y;
}
}
var ye = view.getRoutingCenterY(target) / scale - tr.y;
// Use fixed connection point y-coordinate if one exists
var tc = this.graph.getConnectionConstraint(edge, target, false);
if (tc)
{
var pt = this.graph.getConnectionPoint(target, tc);
if (pt != null)
{
this.convertPoint(pt, false);
ye = pt.y;
}
}
result = [new mxPoint(point.x, y0), new mxPoint(point.x, ye)];
}
this.points = result;
// LATER: Check if points and result are different
edge.view.updateFixedTerminalPoints(edge, source, target);
edge.view.updatePoints(edge, this.points, source, target);
edge.view.updateFloatingTerminalPoints(edge, source, target);
}
};
/**
* Overriden to merge edge segments.
*/
mxEdgeSegmentHandler.prototype.connect = function(edge, terminal, isSource, isClone, me)
{
var model = this.graph.getModel();
var geo = model.getGeometry(edge);
var result = null;
// Merges adjacent edge segments
if (geo != null && geo.points != null && geo.points.length > 0)
{
var pts = this.abspoints;
var pt0 = pts[0];
var pt1 = pts[1];
result = [];
for (var i = 2; i < pts.length; i++)
{
var pt2 = pts[i];
// Merges adjacent segments only if more than 2 to allow for straight edges
if ((Math.round(pt0.x - pt1.x) != 0 || Math.round(pt1.x - pt2.x) != 0) &&
(Math.round(pt0.y - pt1.y) != 0 || Math.round(pt1.y - pt2.y) != 0))
{
result.push(this.convertPoint(pt1.clone(), false));
}
pt0 = pt1;
pt1 = pt2;
}
}
model.beginUpdate();
try
{
if (result != null)
{
var geo = model.getGeometry(edge);
if (geo != null)
{
geo = geo.clone();
geo.points = result;
model.setGeometry(edge, geo);
}
}
edge = mxEdgeHandler.prototype.connect.apply(this, arguments);
}
finally
{
model.endUpdate();
}
return edge;
};
/**
* Function: getTooltipForNode
*
* Returns no tooltips.
*/
mxEdgeSegmentHandler.prototype.getTooltipForNode = function(node)
{
return null;
};
/**
* Function: start
*
* Starts the handling of the mouse gesture.
*/
mxEdgeSegmentHandler.prototype.start = function(x, y, index)
{
mxEdgeHandler.prototype.start.apply(this, arguments);
if (this.bends != null && this.bends[index] != null &&
!this.isSource && !this.isTarget)
{
mxUtils.setOpacity(this.bends[index].node, 100);
}
};
/**
* Function: createBends
*
* Adds custom bends for the center of each segment.
*/
mxEdgeSegmentHandler.prototype.createBends = function()
{
var bends = [];
// Source
var bend = this.createHandleShape(0);
this.initBend(bend);
bend.setCursor(mxConstants.CURSOR_TERMINAL_HANDLE);
bends.push(bend);
var pts = this.getCurrentPoints();
// Waypoints (segment handles)
if (this.graph.isCellBendable(this.state.cell))
{
if (this.points == null)
{
this.points = [];
}
for (var i = 0; i < pts.length - 1; i++)
{
bend = this.createVirtualBend();
bends.push(bend);
var horizontal = Math.round(pts[i].x - pts[i + 1].x) == 0;
// Special case where dy is 0 as well
if (Math.round(pts[i].y - pts[i + 1].y) == 0 && i < pts.length - 2)
{
horizontal = Math.round(pts[i].x - pts[i + 2].x) == 0;
}
bend.setCursor((horizontal) ? 'col-resize' : 'row-resize');
this.points.push(new mxPoint(0,0));
}
}
// Target
var bend = this.createHandleShape(pts.length);
this.initBend(bend);
bend.setCursor(mxConstants.CURSOR_TERMINAL_HANDLE);
bends.push(bend);
return bends;
};
/**
* Function: redraw
*
* Overridden to invoke <refresh> before the redraw.
*/
mxEdgeSegmentHandler.prototype.redraw = function()
{
this.refresh();
mxEdgeHandler.prototype.redraw.apply(this, arguments);
};
/**
* Function: redrawInnerBends
*
* Updates the position of the custom bends.
*/
mxEdgeSegmentHandler.prototype.redrawInnerBends = function(p0, pe)
{
if (this.graph.isCellBendable(this.state.cell))
{
var pts = this.getCurrentPoints();
if (pts != null && pts.length > 1)
{
var straight = false;
// Puts handle in the center of straight edges
if (pts.length == 4 && Math.round(pts[1].x - pts[2].x) == 0 && Math.round(pts[1].y - pts[2].y) == 0)
{
straight = true;
if (Math.round(pts[0].y - pts[pts.length - 1].y) == 0)
{
var cx = pts[0].x + (pts[pts.length - 1].x - pts[0].x) / 2;
pts[1] = new mxPoint(cx, pts[1].y);
pts[2] = new mxPoint(cx, pts[2].y);
}
else
{
var cy = pts[0].y + (pts[pts.length - 1].y - pts[0].y) / 2;
pts[1] = new mxPoint(pts[1].x, cy);
pts[2] = new mxPoint(pts[2].x, cy);
}
}
for (var i = 0; i < pts.length - 1; i++)
{
if (this.bends[i + 1] != null)
{
var p0 = pts[i];
var pe = pts[i + 1];
var pt = new mxPoint(p0.x + (pe.x - p0.x) / 2, p0.y + (pe.y - p0.y) / 2);
var b = this.bends[i + 1].bounds;
this.bends[i + 1].bounds = new mxRectangle(Math.floor(pt.x - b.width / 2),
Math.floor(pt.y - b.height / 2), b.width, b.height);
this.bends[i + 1].redraw();
if (this.manageLabelHandle)
{
this.checkLabelHandle(this.bends[i + 1].bounds);
}
}
}
if (straight)
{
mxUtils.setOpacity(this.bends[1].node, this.virtualBendOpacity);
mxUtils.setOpacity(this.bends[3].node, this.virtualBendOpacity);
}
}
}
};

View File

@ -0,0 +1,230 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxElbowEdgeHandler
*
* Graph event handler that reconnects edges and modifies control points and
* the edge label location. Uses <mxTerminalMarker> for finding and
* highlighting new source and target vertices. This handler is automatically
* created in <mxGraph.createHandler>. It extends <mxEdgeHandler>.
*
* Constructor: mxEdgeHandler
*
* Constructs an edge handler for the specified <mxCellState>.
*
* Parameters:
*
* state - <mxCellState> of the cell to be modified.
*/
function mxElbowEdgeHandler(state)
{
mxEdgeHandler.call(this, state);
};
/**
* Extends mxEdgeHandler.
*/
mxUtils.extend(mxElbowEdgeHandler, mxEdgeHandler);
/**
* Specifies if a double click on the middle handle should call
* <mxGraph.flipEdge>. Default is true.
*/
mxElbowEdgeHandler.prototype.flipEnabled = true;
/**
* Variable: doubleClickOrientationResource
*
* Specifies the resource key for the tooltip to be displayed on the single
* control point for routed edges. If the resource for this key does not
* exist then the value is used as the error message. Default is
* 'doubleClickOrientation'.
*/
mxElbowEdgeHandler.prototype.doubleClickOrientationResource =
(mxClient.language != 'none') ? 'doubleClickOrientation' : '';
/**
* Function: createBends
*
* Overrides <mxEdgeHandler.createBends> to create custom bends.
*/
mxElbowEdgeHandler.prototype.createBends = function()
{
var bends = [];
// Source
var bend = this.createHandleShape(0);
this.initBend(bend);
bend.setCursor(mxConstants.CURSOR_TERMINAL_HANDLE);
bends.push(bend);
// Virtual
bends.push(this.createVirtualBend(mxUtils.bind(this, function(evt)
{
if (!mxEvent.isConsumed(evt) && this.flipEnabled)
{
this.graph.flipEdge(this.state.cell, evt);
mxEvent.consume(evt);
}
})));
this.points.push(new mxPoint(0,0));
// Target
bend = this.createHandleShape(2);
this.initBend(bend);
bend.setCursor(mxConstants.CURSOR_TERMINAL_HANDLE);
bends.push(bend);
return bends;
};
/**
* Function: createVirtualBend
*
* Creates a virtual bend that supports double clicking and calls
* <mxGraph.flipEdge>.
*/
mxElbowEdgeHandler.prototype.createVirtualBend = function(dblClickHandler)
{
var bend = this.createHandleShape();
this.initBend(bend, dblClickHandler);
bend.setCursor(this.getCursorForBend());
if (!this.graph.isCellBendable(this.state.cell))
{
bend.node.style.display = 'none';
}
return bend;
};
/**
* Function: getCursorForBend
*
* Returns the cursor to be used for the bend.
*/
mxElbowEdgeHandler.prototype.getCursorForBend = function()
{
return (this.state.style[mxConstants.STYLE_EDGE] == mxEdgeStyle.TopToBottom ||
this.state.style[mxConstants.STYLE_EDGE] == mxConstants.EDGESTYLE_TOPTOBOTTOM ||
((this.state.style[mxConstants.STYLE_EDGE] == mxEdgeStyle.ElbowConnector ||
this.state.style[mxConstants.STYLE_EDGE] == mxConstants.EDGESTYLE_ELBOW)&&
this.state.style[mxConstants.STYLE_ELBOW] == mxConstants.ELBOW_VERTICAL)) ?
'row-resize' : 'col-resize';
};
/**
* Function: getTooltipForNode
*
* Returns the tooltip for the given node.
*/
mxElbowEdgeHandler.prototype.getTooltipForNode = function(node)
{
var tip = null;
if (this.bends != null && this.bends[1] != null && (node == this.bends[1].node ||
node.parentNode == this.bends[1].node))
{
tip = this.doubleClickOrientationResource;
tip = mxResources.get(tip) || tip; // translate
}
return tip;
};
/**
* Function: convertPoint
*
* Converts the given point in-place from screen to unscaled, untranslated
* graph coordinates and applies the grid.
*
* Parameters:
*
* point - <mxPoint> to be converted.
* gridEnabled - Boolean that specifies if the grid should be applied.
*/
mxElbowEdgeHandler.prototype.convertPoint = function(point, gridEnabled)
{
var scale = this.graph.getView().getScale();
var tr = this.graph.getView().getTranslate();
var origin = this.state.origin;
if (gridEnabled)
{
point.x = this.graph.snap(point.x);
point.y = this.graph.snap(point.y);
}
point.x = Math.round(point.x / scale - tr.x - origin.x);
point.y = Math.round(point.y / scale - tr.y - origin.y);
return point;
};
/**
* Function: redrawInnerBends
*
* Updates and redraws the inner bends.
*
* Parameters:
*
* p0 - <mxPoint> that represents the location of the first point.
* pe - <mxPoint> that represents the location of the last point.
*/
mxElbowEdgeHandler.prototype.redrawInnerBends = function(p0, pe)
{
var g = this.graph.getModel().getGeometry(this.state.cell);
var pts = this.state.absolutePoints;
var pt = null;
// Keeps the virtual bend on the edge shape
if (pts.length > 1)
{
p0 = pts[1];
pe = pts[pts.length - 2];
}
else if (g.points != null && g.points.length > 0)
{
pt = pts[0];
}
if (pt == null)
{
pt = new mxPoint(p0.x + (pe.x - p0.x) / 2, p0.y + (pe.y - p0.y) / 2);
}
else
{
pt = new mxPoint(this.graph.getView().scale * (pt.x + this.graph.getView().translate.x + this.state.origin.x),
this.graph.getView().scale * (pt.y + this.graph.getView().translate.y + this.state.origin.y));
}
// Makes handle slightly bigger if the yellow label handle
// exists and intersects this green handle
var b = this.bends[1].bounds;
var w = b.width;
var h = b.height;
var bounds = new mxRectangle(Math.round(pt.x - w / 2), Math.round(pt.y - h / 2), w, h);
if (this.manageLabelHandle)
{
this.checkLabelHandle(bounds);
}
else if (this.handleImage == null && this.labelShape.visible && mxUtils.intersects(bounds, this.labelShape.bounds))
{
w = mxConstants.HANDLE_SIZE + 3;
h = mxConstants.HANDLE_SIZE + 3;
bounds = new mxRectangle(Math.floor(pt.x - w / 2), Math.floor(pt.y - h / 2), w, h);
}
this.bends[1].bounds = bounds;
this.bends[1].redraw();
if (this.manageLabelHandle)
{
this.checkLabelHandle(this.bends[1].bounds);
}
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,352 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxHandle
*
* Implements a single custom handle for vertices.
*
* Constructor: mxHandle
*
* Constructs a new handle for the given state.
*
* Parameters:
*
* state - <mxCellState> of the cell to be handled.
*/
function mxHandle(state, cursor, image, shape)
{
this.graph = state.view.graph;
this.state = state;
this.cursor = (cursor != null) ? cursor : this.cursor;
this.image = (image != null) ? image : this.image;
this.shape = (shape != null) ? shape : null;
this.init();
};
/**
* Variable: cursor
*
* Specifies the cursor to be used for this handle. Default is 'default'.
*/
mxHandle.prototype.cursor = 'default';
/**
* Variable: image
*
* Specifies the <mxImage> to be used to render the handle. Default is null.
*/
mxHandle.prototype.image = null;
/**
* Variable: ignoreGrid
*
* Default is false.
*/
mxHandle.prototype.ignoreGrid = false;
/**
* Function: getPosition
*
* Hook for subclassers to return the current position of the handle.
*/
mxHandle.prototype.getPosition = function(bounds) { };
/**
* Function: setPosition
*
* Hooks for subclassers to update the style in the <state>.
*/
mxHandle.prototype.setPosition = function(bounds, pt, me) { };
/**
* Function: execute
*
* Hook for subclassers to execute the handle.
*/
mxHandle.prototype.execute = function(me) { };
/**
* Function: copyStyle
*
* Sets the cell style with the given name to the corresponding value in <state>.
*/
mxHandle.prototype.copyStyle = function(key)
{
this.graph.setCellStyles(key, this.state.style[key], [this.state.cell]);
};
/**
* Function: processEvent
*
* Processes the given <mxMouseEvent> and invokes <setPosition>.
*/
mxHandle.prototype.processEvent = function(me)
{
var scale = this.graph.view.scale;
var tr = this.graph.view.translate;
var pt = new mxPoint(me.getGraphX() / scale - tr.x, me.getGraphY() / scale - tr.y);
// Center shape on mouse cursor
if (this.shape != null && this.shape.bounds != null)
{
pt.x -= this.shape.bounds.width / scale / 4;
pt.y -= this.shape.bounds.height / scale / 4;
}
// Snaps to grid for the rotated position then applies the rotation for the direction after that
var alpha1 = -mxUtils.toRadians(this.getRotation());
var alpha2 = -mxUtils.toRadians(this.getTotalRotation()) - alpha1;
pt = this.flipPoint(this.rotatePoint(this.snapPoint(this.rotatePoint(pt, alpha1),
this.ignoreGrid || !this.graph.isGridEnabledEvent(me.getEvent())), alpha2));
this.setPosition(this.state.getPaintBounds(), pt, me);
this.redraw();
};
/**
* Function: positionChanged
*
* Should be called after <setPosition> in <processEvent>.
* This repaints the state using <mxCellRenderer>.
*/
mxHandle.prototype.positionChanged = function()
{
if (this.state.text != null)
{
this.state.text.apply(this.state);
}
if (this.state.shape != null)
{
this.state.shape.apply(this.state);
}
this.graph.cellRenderer.redraw(this.state, true);
};
/**
* Function: getRotation
*
* Returns the rotation defined in the style of the cell.
*/
mxHandle.prototype.getRotation = function()
{
if (this.state.shape != null)
{
return this.state.shape.getRotation();
}
return 0;
};
/**
* Function: getTotalRotation
*
* Returns the rotation from the style and the rotation from the direction of
* the cell.
*/
mxHandle.prototype.getTotalRotation = function()
{
if (this.state.shape != null)
{
return this.state.shape.getShapeRotation();
}
return 0;
};
/**
* Function: init
*
* Creates and initializes the shapes required for this handle.
*/
mxHandle.prototype.init = function()
{
var html = this.isHtmlRequired();
if (this.image != null)
{
this.shape = new mxImageShape(new mxRectangle(0, 0, this.image.width, this.image.height), this.image.src);
this.shape.preserveImageAspect = false;
}
else if (this.shape == null)
{
this.shape = this.createShape(html);
}
this.initShape(html);
};
/**
* Function: createShape
*
* Creates and returns the shape for this handle.
*/
mxHandle.prototype.createShape = function(html)
{
var bounds = new mxRectangle(0, 0, mxConstants.HANDLE_SIZE, mxConstants.HANDLE_SIZE);
return new mxRectangleShape(bounds, mxConstants.HANDLE_FILLCOLOR, mxConstants.HANDLE_STROKECOLOR);
};
/**
* Function: initShape
*
* Initializes <shape> and sets its cursor.
*/
mxHandle.prototype.initShape = function(html)
{
if (html && this.shape.isHtmlAllowed())
{
this.shape.dialect = mxConstants.DIALECT_STRICTHTML;
this.shape.init(this.graph.container);
}
else
{
this.shape.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ?
mxConstants.DIALECT_MIXEDHTML : mxConstants.DIALECT_SVG;
if (this.cursor != null)
{
this.shape.init(this.graph.getView().getOverlayPane());
}
}
mxEvent.redirectMouseEvents(this.shape.node, this.graph, this.state);
this.shape.node.style.cursor = this.cursor;
};
/**
* Function: redraw
*
* Renders the shape for this handle.
*/
mxHandle.prototype.redraw = function()
{
if (this.shape != null && this.state.shape != null)
{
var pt = this.getPosition(this.state.getPaintBounds());
if (pt != null)
{
var alpha = mxUtils.toRadians(this.getTotalRotation());
pt = this.rotatePoint(this.flipPoint(pt), alpha);
var scale = this.graph.view.scale;
var tr = this.graph.view.translate;
this.shape.bounds.x = Math.floor((pt.x + tr.x) * scale - this.shape.bounds.width / 2);
this.shape.bounds.y = Math.floor((pt.y + tr.y) * scale - this.shape.bounds.height / 2);
// Needed to force update of text bounds
this.shape.redraw();
}
}
};
/**
* Function: isHtmlRequired
*
* Returns true if this handle should be rendered in HTML. This returns true if
* the text node is in the graph container.
*/
mxHandle.prototype.isHtmlRequired = function()
{
return this.state.text != null && this.state.text.node.parentNode == this.graph.container;
};
/**
* Function: rotatePoint
*
* Rotates the point by the given angle.
*/
mxHandle.prototype.rotatePoint = function(pt, alpha)
{
var bounds = this.state.getCellBounds();
var cx = new mxPoint(bounds.getCenterX(), bounds.getCenterY());
var cos = Math.cos(alpha);
var sin = Math.sin(alpha);
return mxUtils.getRotatedPoint(pt, cos, sin, cx);
};
/**
* Function: flipPoint
*
* Flips the given point vertically and/or horizontally.
*/
mxHandle.prototype.flipPoint = function(pt)
{
if (this.state.shape != null)
{
var bounds = this.state.getCellBounds();
if (this.state.shape.flipH)
{
pt.x = 2 * bounds.x + bounds.width - pt.x;
}
if (this.state.shape.flipV)
{
pt.y = 2 * bounds.y + bounds.height - pt.y;
}
}
return pt;
};
/**
* Function: snapPoint
*
* Snaps the given point to the grid if ignore is false. This modifies
* the given point in-place and also returns it.
*/
mxHandle.prototype.snapPoint = function(pt, ignore)
{
if (!ignore)
{
pt.x = this.graph.snap(pt.x);
pt.y = this.graph.snap(pt.y);
}
return pt;
};
/**
* Function: setVisible
*
* Shows or hides this handle.
*/
mxHandle.prototype.setVisible = function(visible)
{
if (this.shape != null && this.shape.node != null)
{
this.shape.node.style.display = (visible) ? '' : 'none';
}
};
/**
* Function: reset
*
* Resets the state of this handle by setting its visibility to true.
*/
mxHandle.prototype.reset = function()
{
this.setVisible(true);
this.state.style = this.graph.getCellStyle(this.state.cell);
this.positionChanged();
};
/**
* Function: destroy
*
* Destroys this handle.
*/
mxHandle.prototype.destroy = function()
{
if (this.shape != null)
{
this.shape.destroy();
this.shape = null;
}
};

View File

@ -0,0 +1,428 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxKeyHandler
*
* Event handler that listens to keystroke events. This is not a singleton,
* however, it is normally only required once if the target is the document
* element (default).
*
* This handler installs a key event listener in the topmost DOM node and
* processes all events that originate from descandants of <mxGraph.container>
* or from the topmost DOM node. The latter means that all unhandled keystrokes
* are handled by this object regardless of the focused state of the <graph>.
*
* Example:
*
* The following example creates a key handler that listens to the delete key
* (46) and deletes the selection cells if the graph is enabled.
*
* (code)
* var keyHandler = new mxKeyHandler(graph);
* keyHandler.bindKey(46, function(evt)
* {
* if (graph.isEnabled())
* {
* graph.removeCells();
* }
* });
* (end)
*
* Keycodes:
*
* See http://tinyurl.com/yp8jgl or http://tinyurl.com/229yqw for a list of
* keycodes or install a key event listener into the document element and print
* the key codes of the respective events to the console.
*
* To support the Command key and the Control key on the Mac, the following
* code can be used.
*
* (code)
* keyHandler.getFunction = function(evt)
* {
* if (evt != null)
* {
* return (mxEvent.isControlDown(evt) || (mxClient.IS_MAC && evt.metaKey)) ? this.controlKeys[evt.keyCode] : this.normalKeys[evt.keyCode];
* }
*
* return null;
* };
* (end)
*
* Constructor: mxKeyHandler
*
* Constructs an event handler that executes functions bound to specific
* keystrokes.
*
* Parameters:
*
* graph - Reference to the associated <mxGraph>.
* target - Optional reference to the event target. If null, the document
* element is used as the event target, that is, the object where the key
* event listener is installed.
*/
function mxKeyHandler(graph, target)
{
if (graph != null)
{
this.graph = graph;
this.target = target || document.documentElement;
// Creates the arrays to map from keycodes to functions
this.normalKeys = [];
this.shiftKeys = [];
this.controlKeys = [];
this.controlShiftKeys = [];
this.keydownHandler = mxUtils.bind(this, function(evt)
{
this.keyDown(evt);
});
// Installs the keystroke listener in the target
mxEvent.addListener(this.target, 'keydown', this.keydownHandler);
// Automatically deallocates memory in IE
if (mxClient.IS_IE)
{
mxEvent.addListener(window, 'unload',
mxUtils.bind(this, function()
{
this.destroy();
})
);
}
}
};
/**
* Variable: graph
*
* Reference to the <mxGraph> associated with this handler.
*/
mxKeyHandler.prototype.graph = null;
/**
* Variable: target
*
* Reference to the target DOM, that is, the DOM node where the key event
* listeners are installed.
*/
mxKeyHandler.prototype.target = null;
/**
* Variable: normalKeys
*
* Maps from keycodes to functions for non-pressed control keys.
*/
mxKeyHandler.prototype.normalKeys = null;
/**
* Variable: shiftKeys
*
* Maps from keycodes to functions for pressed shift keys.
*/
mxKeyHandler.prototype.shiftKeys = null;
/**
* Variable: controlKeys
*
* Maps from keycodes to functions for pressed control keys.
*/
mxKeyHandler.prototype.controlKeys = null;
/**
* Variable: controlShiftKeys
*
* Maps from keycodes to functions for pressed control and shift keys.
*/
mxKeyHandler.prototype.controlShiftKeys = null;
/**
* Variable: enabled
*
* Specifies if events are handled. Default is true.
*/
mxKeyHandler.prototype.enabled = true;
/**
* Function: isEnabled
*
* Returns true if events are handled. This implementation returns
* <enabled>.
*/
mxKeyHandler.prototype.isEnabled = function()
{
return this.enabled;
};
/**
* Function: setEnabled
*
* Enables or disables event handling by updating <enabled>.
*
* Parameters:
*
* enabled - Boolean that specifies the new enabled state.
*/
mxKeyHandler.prototype.setEnabled = function(enabled)
{
this.enabled = enabled;
};
/**
* Function: bindKey
*
* Binds the specified keycode to the given function. This binding is used
* if the control key is not pressed.
*
* Parameters:
*
* code - Integer that specifies the keycode.
* funct - JavaScript function that takes the key event as an argument.
*/
mxKeyHandler.prototype.bindKey = function(code, funct)
{
this.normalKeys[code] = funct;
};
/**
* Function: bindShiftKey
*
* Binds the specified keycode to the given function. This binding is used
* if the shift key is pressed.
*
* Parameters:
*
* code - Integer that specifies the keycode.
* funct - JavaScript function that takes the key event as an argument.
*/
mxKeyHandler.prototype.bindShiftKey = function(code, funct)
{
this.shiftKeys[code] = funct;
};
/**
* Function: bindControlKey
*
* Binds the specified keycode to the given function. This binding is used
* if the control key is pressed.
*
* Parameters:
*
* code - Integer that specifies the keycode.
* funct - JavaScript function that takes the key event as an argument.
*/
mxKeyHandler.prototype.bindControlKey = function(code, funct)
{
this.controlKeys[code] = funct;
};
/**
* Function: bindControlShiftKey
*
* Binds the specified keycode to the given function. This binding is used
* if the control and shift key are pressed.
*
* Parameters:
*
* code - Integer that specifies the keycode.
* funct - JavaScript function that takes the key event as an argument.
*/
mxKeyHandler.prototype.bindControlShiftKey = function(code, funct)
{
this.controlShiftKeys[code] = funct;
};
/**
* Function: isControlDown
*
* Returns true if the control key is pressed. This uses <mxEvent.isControlDown>.
*
* Parameters:
*
* evt - Key event whose control key pressed state should be returned.
*/
mxKeyHandler.prototype.isControlDown = function(evt)
{
return mxEvent.isControlDown(evt);
};
/**
* Function: getFunction
*
* Returns the function associated with the given key event or null if no
* function is associated with the given event.
*
* Parameters:
*
* evt - Key event whose associated function should be returned.
*/
mxKeyHandler.prototype.getFunction = function(evt)
{
if (evt != null && !mxEvent.isAltDown(evt))
{
if (this.isControlDown(evt))
{
if (mxEvent.isShiftDown(evt))
{
return this.controlShiftKeys[evt.keyCode];
}
else
{
return this.controlKeys[evt.keyCode];
}
}
else
{
if (mxEvent.isShiftDown(evt))
{
return this.shiftKeys[evt.keyCode];
}
else
{
return this.normalKeys[evt.keyCode];
}
}
}
return null;
};
/**
* Function: isGraphEvent
*
* Returns true if the event should be processed by this handler, that is,
* if the event source is either the target, one of its direct children, a
* descendant of the <mxGraph.container>, or the <mxGraph.cellEditor> of the
* <graph>.
*
* Parameters:
*
* evt - Key event that represents the keystroke.
*/
mxKeyHandler.prototype.isGraphEvent = function(evt)
{
var source = mxEvent.getSource(evt);
// Accepts events from the target object or
// in-place editing inside graph
if ((source == this.target || source.parentNode == this.target) ||
(this.graph.cellEditor != null && this.graph.cellEditor.isEventSource(evt)))
{
return true;
}
// Accepts events from inside the container
return mxUtils.isAncestorNode(this.graph.container, source);
};
/**
* Function: keyDown
*
* Handles the event by invoking the function bound to the respective keystroke
* if <isEnabledForEvent> returns true for the given event and if
* <isEventIgnored> returns false, except for escape for which
* <isEventIgnored> is not invoked.
*
* Parameters:
*
* evt - Key event that represents the keystroke.
*/
mxKeyHandler.prototype.keyDown = function(evt)
{
if (this.isEnabledForEvent(evt))
{
// Cancels the editing if escape is pressed
if (evt.keyCode == 27 /* Escape */)
{
this.escape(evt);
}
// Invokes the function for the keystroke
else if (!this.isEventIgnored(evt))
{
var boundFunction = this.getFunction(evt);
if (boundFunction != null)
{
boundFunction(evt);
mxEvent.consume(evt);
}
}
}
};
/**
* Function: isEnabledForEvent
*
* Returns true if the given event should be handled. <isEventIgnored> is
* called later if the event is not an escape key stroke, in which case
* <escape> is called. This implementation returns true if <isEnabled>
* returns true for both, this handler and <graph>, if the event is not
* consumed and if <isGraphEvent> returns true.
*
* Parameters:
*
* evt - Key event that represents the keystroke.
*/
mxKeyHandler.prototype.isEnabledForEvent = function(evt)
{
return (this.graph.isEnabled() && !mxEvent.isConsumed(evt) &&
this.isGraphEvent(evt) && this.isEnabled());
};
/**
* Function: isEventIgnored
*
* Returns true if the given keystroke should be ignored. This returns
* graph.isEditing().
*
* Parameters:
*
* evt - Key event that represents the keystroke.
*/
mxKeyHandler.prototype.isEventIgnored = function(evt)
{
return this.graph.isEditing();
};
/**
* Function: escape
*
* Hook to process ESCAPE keystrokes. This implementation invokes
* <mxGraph.stopEditing> to cancel the current editing, connecting
* and/or other ongoing modifications.
*
* Parameters:
*
* evt - Key event that represents the keystroke. Possible keycode in this
* case is 27 (ESCAPE).
*/
mxKeyHandler.prototype.escape = function(evt)
{
if (this.graph.isEscapeEnabled())
{
this.graph.escape(evt);
}
};
/**
* Function: destroy
*
* Destroys the handler and all its references into the DOM. This does
* normally not need to be called, it is called automatically when the
* window unloads (in IE).
*/
mxKeyHandler.prototype.destroy = function()
{
if (this.target != null && this.keydownHandler != null)
{
mxEvent.removeListener(this.target, 'keydown', this.keydownHandler);
this.keydownHandler = null;
}
this.target = null;
};

View File

@ -0,0 +1,494 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxPanningHandler
*
* Event handler that pans and creates popupmenus. To use the left
* mousebutton for panning without interfering with cell moving and
* resizing, use <isUseLeftButton> and <isIgnoreCell>. For grid size
* steps while panning, use <useGrid>. This handler is built-into
* <mxGraph.panningHandler> and enabled using <mxGraph.setPanning>.
*
* Constructor: mxPanningHandler
*
* Constructs an event handler that creates a <mxPopupMenu>
* and pans the graph.
*
* Event: mxEvent.PAN_START
*
* Fires when the panning handler changes its <active> state to true. The
* <code>event</code> property contains the corresponding <mxMouseEvent>.
*
* Event: mxEvent.PAN
*
* Fires while handle is processing events. The <code>event</code> property contains
* the corresponding <mxMouseEvent>.
*
* Event: mxEvent.PAN_END
*
* Fires when the panning handler changes its <active> state to false. The
* <code>event</code> property contains the corresponding <mxMouseEvent>.
*/
function mxPanningHandler(graph)
{
if (graph != null)
{
this.graph = graph;
this.graph.addMouseListener(this);
// Handles force panning event
this.forcePanningHandler = mxUtils.bind(this, function(sender, evt)
{
var evtName = evt.getProperty('eventName');
var me = evt.getProperty('event');
if (evtName == mxEvent.MOUSE_DOWN && this.isForcePanningEvent(me))
{
this.start(me);
this.active = true;
this.fireEvent(new mxEventObject(mxEvent.PAN_START, 'event', me));
me.consume();
}
});
this.graph.addListener(mxEvent.FIRE_MOUSE_EVENT, this.forcePanningHandler);
// Handles pinch gestures
this.gestureHandler = mxUtils.bind(this, function(sender, eo)
{
if (this.isPinchEnabled())
{
var evt = eo.getProperty('event');
if (!mxEvent.isConsumed(evt) && evt.type == 'gesturestart')
{
this.initialScale = this.graph.view.scale;
// Forces start of panning when pinch gesture starts
if (!this.active && this.mouseDownEvent != null)
{
this.start(this.mouseDownEvent);
this.mouseDownEvent = null;
}
}
else if (evt.type == 'gestureend' && this.initialScale != null)
{
this.initialScale = null;
}
if (this.initialScale != null)
{
this.zoomGraph(evt);
}
}
});
this.graph.addListener(mxEvent.GESTURE, this.gestureHandler);
this.mouseUpListener = mxUtils.bind(this, function()
{
if (this.active)
{
this.reset();
}
});
// Stops scrolling on every mouseup anywhere in the document
mxEvent.addListener(document, 'mouseup', this.mouseUpListener);
}
};
/**
* Extends mxEventSource.
*/
mxPanningHandler.prototype = new mxEventSource();
mxPanningHandler.prototype.constructor = mxPanningHandler;
/**
* Variable: graph
*
* Reference to the enclosing <mxGraph>.
*/
mxPanningHandler.prototype.graph = null;
/**
* Variable: useLeftButtonForPanning
*
* Specifies if panning should be active for the left mouse button.
* Setting this to true may conflict with <mxRubberband>. Default is false.
*/
mxPanningHandler.prototype.useLeftButtonForPanning = false;
/**
* Variable: usePopupTrigger
*
* Specifies if <mxEvent.isPopupTrigger> should also be used for panning.
*/
mxPanningHandler.prototype.usePopupTrigger = true;
/**
* Variable: ignoreCell
*
* Specifies if panning should be active even if there is a cell under the
* mousepointer. Default is false.
*/
mxPanningHandler.prototype.ignoreCell = false;
/**
* Variable: previewEnabled
*
* Specifies if the panning should be previewed. Default is true.
*/
mxPanningHandler.prototype.previewEnabled = true;
/**
* Variable: useGrid
*
* Specifies if the panning steps should be aligned to the grid size.
* Default is false.
*/
mxPanningHandler.prototype.useGrid = false;
/**
* Variable: panningEnabled
*
* Specifies if panning should be enabled. Default is true.
*/
mxPanningHandler.prototype.panningEnabled = true;
/**
* Variable: pinchEnabled
*
* Specifies if pinch gestures should be handled as zoom. Default is true.
*/
mxPanningHandler.prototype.pinchEnabled = true;
/**
* Variable: maxScale
*
* Specifies the maximum scale. Default is 8.
*/
mxPanningHandler.prototype.maxScale = 8;
/**
* Variable: minScale
*
* Specifies the minimum scale. Default is 0.01.
*/
mxPanningHandler.prototype.minScale = 0.01;
/**
* Variable: dx
*
* Holds the current horizontal offset.
*/
mxPanningHandler.prototype.dx = null;
/**
* Variable: dy
*
* Holds the current vertical offset.
*/
mxPanningHandler.prototype.dy = null;
/**
* Variable: startX
*
* Holds the x-coordinate of the start point.
*/
mxPanningHandler.prototype.startX = 0;
/**
* Variable: startY
*
* Holds the y-coordinate of the start point.
*/
mxPanningHandler.prototype.startY = 0;
/**
* Function: isActive
*
* Returns true if the handler is currently active.
*/
mxPanningHandler.prototype.isActive = function()
{
return this.active || this.initialScale != null;
};
/**
* Function: isPanningEnabled
*
* Returns <panningEnabled>.
*/
mxPanningHandler.prototype.isPanningEnabled = function()
{
return this.panningEnabled;
};
/**
* Function: setPanningEnabled
*
* Sets <panningEnabled>.
*/
mxPanningHandler.prototype.setPanningEnabled = function(value)
{
this.panningEnabled = value;
};
/**
* Function: isPinchEnabled
*
* Returns <pinchEnabled>.
*/
mxPanningHandler.prototype.isPinchEnabled = function()
{
return this.pinchEnabled;
};
/**
* Function: setPinchEnabled
*
* Sets <pinchEnabled>.
*/
mxPanningHandler.prototype.setPinchEnabled = function(value)
{
this.pinchEnabled = value;
};
/**
* Function: isPanningTrigger
*
* Returns true if the given event is a panning trigger for the optional
* given cell. This returns true if control-shift is pressed or if
* <usePopupTrigger> is true and the event is a popup trigger.
*/
mxPanningHandler.prototype.isPanningTrigger = function(me)
{
var evt = me.getEvent();
return (this.useLeftButtonForPanning && me.getState() == null &&
mxEvent.isLeftMouseButton(evt)) || (mxEvent.isControlDown(evt) &&
mxEvent.isShiftDown(evt)) || (this.usePopupTrigger && mxEvent.isPopupTrigger(evt));
};
/**
* Function: isForcePanningEvent
*
* Returns true if the given <mxMouseEvent> should start panning. This
* implementation always returns true if <ignoreCell> is true or for
* multi touch events.
*/
mxPanningHandler.prototype.isForcePanningEvent = function(me)
{
return this.ignoreCell || mxEvent.isMultiTouchEvent(me.getEvent());
};
/**
* Function: mouseDown
*
* Handles the event by initiating the panning. By consuming the event all
* subsequent events of the gesture are redirected to this handler.
*/
mxPanningHandler.prototype.mouseDown = function(sender, me)
{
this.mouseDownEvent = me;
if (!me.isConsumed() && this.isPanningEnabled() && !this.active && this.isPanningTrigger(me))
{
this.start(me);
this.consumePanningTrigger(me);
}
};
/**
* Function: start
*
* Starts panning at the given event.
*/
mxPanningHandler.prototype.start = function(me)
{
this.dx0 = -this.graph.container.scrollLeft;
this.dy0 = -this.graph.container.scrollTop;
// Stores the location of the trigger event
this.startX = me.getX();
this.startY = me.getY();
this.dx = null;
this.dy = null;
this.panningTrigger = true;
};
/**
* Function: consumePanningTrigger
*
* Consumes the given <mxMouseEvent> if it was a panning trigger in
* <mouseDown>. The default is to invoke <mxMouseEvent.consume>. Note that this
* will block any further event processing. If you haven't disabled built-in
* context menus and require immediate selection of the cell on mouseDown in
* Safari and/or on the Mac, then use the following code:
*
* (code)
* mxPanningHandler.prototype.consumePanningTrigger = function(me)
* {
* if (me.evt.preventDefault)
* {
* me.evt.preventDefault();
* }
*
* // Stops event processing in IE
* me.evt.returnValue = false;
*
* // Sets local consumed state
* if (!mxClient.IS_SF && !mxClient.IS_MAC)
* {
* me.consumed = true;
* }
* };
* (end)
*/
mxPanningHandler.prototype.consumePanningTrigger = function(me)
{
me.consume();
};
/**
* Function: mouseMove
*
* Handles the event by updating the panning on the graph.
*/
mxPanningHandler.prototype.mouseMove = function(sender, me)
{
this.dx = me.getX() - this.startX;
this.dy = me.getY() - this.startY;
if (this.active)
{
if (this.previewEnabled)
{
// Applies the grid to the panning steps
if (this.useGrid)
{
this.dx = this.graph.snap(this.dx);
this.dy = this.graph.snap(this.dy);
}
this.graph.panGraph(this.dx + this.dx0, this.dy + this.dy0);
}
this.fireEvent(new mxEventObject(mxEvent.PAN, 'event', me));
}
else if (this.panningTrigger)
{
var tmp = this.active;
// Panning is activated only if the mouse is moved
// beyond the graph tolerance
this.active = Math.abs(this.dx) > this.graph.tolerance || Math.abs(this.dy) > this.graph.tolerance;
if (!tmp && this.active)
{
this.fireEvent(new mxEventObject(mxEvent.PAN_START, 'event', me));
}
}
if (this.active || this.panningTrigger)
{
me.consume();
}
};
/**
* Function: mouseUp
*
* Handles the event by setting the translation on the view or showing the
* popupmenu.
*/
mxPanningHandler.prototype.mouseUp = function(sender, me)
{
if (this.active)
{
if (this.dx != null && this.dy != null)
{
// Ignores if scrollbars have been used for panning
if (!this.graph.useScrollbarsForPanning || !mxUtils.hasScrollbars(this.graph.container))
{
var scale = this.graph.getView().scale;
var t = this.graph.getView().translate;
this.graph.panGraph(0, 0);
this.panGraph(t.x + this.dx / scale, t.y + this.dy / scale);
}
me.consume();
}
this.fireEvent(new mxEventObject(mxEvent.PAN_END, 'event', me));
}
this.reset();
};
/**
* Function: zoomGraph
*
* Zooms the graph to the given value and consumed the event if needed.
*/
mxPanningHandler.prototype.zoomGraph = function(evt)
{
var value = Math.round(this.initialScale * evt.scale * 100) / 100;
if (this.minScale != null)
{
value = Math.max(this.minScale, value);
}
if (this.maxScale != null)
{
value = Math.min(this.maxScale, value);
}
if (this.graph.view.scale != value)
{
this.graph.zoomTo(value);
mxEvent.consume(evt);
}
};
/**
* Function: reset
*
* Resets the state of this handler.
*/
mxPanningHandler.prototype.reset = function()
{
this.panningTrigger = false;
this.mouseDownEvent = null;
this.active = false;
this.dx = null;
this.dy = null;
};
/**
* Function: panGraph
*
* Pans <graph> by the given amount.
*/
mxPanningHandler.prototype.panGraph = function(dx, dy)
{
this.graph.getView().setTranslate(dx, dy);
};
/**
* Function: destroy
*
* Destroys the handler and all its resources and DOM nodes.
*/
mxPanningHandler.prototype.destroy = function()
{
this.graph.removeMouseListener(this);
this.graph.removeListener(this.forcePanningHandler);
this.graph.removeListener(this.gestureHandler);
mxEvent.removeListener(document, 'mouseup', this.mouseUpListener);
};

View File

@ -0,0 +1,218 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxPopupMenuHandler
*
* Event handler that creates popupmenus.
*
* Constructor: mxPopupMenuHandler
*
* Constructs an event handler that creates a <mxPopupMenu>.
*/
function mxPopupMenuHandler(graph, factoryMethod)
{
if (graph != null)
{
this.graph = graph;
this.factoryMethod = factoryMethod;
this.graph.addMouseListener(this);
// Does not show menu if any touch gestures take place after the trigger
this.gestureHandler = mxUtils.bind(this, function(sender, eo)
{
this.inTolerance = false;
});
this.graph.addListener(mxEvent.GESTURE, this.gestureHandler);
this.init();
}
};
/**
* Extends mxPopupMenu.
*/
mxPopupMenuHandler.prototype = new mxPopupMenu();
mxPopupMenuHandler.prototype.constructor = mxPopupMenuHandler;
/**
* Variable: graph
*
* Reference to the enclosing <mxGraph>.
*/
mxPopupMenuHandler.prototype.graph = null;
/**
* Variable: selectOnPopup
*
* Specifies if cells should be selected if a popupmenu is displayed for
* them. Default is true.
*/
mxPopupMenuHandler.prototype.selectOnPopup = true;
/**
* Variable: clearSelectionOnBackground
*
* Specifies if cells should be deselected if a popupmenu is displayed for
* the diagram background. Default is true.
*/
mxPopupMenuHandler.prototype.clearSelectionOnBackground = true;
/**
* Variable: triggerX
*
* X-coordinate of the mouse down event.
*/
mxPopupMenuHandler.prototype.triggerX = null;
/**
* Variable: triggerY
*
* Y-coordinate of the mouse down event.
*/
mxPopupMenuHandler.prototype.triggerY = null;
/**
* Variable: screenX
*
* Screen X-coordinate of the mouse down event.
*/
mxPopupMenuHandler.prototype.screenX = null;
/**
* Variable: screenY
*
* Screen Y-coordinate of the mouse down event.
*/
mxPopupMenuHandler.prototype.screenY = null;
/**
* Function: init
*
* Initializes the shapes required for this vertex handler.
*/
mxPopupMenuHandler.prototype.init = function()
{
// Supercall
mxPopupMenu.prototype.init.apply(this);
// Hides the tooltip if the mouse is over
// the context menu
mxEvent.addGestureListeners(this.div, mxUtils.bind(this, function(evt)
{
this.graph.tooltipHandler.hide();
}));
};
/**
* Function: isSelectOnPopup
*
* Hook for returning if a cell should be selected for a given <mxMouseEvent>.
* This implementation returns <selectOnPopup>.
*/
mxPopupMenuHandler.prototype.isSelectOnPopup = function(me)
{
return this.selectOnPopup;
};
/**
* Function: mouseDown
*
* Handles the event by initiating the panning. By consuming the event all
* subsequent events of the gesture are redirected to this handler.
*/
mxPopupMenuHandler.prototype.mouseDown = function(sender, me)
{
if (this.isEnabled() && !mxEvent.isMultiTouchEvent(me.getEvent()))
{
// Hides the popupmenu if is is being displayed
this.hideMenu();
this.triggerX = me.getGraphX();
this.triggerY = me.getGraphY();
this.screenX = mxEvent.getMainEvent(me.getEvent()).screenX;
this.screenY = mxEvent.getMainEvent(me.getEvent()).screenY;
this.popupTrigger = this.isPopupTrigger(me);
this.inTolerance = true;
}
};
/**
* Function: mouseMove
*
* Handles the event by updating the panning on the graph.
*/
mxPopupMenuHandler.prototype.mouseMove = function(sender, me)
{
// Popup trigger may change on mouseUp so ignore it
if (this.inTolerance && this.screenX != null && this.screenY != null)
{
if (Math.abs(mxEvent.getMainEvent(me.getEvent()).screenX - this.screenX) > this.graph.tolerance ||
Math.abs(mxEvent.getMainEvent(me.getEvent()).screenY - this.screenY) > this.graph.tolerance)
{
this.inTolerance = false;
}
}
};
/**
* Function: mouseUp
*
* Handles the event by setting the translation on the view or showing the
* popupmenu.
*/
mxPopupMenuHandler.prototype.mouseUp = function(sender, me)
{
if (this.popupTrigger && this.inTolerance && this.triggerX != null && this.triggerY != null)
{
var cell = this.getCellForPopupEvent(me);
// Selects the cell for which the context menu is being displayed
if (this.graph.isEnabled() && this.isSelectOnPopup(me) &&
cell != null && !this.graph.isCellSelected(cell))
{
this.graph.setSelectionCell(cell);
}
else if (this.clearSelectionOnBackground && cell == null)
{
this.graph.clearSelection();
}
// Hides the tooltip if there is one
this.graph.tooltipHandler.hide();
// Menu is shifted by 1 pixel so that the mouse up event
// is routed via the underlying shape instead of the DIV
var origin = mxUtils.getScrollOrigin();
this.popup(me.getX() + origin.x + 1, me.getY() + origin.y + 1, cell, me.getEvent());
me.consume();
}
this.popupTrigger = false;
this.inTolerance = false;
};
/**
* Function: getCellForPopupEvent
*
* Hook to return the cell for the mouse up popup trigger handling.
*/
mxPopupMenuHandler.prototype.getCellForPopupEvent = function(me)
{
return me.getCell();
};
/**
* Function: destroy
*
* Destroys the handler and all its resources and DOM nodes.
*/
mxPopupMenuHandler.prototype.destroy = function()
{
this.graph.removeMouseListener(this);
this.graph.removeListener(this.gestureHandler);
// Supercall
mxPopupMenu.prototype.destroy.apply(this);
};

View File

@ -0,0 +1,429 @@
/**
* Copyright (c) 2006-2016, JGraph Ltd
* Copyright (c) 2006-2016, Gaudenz Alder
*/
/**
* Class: mxRubberband
*
* Event handler that selects rectangular regions. This is not built-into
* <mxGraph>. To enable rubberband selection in a graph, use the following code.
*
* Example:
*
* (code)
* var rubberband = new mxRubberband(graph);
* (end)
*
* Constructor: mxRubberband
*
* Constructs an event handler that selects rectangular regions in the graph
* using rubberband selection.
*/
function mxRubberband(graph)
{
if (graph != null)
{
this.graph = graph;
this.graph.addMouseListener(this);
// Handles force rubberband event
this.forceRubberbandHandler = mxUtils.bind(this, function(sender, evt)
{
var evtName = evt.getProperty('eventName');
var me = evt.getProperty('event');
if (evtName == mxEvent.MOUSE_DOWN && this.isForceRubberbandEvent(me))
{
var offset = mxUtils.getOffset(this.graph.container);
var origin = mxUtils.getScrollOrigin(this.graph.container);
origin.x -= offset.x;
origin.y -= offset.y;
this.start(me.getX() + origin.x, me.getY() + origin.y);
me.consume(false);
}
});
this.graph.addListener(mxEvent.FIRE_MOUSE_EVENT, this.forceRubberbandHandler);
// Repaints the marquee after autoscroll
this.panHandler = mxUtils.bind(this, function()
{
this.repaint();
});
this.graph.addListener(mxEvent.PAN, this.panHandler);
// Does not show menu if any touch gestures take place after the trigger
this.gestureHandler = mxUtils.bind(this, function(sender, eo)
{
if (this.first != null)
{
this.reset();
}
});
this.graph.addListener(mxEvent.GESTURE, this.gestureHandler);
// Automatic deallocation of memory
if (mxClient.IS_IE)
{
mxEvent.addListener(window, 'unload',
mxUtils.bind(this, function()
{
this.destroy();
})
);
}
}
};
/**
* Variable: defaultOpacity
*
* Specifies the default opacity to be used for the rubberband div. Default
* is 20.
*/
mxRubberband.prototype.defaultOpacity = 20;
/**
* Variable: enabled
*
* Specifies if events are handled. Default is true.
*/
mxRubberband.prototype.enabled = true;
/**
* Variable: div
*
* Holds the DIV element which is currently visible.
*/
mxRubberband.prototype.div = null;
/**
* Variable: sharedDiv
*
* Holds the DIV element which is used to display the rubberband.
*/
mxRubberband.prototype.sharedDiv = null;
/**
* Variable: currentX
*
* Holds the value of the x argument in the last call to <update>.
*/
mxRubberband.prototype.currentX = 0;
/**
* Variable: currentY
*
* Holds the value of the y argument in the last call to <update>.
*/
mxRubberband.prototype.currentY = 0;
/**
* Variable: fadeOut
*
* Optional fade out effect. Default is false.
*/
mxRubberband.prototype.fadeOut = false;
/**
* Function: isEnabled
*
* Returns true if events are handled. This implementation returns
* <enabled>.
*/
mxRubberband.prototype.isEnabled = function()
{
return this.enabled;
};
/**
* Function: setEnabled
*
* Enables or disables event handling. This implementation updates
* <enabled>.
*/
mxRubberband.prototype.setEnabled = function(enabled)
{
this.enabled = enabled;
};
/**
* Function: isForceRubberbandEvent
*
* Returns true if the given <mxMouseEvent> should start rubberband selection.
* This implementation returns true if the alt key is pressed.
*/
mxRubberband.prototype.isForceRubberbandEvent = function(me)
{
return mxEvent.isAltDown(me.getEvent());
};
/**
* Function: mouseDown
*
* Handles the event by initiating a rubberband selection. By consuming the
* event all subsequent events of the gesture are redirected to this
* handler.
*/
mxRubberband.prototype.mouseDown = function(sender, me)
{
if (!me.isConsumed() && this.isEnabled() && this.graph.isEnabled() &&
me.getState() == null && !mxEvent.isMultiTouchEvent(me.getEvent()))
{
var offset = mxUtils.getOffset(this.graph.container);
var origin = mxUtils.getScrollOrigin(this.graph.container);
origin.x -= offset.x;
origin.y -= offset.y;
this.start(me.getX() + origin.x, me.getY() + origin.y);
// Does not prevent the default for this event so that the
// event processing chain is still executed even if we start
// rubberbanding. This is required eg. in ExtJs to hide the
// current context menu. In mouseMove we'll make sure we're
// not selecting anything while we're rubberbanding.
me.consume(false);
}
};
/**
* Function: start
*
* Sets the start point for the rubberband selection.
*/
mxRubberband.prototype.start = function(x, y)
{
this.first = new mxPoint(x, y);
var container = this.graph.container;
function createMouseEvent(evt)
{
var me = new mxMouseEvent(evt);
var pt = mxUtils.convertPoint(container, me.getX(), me.getY());
me.graphX = pt.x;
me.graphY = pt.y;
return me;
};
this.dragHandler = mxUtils.bind(this, function(evt)
{
this.mouseMove(this.graph, createMouseEvent(evt));
});
this.dropHandler = mxUtils.bind(this, function(evt)
{
this.mouseUp(this.graph, createMouseEvent(evt));
});
// Workaround for rubberband stopping if the mouse leaves the container in Firefox
if (mxClient.IS_FF)
{
mxEvent.addGestureListeners(document, null, this.dragHandler, this.dropHandler);
}
};
/**
* Function: mouseMove
*
* Handles the event by updating therubberband selection.
*/
mxRubberband.prototype.mouseMove = function(sender, me)
{
if (!me.isConsumed() && this.first != null)
{
var origin = mxUtils.getScrollOrigin(this.graph.container);
var offset = mxUtils.getOffset(this.graph.container);
origin.x -= offset.x;
origin.y -= offset.y;
var x = me.getX() + origin.x;
var y = me.getY() + origin.y;
var dx = this.first.x - x;
var dy = this.first.y - y;
var tol = this.graph.tolerance;
if (this.div != null || Math.abs(dx) > tol || Math.abs(dy) > tol)
{
if (this.div == null)
{
this.div = this.createShape();
}
// Clears selection while rubberbanding. This is required because
// the event is not consumed in mouseDown.
mxUtils.clearSelection();
this.update(x, y);
me.consume();
}
}
};
/**
* Function: createShape
*
* Creates the rubberband selection shape.
*/
mxRubberband.prototype.createShape = function()
{
if (this.sharedDiv == null)
{
this.sharedDiv = document.createElement('div');
this.sharedDiv.className = 'mxRubberband';
mxUtils.setOpacity(this.sharedDiv, this.defaultOpacity);
}
this.graph.container.appendChild(this.sharedDiv);
var result = this.sharedDiv;
if (mxClient.IS_SVG && (!mxClient.IS_IE || document.documentMode >= 10) && this.fadeOut)
{
this.sharedDiv = null;
}
return result;
};
/**
* Function: isActive
*
* Returns true if this handler is active.
*/
mxRubberband.prototype.isActive = function(sender, me)
{
return this.div != null && this.div.style.display != 'none';
};
/**
* Function: mouseUp
*
* Handles the event by selecting the region of the rubberband using
* <mxGraph.selectRegion>.
*/
mxRubberband.prototype.mouseUp = function(sender, me)
{
var active = this.isActive();
this.reset();
if (active)
{
this.execute(me.getEvent());
me.consume();
}
};
/**
* Function: execute
*
* Resets the state of this handler and selects the current region
* for the given event.
*/
mxRubberband.prototype.execute = function(evt)
{
var rect = new mxRectangle(this.x, this.y, this.width, this.height);
this.graph.selectRegion(rect, evt);
};
/**
* Function: reset
*
* Resets the state of the rubberband selection.
*/
mxRubberband.prototype.reset = function()
{
if (this.div != null)
{
if (mxClient.IS_SVG && (!mxClient.IS_IE || document.documentMode >= 10) && this.fadeOut)
{
var temp = this.div;
mxUtils.setPrefixedStyle(temp.style, 'transition', 'all 0.2s linear');
temp.style.pointerEvents = 'none';
temp.style.opacity = 0;
window.setTimeout(function()
{
temp.parentNode.removeChild(temp);
}, 200);
}
else
{
this.div.parentNode.removeChild(this.div);
}
}
mxEvent.removeGestureListeners(document, null, this.dragHandler, this.dropHandler);
this.dragHandler = null;
this.dropHandler = null;
this.currentX = 0;
this.currentY = 0;
this.first = null;
this.div = null;
};
/**
* Function: update
*
* Sets <currentX> and <currentY> and calls <repaint>.
*/
mxRubberband.prototype.update = function(x, y)
{
this.currentX = x;
this.currentY = y;
this.repaint();
};
/**
* Function: repaint
*
* Computes the bounding box and updates the style of the <div>.
*/
mxRubberband.prototype.repaint = function()
{
if (this.div != null)
{
var x = this.currentX - this.graph.panDx;
var y = this.currentY - this.graph.panDy;
this.x = Math.min(this.first.x, x);
this.y = Math.min(this.first.y, y);
this.width = Math.max(this.first.x, x) - this.x;
this.height = Math.max(this.first.y, y) - this.y;
var dx = (mxClient.IS_VML) ? this.graph.panDx : 0;
var dy = (mxClient.IS_VML) ? this.graph.panDy : 0;
this.div.style.left = (this.x + dx) + 'px';
this.div.style.top = (this.y + dy) + 'px';
this.div.style.width = Math.max(1, this.width) + 'px';
this.div.style.height = Math.max(1, this.height) + 'px';
}
};
/**
* Function: destroy
*
* Destroys the handler and all its resources and DOM nodes. This does
* normally not need to be called, it is called automatically when the
* window unloads.
*/
mxRubberband.prototype.destroy = function()
{
if (!this.destroyed)
{
this.destroyed = true;
this.graph.removeMouseListener(this);
this.graph.removeListener(this.forceRubberbandHandler);
this.graph.removeListener(this.panHandler);
this.reset();
if (this.sharedDiv != null)
{
this.sharedDiv = null;
}
}
};

View File

@ -0,0 +1,344 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxSelectionCellsHandler
*
* An event handler that manages cell handlers and invokes their mouse event
* processing functions.
*
* Group: Events
*
* Event: mxEvent.ADD
*
* Fires if a cell has been added to the selection. The <code>state</code>
* property contains the <mxCellState> that has been added.
*
* Event: mxEvent.REMOVE
*
* Fires if a cell has been remove from the selection. The <code>state</code>
* property contains the <mxCellState> that has been removed.
*
* Parameters:
*
* graph - Reference to the enclosing <mxGraph>.
*/
function mxSelectionCellsHandler(graph)
{
mxEventSource.call(this);
this.graph = graph;
this.handlers = new mxDictionary();
this.graph.addMouseListener(this);
this.refreshHandler = mxUtils.bind(this, function(sender, evt)
{
if (this.isEnabled())
{
this.refresh();
}
});
this.graph.getSelectionModel().addListener(mxEvent.CHANGE, this.refreshHandler);
this.graph.getModel().addListener(mxEvent.CHANGE, this.refreshHandler);
this.graph.getView().addListener(mxEvent.SCALE, this.refreshHandler);
this.graph.getView().addListener(mxEvent.TRANSLATE, this.refreshHandler);
this.graph.getView().addListener(mxEvent.SCALE_AND_TRANSLATE, this.refreshHandler);
this.graph.getView().addListener(mxEvent.DOWN, this.refreshHandler);
this.graph.getView().addListener(mxEvent.UP, this.refreshHandler);
};
/**
* Extends mxEventSource.
*/
mxUtils.extend(mxSelectionCellsHandler, mxEventSource);
/**
* Variable: graph
*
* Reference to the enclosing <mxGraph>.
*/
mxSelectionCellsHandler.prototype.graph = null;
/**
* Variable: enabled
*
* Specifies if events are handled. Default is true.
*/
mxSelectionCellsHandler.prototype.enabled = true;
/**
* Variable: refreshHandler
*
* Keeps a reference to an event listener for later removal.
*/
mxSelectionCellsHandler.prototype.refreshHandler = null;
/**
* Variable: maxHandlers
*
* Defines the maximum number of handlers to paint individually. Default is 100.
*/
mxSelectionCellsHandler.prototype.maxHandlers = 100;
/**
* Variable: handlers
*
* <mxDictionary> that maps from cells to handlers.
*/
mxSelectionCellsHandler.prototype.handlers = null;
/**
* Function: isEnabled
*
* Returns <enabled>.
*/
mxSelectionCellsHandler.prototype.isEnabled = function()
{
return this.enabled;
};
/**
* Function: setEnabled
*
* Sets <enabled>.
*/
mxSelectionCellsHandler.prototype.setEnabled = function(value)
{
this.enabled = value;
};
/**
* Function: getHandler
*
* Returns the handler for the given cell.
*/
mxSelectionCellsHandler.prototype.getHandler = function(cell)
{
return this.handlers.get(cell);
};
/**
* Function: isHandled
*
* Returns true if the given cell has a handler.
*/
mxSelectionCellsHandler.prototype.isHandled = function(cell)
{
return this.getHandler(cell) != null;
};
/**
* Function: reset
*
* Resets all handlers.
*/
mxSelectionCellsHandler.prototype.reset = function()
{
this.handlers.visit(function(key, handler)
{
handler.reset.apply(handler);
});
};
/**
* Function: getHandledSelectionCells
*
* Reloads or updates all handlers.
*/
mxSelectionCellsHandler.prototype.getHandledSelectionCells = function()
{
return this.graph.getSelectionCells();
};
/**
* Function: refresh
*
* Reloads or updates all handlers.
*/
mxSelectionCellsHandler.prototype.refresh = function()
{
// Removes all existing handlers
var oldHandlers = this.handlers;
this.handlers = new mxDictionary();
// Creates handles for all selection cells
var tmp = mxUtils.sortCells(this.getHandledSelectionCells(), false);
// Destroys or updates old handlers
for (var i = 0; i < tmp.length; i++)
{
var state = this.graph.view.getState(tmp[i]);
if (state != null)
{
var handler = oldHandlers.remove(tmp[i]);
if (handler != null)
{
if (handler.state != state)
{
handler.destroy();
handler = null;
}
else if (!this.isHandlerActive(handler))
{
if (handler.refresh != null)
{
handler.refresh();
}
handler.redraw();
}
}
if (handler != null)
{
this.handlers.put(tmp[i], handler);
}
}
}
// Destroys unused handlers
oldHandlers.visit(mxUtils.bind(this, function(key, handler)
{
this.fireEvent(new mxEventObject(mxEvent.REMOVE, 'state', handler.state));
handler.destroy();
}));
// Creates new handlers and updates parent highlight on existing handlers
for (var i = 0; i < tmp.length; i++)
{
var state = this.graph.view.getState(tmp[i]);
if (state != null)
{
var handler = this.handlers.get(tmp[i]);
if (handler == null)
{
handler = this.graph.createHandler(state);
this.fireEvent(new mxEventObject(mxEvent.ADD, 'state', state));
this.handlers.put(tmp[i], handler);
}
else
{
handler.updateParentHighlight();
}
}
}
};
/**
* Function: isHandlerActive
*
* Returns true if the given handler is active and should not be redrawn.
*/
mxSelectionCellsHandler.prototype.isHandlerActive = function(handler)
{
return handler.index != null;
};
/**
* Function: updateHandler
*
* Updates the handler for the given shape if one exists.
*/
mxSelectionCellsHandler.prototype.updateHandler = function(state)
{
var handler = this.handlers.remove(state.cell);
if (handler != null)
{
// Transfers the current state to the new handler
var index = handler.index;
var x = handler.startX;
var y = handler.startY;
handler.destroy();
handler = this.graph.createHandler(state);
if (handler != null)
{
this.handlers.put(state.cell, handler);
if (index != null && x != null && y != null)
{
handler.start(x, y, index);
}
}
}
};
/**
* Function: mouseDown
*
* Redirects the given event to the handlers.
*/
mxSelectionCellsHandler.prototype.mouseDown = function(sender, me)
{
if (this.graph.isEnabled() && this.isEnabled())
{
var args = [sender, me];
this.handlers.visit(function(key, handler)
{
handler.mouseDown.apply(handler, args);
});
}
};
/**
* Function: mouseMove
*
* Redirects the given event to the handlers.
*/
mxSelectionCellsHandler.prototype.mouseMove = function(sender, me)
{
if (this.graph.isEnabled() && this.isEnabled())
{
var args = [sender, me];
this.handlers.visit(function(key, handler)
{
handler.mouseMove.apply(handler, args);
});
}
};
/**
* Function: mouseUp
*
* Redirects the given event to the handlers.
*/
mxSelectionCellsHandler.prototype.mouseUp = function(sender, me)
{
if (this.graph.isEnabled() && this.isEnabled())
{
var args = [sender, me];
this.handlers.visit(function(key, handler)
{
handler.mouseUp.apply(handler, args);
});
}
};
/**
* Function: destroy
*
* Destroys the handler and all its resources and DOM nodes.
*/
mxSelectionCellsHandler.prototype.destroy = function()
{
this.graph.removeMouseListener(this);
if (this.refreshHandler != null)
{
this.graph.getSelectionModel().removeListener(this.refreshHandler);
this.graph.getModel().removeListener(this.refreshHandler);
this.graph.getView().removeListener(this.refreshHandler);
this.refreshHandler = null;
}
};

View File

@ -0,0 +1,353 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxTooltipHandler
*
* Graph event handler that displays tooltips. <mxGraph.getTooltip> is used to
* get the tooltip for a cell or handle. This handler is built-into
* <mxGraph.tooltipHandler> and enabled using <mxGraph.setTooltips>.
*
* Example:
*
* (code>
* new mxTooltipHandler(graph);
* (end)
*
* Constructor: mxTooltipHandler
*
* Constructs an event handler that displays tooltips with the specified
* delay (in milliseconds). If no delay is specified then a default delay
* of 500 ms (0.5 sec) is used.
*
* Parameters:
*
* graph - Reference to the enclosing <mxGraph>.
* delay - Optional delay in milliseconds.
*/
function mxTooltipHandler(graph, delay)
{
if (graph != null)
{
this.graph = graph;
this.delay = delay || 500;
this.graph.addMouseListener(this);
}
};
/**
* Variable: zIndex
*
* Specifies the zIndex for the tooltip and its shadow. Default is 10005.
*/
mxTooltipHandler.prototype.zIndex = 10005;
/**
* Variable: graph
*
* Reference to the enclosing <mxGraph>.
*/
mxTooltipHandler.prototype.graph = null;
/**
* Variable: delay
*
* Delay to show the tooltip in milliseconds. Default is 500.
*/
mxTooltipHandler.prototype.delay = null;
/**
* Variable: ignoreTouchEvents
*
* Specifies if touch and pen events should be ignored. Default is true.
*/
mxTooltipHandler.prototype.ignoreTouchEvents = true;
/**
* Variable: hideOnHover
*
* Specifies if the tooltip should be hidden if the mouse is moved over the
* current cell. Default is false.
*/
mxTooltipHandler.prototype.hideOnHover = false;
/**
* Variable: destroyed
*
* True if this handler was destroyed using <destroy>.
*/
mxTooltipHandler.prototype.destroyed = false;
/**
* Variable: enabled
*
* Specifies if events are handled. Default is true.
*/
mxTooltipHandler.prototype.enabled = true;
/**
* Function: isEnabled
*
* Returns true if events are handled. This implementation
* returns <enabled>.
*/
mxTooltipHandler.prototype.isEnabled = function()
{
return this.enabled;
};
/**
* Function: setEnabled
*
* Enables or disables event handling. This implementation
* updates <enabled>.
*/
mxTooltipHandler.prototype.setEnabled = function(enabled)
{
this.enabled = enabled;
};
/**
* Function: isHideOnHover
*
* Returns <hideOnHover>.
*/
mxTooltipHandler.prototype.isHideOnHover = function()
{
return this.hideOnHover;
};
/**
* Function: setHideOnHover
*
* Sets <hideOnHover>.
*/
mxTooltipHandler.prototype.setHideOnHover = function(value)
{
this.hideOnHover = value;
};
/**
* Function: init
*
* Initializes the DOM nodes required for this tooltip handler.
*/
mxTooltipHandler.prototype.init = function()
{
if (document.body != null)
{
this.div = document.createElement('div');
this.div.className = 'mxTooltip';
this.div.style.visibility = 'hidden';
document.body.appendChild(this.div);
mxEvent.addGestureListeners(this.div, mxUtils.bind(this, function(evt)
{
var source = mxEvent.getSource(evt);
if (source.nodeName != 'A')
{
this.hideTooltip();
}
}));
}
};
/**
* Function: getStateForEvent
*
* Returns the <mxCellState> to be used for showing a tooltip for this event.
*/
mxTooltipHandler.prototype.getStateForEvent = function(me)
{
return me.getState();
};
/**
* Function: mouseDown
*
* Handles the event by initiating a rubberband selection. By consuming the
* event all subsequent events of the gesture are redirected to this
* handler.
*/
mxTooltipHandler.prototype.mouseDown = function(sender, me)
{
this.reset(me, false);
this.hideTooltip();
};
/**
* Function: mouseMove
*
* Handles the event by updating the rubberband selection.
*/
mxTooltipHandler.prototype.mouseMove = function(sender, me)
{
if (me.getX() != this.lastX || me.getY() != this.lastY)
{
this.reset(me, true);
var state = this.getStateForEvent(me);
if (this.isHideOnHover() || state != this.state || (me.getSource() != this.node &&
(!this.stateSource || (state != null && this.stateSource ==
(me.isSource(state.shape) || !me.isSource(state.text))))))
{
this.hideTooltip();
}
}
this.lastX = me.getX();
this.lastY = me.getY();
};
/**
* Function: mouseUp
*
* Handles the event by resetting the tooltip timer or hiding the existing
* tooltip.
*/
mxTooltipHandler.prototype.mouseUp = function(sender, me)
{
this.reset(me, true);
this.hideTooltip();
};
/**
* Function: resetTimer
*
* Resets the timer.
*/
mxTooltipHandler.prototype.resetTimer = function()
{
if (this.thread != null)
{
window.clearTimeout(this.thread);
this.thread = null;
}
};
/**
* Function: reset
*
* Resets and/or restarts the timer to trigger the display of the tooltip.
*/
mxTooltipHandler.prototype.reset = function(me, restart, state)
{
if (!this.ignoreTouchEvents || mxEvent.isMouseEvent(me.getEvent()))
{
this.resetTimer();
state = (state != null) ? state : this.getStateForEvent(me);
if (restart && this.isEnabled() && state != null && (this.div == null ||
this.div.style.visibility == 'hidden'))
{
var node = me.getSource();
var x = me.getX();
var y = me.getY();
var stateSource = me.isSource(state.shape) || me.isSource(state.text);
this.thread = window.setTimeout(mxUtils.bind(this, function()
{
if (!this.graph.isEditing() && !this.graph.popupMenuHandler.isMenuShowing() && !this.graph.isMouseDown)
{
// Uses information from inside event cause using the event at
// this (delayed) point in time is not possible in IE as it no
// longer contains the required information (member not found)
var tip = this.graph.getTooltip(state, node, x, y);
this.show(tip, x, y);
this.state = state;
this.node = node;
this.stateSource = stateSource;
}
}), this.delay);
}
}
};
/**
* Function: hide
*
* Hides the tooltip and resets the timer.
*/
mxTooltipHandler.prototype.hide = function()
{
this.resetTimer();
this.hideTooltip();
};
/**
* Function: hideTooltip
*
* Hides the tooltip.
*/
mxTooltipHandler.prototype.hideTooltip = function()
{
if (this.div != null)
{
this.div.style.visibility = 'hidden';
this.div.innerHTML = '';
}
};
/**
* Function: show
*
* Shows the tooltip for the specified cell and optional index at the
* specified location (with a vertical offset of 10 pixels).
*/
mxTooltipHandler.prototype.show = function(tip, x, y)
{
if (!this.destroyed && tip != null && tip.length > 0)
{
// Initializes the DOM nodes if required
if (this.div == null)
{
this.init();
}
var origin = mxUtils.getScrollOrigin();
this.div.style.zIndex = this.zIndex;
this.div.style.left = (x + origin.x) + 'px';
this.div.style.top = (y + mxConstants.TOOLTIP_VERTICAL_OFFSET +
origin.y) + 'px';
if (!mxUtils.isNode(tip))
{
this.div.innerHTML = tip.replace(/\n/g, '<br>');
}
else
{
this.div.innerHTML = '';
this.div.appendChild(tip);
}
this.div.style.visibility = '';
mxUtils.fit(this.div);
}
};
/**
* Function: destroy
*
* Destroys the handler and all its resources and DOM nodes.
*/
mxTooltipHandler.prototype.destroy = function()
{
if (!this.destroyed)
{
this.graph.removeMouseListener(this);
mxEvent.release(this.div);
if (this.div != null && this.div.parentNode != null)
{
this.div.parentNode.removeChild(this.div);
}
this.destroyed = true;
this.div = null;
}
};

File diff suppressed because it is too large Load Diff

316
Schematic/src/js/index.txt Normal file
View File

@ -0,0 +1,316 @@
Document: API Specification
Overview:
This JavaScript library is divided into 8 packages. The top-level <mxClient>
class includes (or dynamically imports) everything else. The current version
is stored in <mxClient.VERSION>.
The *editor* package provides the classes required to implement a diagram
editor. The main class in this package is <mxEditor>.
The *view* and *model* packages implement the graph component, represented
by <mxGraph>. It refers to a <mxGraphModel> which contains <mxCell>s and
caches the state of the cells in a <mxGraphView>. The cells are painted
using a <mxCellRenderer> based on the appearance defined in <mxStylesheet>.
Undo history is implemented in <mxUndoManager>. To display an icon on the
graph, <mxCellOverlay> may be used. Validation rules are defined with
<mxMultiplicity>.
The *handler*, *layout* and *shape* packages contain event listeners,
layout algorithms and shapes, respectively. The graph event listeners
include <mxRubberband> for rubberband selection, <mxTooltipHandler>
for tooltips and <mxGraphHandler> for basic cell modifications.
<mxCompactTreeLayout> implements a tree layout algorithm, and the
shape package provides various shapes, which are subclasses of
<mxShape>.
The *util* package provides utility classes including <mxClipboard> for
copy-paste, <mxDatatransfer> for drag-and-drop, <mxConstants> for keys and
values of stylesheets, <mxEvent> and <mxUtils> for cross-browser
event-handling and general purpose functions, <mxResources> for
internationalization and <mxLog> for console output.
The *io* package implements a generic <mxObjectCodec> for turning
JavaScript objects into XML. The main class is <mxCodec>.
<mxCodecRegistry> is the global registry for custom codecs.
Events:
There are three different types of events, namely native DOM events,
<mxEventObjects> which are fired in an <mxEventSource>, and <mxMouseEvents>
which are fired in <mxGraph>.
Some helper methods for handling native events are provided in <mxEvent>. It
also takes care of resolving cycles between DOM nodes and JavaScript event
handlers, which can lead to memory leaks in IE6.
Most custom events in mxGraph are implemented using <mxEventSource>. Its
listeners are functions that take a sender and <mxEventObject>. Additionally,
the <mxGraph> class fires special <mxMouseEvents> which are handled using
mouse listeners, which are objects that provide a mousedown, mousemove and
mouseup method.
Events in <mxEventSource> are fired using <mxEventSource.fireEvent>.
Listeners are added and removed using <mxEventSource.addListener> and
<mxEventSource.removeListener>. <mxMouseEvents> in <mxGraph> are fired using
<mxGraph.fireMouseEvent>. Listeners are added and removed using
<mxGraph.addMouseListener> and <mxGraph.removeMouseListener>, respectively.
Key bindings:
The following key bindings are defined for mouse events in the client across
all browsers and platforms:
- Control-Drag: Duplicates (clones) selected cells
- Shift-Rightlick: Shows the context menu
- Alt-Click: Forces rubberband (aka. marquee)
- Control-Select: Toggles the selection state
- Shift-Drag: Constrains the offset to one direction
- Shift-Control-Drag: Panning (also Shift-Rightdrag)
Configuration:
The following global variables may be defined before the client is loaded to
specify its language or base path, respectively.
- mxBasePath: Specifies the path in <mxClient.basePath>.
- mxImageBasePath: Specifies the path in <mxClient.imageBasePath>.
- mxLanguage: Specifies the language for resources in <mxClient.language>.
- mxDefaultLanguage: Specifies the default language in <mxClient.defaultLanguage>.
- mxLoadResources: Specifies if any resources should be loaded. Default is true.
- mxLoadStylesheets: Specifies if any stylesheets should be loaded. Default is true.
Reserved Words:
The mx prefix is used for all classes and objects in mxGraph. The mx prefix
can be seen as the global namespace for all JavaScript code in mxGraph. The
following fieldnames should not be used in objects.
- *mxObjectId*: If the object is used with mxObjectIdentity
- *as*: If the object is a field of another object
- *id*: If the object is an idref in a codec
- *mxListenerList*: Added to DOM nodes when used with <mxEvent>
- *window._mxDynamicCode*: Temporarily used to load code in Safari and Chrome
(see <mxClient.include>).
- *_mxJavaScriptExpression*: Global variable that is temporarily used to
evaluate code in Safari, Opera, Firefox 3 and IE (see <mxUtils.eval>).
Files:
The library contains these relative filenames. All filenames are relative
to <mxClient.basePath>.
Built-in Images:
All images are loaded from the <mxClient.imageBasePath>,
which you can change to reflect your environment. The image variables can
also be changed individually.
- mxGraph.prototype.collapsedImage
- mxGraph.prototype.expandedImage
- mxGraph.prototype.warningImage
- mxWindow.prototype.closeImage
- mxWindow.prototype.minimizeImage
- mxWindow.prototype.normalizeImage
- mxWindow.prototype.maximizeImage
- mxWindow.prototype.resizeImage
- mxPopupMenu.prototype.submenuImage
- mxUtils.errorImage
- mxConstraintHandler.prototype.pointImage
The basename of the warning image (images/warning without extension) used in
<mxGraph.setCellWarning> is defined in <mxGraph.warningImage>.
Resources:
The <mxEditor> and <mxGraph> classes add the following resources to
<mxResources> at class loading time:
- resources/editor*.properties
- resources/graph*.properties
By default, the library ships with English and German resource files.
Images:
Recommendations for using images. Use GIF images (256 color palette) in HTML
elements (such as the toolbar and context menu), and PNG images (24 bit) for
all images which appear inside the graph component.
- For PNG images inside HTML elements, Internet Explorer will ignore any
transparency information.
- For GIF images inside the graph, Firefox on the Mac will display strange
colors. Furthermore, only the first image for animated GIFs is displayed
on the Mac.
For faster image rendering during application runtime, images can be
prefetched using the following code:
(code)
var image = new Image();
image.src = url_to_image;
(end)
Deployment:
The client is added to the page using the following script tag inside the
head of a document:
(code)
<script type="text/javascript" src="js/mxClient.js"></script>
(end)
The deployment version of the mxClient.js file contains all required code
in a single file. For deployment, the complete javascript/src directory is
required.
Source Code:
If you are a source code customer and you wish to develop using the
full source code, the commented source code is shipped in the
javascript/devel/source.zip file. It contains one file for each class
in mxGraph. To use the source code the source.zip file must be
uncompressed and the mxClient.js URL in the HTML page must be changed
to reference the uncompressed mxClient.js from the source.zip file.
Compression:
When using Apache2 with mod_deflate, you can use the following directive
in src/js/.htaccess to speedup the loading of the JavaScript sources:
(code)
SetOutputFilter DEFLATE
(end)
Classes:
There are two types of "classes" in mxGraph: classes and singletons (where
only one instance exists). Singletons are mapped to global objects where the
variable name equals the classname. For example mxConstants is an object with
all the constants defined as object fields. Normal classes are mapped to a
constructor function and a prototype which defines the instance fields and
methods. For example, <mxEditor> is a function and mxEditor.prototype is the
prototype for the object that the mxEditor function creates. The mx prefix is
a convention that is used for all classes in the mxGraph package to avoid
conflicts with other objects in the global namespace.
Subclassing:
For subclassing, the superclass must provide a constructor that is either
parameterless or handles an invocation with no arguments. Furthermore, the
special constructor field must be redefined after extending the prototype.
For example, the superclass of mxEditor is <mxEventSource>. This is
represented in JavaScript by first "inheriting" all fields and methods from
the superclass by assigning the prototype to an instance of the superclass,
eg. mxEditor.prototype = new mxEventSource() and redefining the constructor
field using mxEditor.prototype.constructor = mxEditor. The latter rule is
applied so that the type of an object can be retrieved via the name of its
constructor using mxUtils.getFunctionName(obj.constructor).
Constructor:
For subclassing in mxGraph, the same scheme should be applied. For example,
for subclassing the <mxGraph> class, first a constructor must be defined for
the new class. The constructor calls the super constructor with any arguments
that it may have using the call function on the mxGraph function object,
passing along explitely each argument:
(code)
function MyGraph(container)
{
mxGraph.call(this, container);
}
(end)
The prototype of MyGraph inherits from mxGraph as follows. As usual, the
constructor is redefined after extending the superclass:
(code)
MyGraph.prototype = new mxGraph();
MyGraph.prototype.constructor = MyGraph;
(end)
You may want to define the codec associated for the class after the above
code. This code will be executed at class loading time and makes sure the
same codec is used to encode instances of mxGraph and MyGraph.
(code)
var codec = mxCodecRegistry.getCodec(mxGraph);
codec.template = new MyGraph();
mxCodecRegistry.register(codec);
(end)
Functions:
In the prototype for MyGraph, functions of mxGraph can then be extended as
follows.
(code)
MyGraph.prototype.isCellSelectable = function(cell)
{
var selectable = mxGraph.prototype.isSelectable.apply(this, arguments);
var geo = this.model.getGeometry(cell);
return selectable && (geo == null || !geo.relative);
}
(end)
The supercall in the first line is optional. It is done using the apply
function on the isSelectable function object of the mxGraph prototype, using
the special this and arguments variables as parameters. Calls to the
superclass function are only possible if the function is not replaced in the
superclass as follows, which is another way of “subclassing” in JavaScript.
(code)
mxGraph.prototype.isCellSelectable = function(cell)
{
var geo = this.model.getGeometry(cell);
return selectable &&
(geo == null ||
!geo.relative);
}
(end)
The above scheme is useful if a function definition needs to be replaced
completely.
In order to add new functions and fields to the subclass, the following code
is used. The example below adds a new function to return the XML
representation of the graph model:
(code)
MyGraph.prototype.getXml = function()
{
var enc = new mxCodec();
return enc.encode(this.getModel());
}
(end)
Variables:
Likewise, a new field is declared and defined as follows.
(code)
MyGraph.prototype.myField = 'Hello, World!';
(end)
Note that the value assigned to myField is created only once, that is, all
instances of MyGraph share the same value. If you require instance-specific
values, then the field must be defined in the constructor instead.
(code)
function MyGraph(container)
{
mxGraph.call(this, container);
this.myField = new Array();
}
(end)
Finally, a new instance of MyGraph is created using the following code, where
container is a DOM node that acts as a container for the graph view:
(code)
var graph = new MyGraph(container);
(end)

View File

@ -0,0 +1,189 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
mxCodecRegistry.register(function()
{
/**
* Class: mxCellCodec
*
* Codec for <mxCell>s. This class is created and registered
* dynamically at load time and used implicitly via <mxCodec>
* and the <mxCodecRegistry>.
*
* Transient Fields:
*
* - children
* - edges
* - overlays
* - mxTransient
*
* Reference Fields:
*
* - parent
* - source
* - target
*
* Transient fields can be added using the following code:
*
* mxCodecRegistry.getCodec(mxCell).exclude.push('name_of_field');
*
* To subclass <mxCell>, replace the template and add an alias as
* follows.
*
* (code)
* function CustomCell(value, geometry, style)
* {
* mxCell.apply(this, arguments);
* }
*
* mxUtils.extend(CustomCell, mxCell);
*
* mxCodecRegistry.getCodec(mxCell).template = new CustomCell();
* mxCodecRegistry.addAlias('CustomCell', 'mxCell');
* (end)
*/
var codec = new mxObjectCodec(new mxCell(),
['children', 'edges', 'overlays', 'mxTransient'],
['parent', 'source', 'target']);
/**
* Function: isCellCodec
*
* Returns true since this is a cell codec.
*/
codec.isCellCodec = function()
{
return true;
};
/**
* Overidden to disable conversion of value to number.
*/
codec.isNumericAttribute = function(dec, attr, obj)
{
return attr.nodeName !== 'value' && mxObjectCodec.prototype.isNumericAttribute.apply(this, arguments);
};
/**
* Function: isExcluded
*
* Excludes user objects that are XML nodes.
*/
codec.isExcluded = function(obj, attr, value, isWrite)
{
return mxObjectCodec.prototype.isExcluded.apply(this, arguments) ||
(isWrite && attr == 'value' &&
value.nodeType == mxConstants.NODETYPE_ELEMENT);
};
/**
* Function: afterEncode
*
* Encodes an <mxCell> and wraps the XML up inside the
* XML of the user object (inversion).
*/
codec.afterEncode = function(enc, obj, node)
{
if (obj.value != null && obj.value.nodeType == mxConstants.NODETYPE_ELEMENT)
{
// Wraps the graphical annotation up in the user object (inversion)
// by putting the result of the default encoding into a clone of the
// user object (node type 1) and returning this cloned user object.
var tmp = node;
node = mxUtils.importNode(enc.document, obj.value, true);
node.appendChild(tmp);
// Moves the id attribute to the outermost XML node, namely the
// node which denotes the object boundaries in the file.
var id = tmp.getAttribute('id');
node.setAttribute('id', id);
tmp.removeAttribute('id');
}
return node;
};
/**
* Function: beforeDecode
*
* Decodes an <mxCell> and uses the enclosing XML node as
* the user object for the cell (inversion).
*/
codec.beforeDecode = function(dec, node, obj)
{
var inner = node.cloneNode(true);
var classname = this.getName();
if (node.nodeName != classname)
{
// Passes the inner graphical annotation node to the
// object codec for further processing of the cell.
var tmp = node.getElementsByTagName(classname)[0];
if (tmp != null && tmp.parentNode == node)
{
mxUtils.removeWhitespace(tmp, true);
mxUtils.removeWhitespace(tmp, false);
tmp.parentNode.removeChild(tmp);
inner = tmp;
}
else
{
inner = null;
}
// Creates the user object out of the XML node
obj.value = node.cloneNode(true);
var id = obj.value.getAttribute('id');
if (id != null)
{
obj.setId(id);
obj.value.removeAttribute('id');
}
}
else
{
// Uses ID from XML file as ID for cell in model
obj.setId(node.getAttribute('id'));
}
// Preprocesses and removes all Id-references in order to use the
// correct encoder (this) for the known references to cells (all).
if (inner != null)
{
for (var i = 0; i < this.idrefs.length; i++)
{
var attr = this.idrefs[i];
var ref = inner.getAttribute(attr);
if (ref != null)
{
inner.removeAttribute(attr);
var object = dec.objects[ref] || dec.lookup(ref);
if (object == null)
{
// Needs to decode forward reference
var element = dec.getElementById(ref);
if (element != null)
{
var decoder = mxCodecRegistry.codecs[element.nodeName] || this;
object = decoder.decode(dec, element);
}
}
obj[attr] = object;
}
}
}
return inner;
};
// Returns the codec into the registry
return codec;
}());

View File

@ -0,0 +1,168 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
mxCodecRegistry.register(function()
{
/**
* Class: mxChildChangeCodec
*
* Codec for <mxChildChange>s. This class is created and registered
* dynamically at load time and used implicitly via <mxCodec> and
* the <mxCodecRegistry>.
*
* Transient Fields:
*
* - model
* - previous
* - previousIndex
* - child
*
* Reference Fields:
*
* - parent
*/
var codec = new mxObjectCodec(new mxChildChange(),
['model', 'child', 'previousIndex'],
['parent', 'previous']);
/**
* Function: isReference
*
* Returns true for the child attribute if the child
* cell had a previous parent or if we're reading the
* child as an attribute rather than a child node, in
* which case it's always a reference.
*/
codec.isReference = function(obj, attr, value, isWrite)
{
if (attr == 'child' && (!isWrite || obj.model.contains(obj.previous)))
{
return true;
}
return mxUtils.indexOf(this.idrefs, attr) >= 0;
};
/**
* Function: isExcluded
*
* Excludes references to parent or previous if not in the model.
*/
codec.isExcluded = function(obj, attr, value, write)
{
return mxObjectCodec.prototype.isExcluded.apply(this, arguments) ||
(write && value != null && (attr == 'previous' ||
attr == 'parent') && !obj.model.contains(value));
};
/**
* Function: afterEncode
*
* Encodes the child recusively and adds the result
* to the given node.
*/
codec.afterEncode = function(enc, obj, node)
{
if (this.isReference(obj, 'child', obj.child, true))
{
// Encodes as reference (id)
node.setAttribute('child', enc.getId(obj.child));
}
else
{
// At this point, the encoder is no longer able to know which cells
// are new, so we have to encode the complete cell hierarchy and
// ignore the ones that are already there at decoding time. Note:
// This can only be resolved by moving the notify event into the
// execute of the edit.
enc.encodeCell(obj.child, node);
}
return node;
};
/**
* Function: beforeDecode
*
* Decodes the any child nodes as using the respective
* codec from the registry.
*/
codec.beforeDecode = function(dec, node, obj)
{
if (node.firstChild != null &&
node.firstChild.nodeType == mxConstants.NODETYPE_ELEMENT)
{
// Makes sure the original node isn't modified
node = node.cloneNode(true);
var tmp = node.firstChild;
obj.child = dec.decodeCell(tmp, false);
var tmp2 = tmp.nextSibling;
tmp.parentNode.removeChild(tmp);
tmp = tmp2;
while (tmp != null)
{
tmp2 = tmp.nextSibling;
if (tmp.nodeType == mxConstants.NODETYPE_ELEMENT)
{
// Ignores all existing cells because those do not need to
// be re-inserted into the model. Since the encoded version
// of these cells contains the new parent, this would leave
// to an inconsistent state on the model (ie. a parent
// change without a call to parentForCellChanged).
var id = tmp.getAttribute('id');
if (dec.lookup(id) == null)
{
dec.decodeCell(tmp);
}
}
tmp.parentNode.removeChild(tmp);
tmp = tmp2;
}
}
else
{
var childRef = node.getAttribute('child');
obj.child = dec.getObject(childRef);
}
return node;
};
/**
* Function: afterDecode
*
* Restores object state in the child change.
*/
codec.afterDecode = function(dec, node, obj)
{
// Cells are decoded here after a complete transaction so the previous
// parent must be restored on the cell for the case where the cell was
// added. This is needed for the local model to identify the cell as a
// new cell and register the ID.
if (obj.child != null)
{
if (obj.child.parent != null && obj.previous != null &&
obj.child.parent != obj.previous)
{
obj.previous = obj.child.parent;
}
obj.child.parent = obj.previous;
obj.previous = obj.parent;
obj.previousIndex = obj.index;
}
return obj;
};
// Returns the codec into the registry
return codec;
}());

View File

@ -0,0 +1,621 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxCodec
*
* XML codec for JavaScript object graphs. See <mxObjectCodec> for a
* description of the general encoding/decoding scheme. This class uses the
* codecs registered in <mxCodecRegistry> for encoding/decoding each object.
*
* References:
*
* In order to resolve references, especially forward references, the mxCodec
* constructor must be given the document that contains the referenced
* elements.
*
* Examples:
*
* The following code is used to encode a graph model.
*
* (code)
* var encoder = new mxCodec();
* var result = encoder.encode(graph.getModel());
* var xml = mxUtils.getXml(result);
* (end)
*
* Example:
*
* Using the code below, an XML document is decoded into an existing model. The
* document may be obtained using one of the functions in mxUtils for loading
* an XML file, eg. <mxUtils.get>, or using <mxUtils.parseXml> for parsing an
* XML string.
*
* (code)
* var doc = mxUtils.parseXml(xmlString);
* var codec = new mxCodec(doc);
* codec.decode(doc.documentElement, graph.getModel());
* (end)
*
* Example:
*
* This example demonstrates parsing a list of isolated cells into an existing
* graph model. Note that the cells do not have a parent reference so they can
* be added anywhere in the cell hierarchy after parsing.
*
* (code)
* var xml = '<root><mxCell id="2" value="Hello," vertex="1"><mxGeometry x="20" y="20" width="80" height="30" as="geometry"/></mxCell><mxCell id="3" value="World!" vertex="1"><mxGeometry x="200" y="150" width="80" height="30" as="geometry"/></mxCell><mxCell id="4" value="" edge="1" source="2" target="3"><mxGeometry relative="1" as="geometry"/></mxCell></root>';
* var doc = mxUtils.parseXml(xml);
* var codec = new mxCodec(doc);
* var elt = doc.documentElement.firstChild;
* var cells = [];
*
* while (elt != null)
* {
* cells.push(codec.decode(elt));
* elt = elt.nextSibling;
* }
*
* graph.addCells(cells);
* (end)
*
* Example:
*
* Using the following code, the selection cells of a graph are encoded and the
* output is displayed in a dialog box.
*
* (code)
* var enc = new mxCodec();
* var cells = graph.getSelectionCells();
* mxUtils.alert(mxUtils.getPrettyXml(enc.encode(cells)));
* (end)
*
* Newlines in the XML can be converted to <br>, in which case a '<br>' argument
* must be passed to <mxUtils.getXml> as the second argument.
*
* Debugging:
*
* For debugging I/O you can use the following code to get the sequence of
* encoded objects:
*
* (code)
* var oldEncode = mxCodec.prototype.encode;
* mxCodec.prototype.encode = function(obj)
* {
* mxLog.show();
* mxLog.debug('mxCodec.encode: obj='+mxUtils.getFunctionName(obj.constructor));
*
* return oldEncode.apply(this, arguments);
* };
* (end)
*
* Note that the I/O system adds object codecs for new object automatically. For
* decoding those objects, the constructor should be written as follows:
*
* (code)
* var MyObj = function(name)
* {
* // ...
* };
* (end)
*
* Constructor: mxCodec
*
* Constructs an XML encoder/decoder for the specified
* owner document.
*
* Parameters:
*
* document - Optional XML document that contains the data.
* If no document is specified then a new document is created
* using <mxUtils.createXmlDocument>.
*/
function mxCodec(document)
{
this.document = document || mxUtils.createXmlDocument();
this.objects = [];
};
/**
* Variable: document
*
* The owner document of the codec.
*/
mxCodec.prototype.document = null;
/**
* Variable: objects
*
* Maps from IDs to objects.
*/
mxCodec.prototype.objects = null;
/**
* Variable: elements
*
* Lookup table for resolving IDs to elements.
*/
mxCodec.prototype.elements = null;
/**
* Variable: encodeDefaults
*
* Specifies if default values should be encoded. Default is false.
*/
mxCodec.prototype.encodeDefaults = false;
/**
* Function: putObject
*
* Assoiates the given object with the given ID and returns the given object.
*
* Parameters
*
* id - ID for the object to be associated with.
* obj - Object to be associated with the ID.
*/
mxCodec.prototype.putObject = function(id, obj)
{
this.objects[id] = obj;
return obj;
};
/**
* Function: getObject
*
* Returns the decoded object for the element with the specified ID in
* <document>. If the object is not known then <lookup> is used to find an
* object. If no object is found, then the element with the respective ID
* from the document is parsed using <decode>.
*/
mxCodec.prototype.getObject = function(id)
{
var obj = null;
if (id != null)
{
obj = this.objects[id];
if (obj == null)
{
obj = this.lookup(id);
if (obj == null)
{
var node = this.getElementById(id);
if (node != null)
{
obj = this.decode(node);
}
}
}
}
return obj;
};
/**
* Function: lookup
*
* Hook for subclassers to implement a custom lookup mechanism for cell IDs.
* This implementation always returns null.
*
* Example:
*
* (code)
* var codec = new mxCodec();
* codec.lookup = function(id)
* {
* return model.getCell(id);
* };
* (end)
*
* Parameters:
*
* id - ID of the object to be returned.
*/
mxCodec.prototype.lookup = function(id)
{
return null;
};
/**
* Function: getElementById
*
* Returns the element with the given ID from <document>.
*
* Parameters:
*
* id - String that contains the ID.
*/
mxCodec.prototype.getElementById = function(id)
{
this.updateElements();
return this.elements[id];
};
/**
* Function: updateElements
*
* Returns the element with the given ID from <document>.
*
* Parameters:
*
* id - String that contains the ID.
*/
mxCodec.prototype.updateElements = function()
{
if (this.elements == null)
{
this.elements = new Object();
if (this.document.documentElement != null)
{
this.addElement(this.document.documentElement);
}
}
};
/**
* Function: addElement
*
* Adds the given element to <elements> if it has an ID.
*/
mxCodec.prototype.addElement = function(node)
{
if (node.nodeType == mxConstants.NODETYPE_ELEMENT)
{
var id = node.getAttribute('id');
if (id != null)
{
if (this.elements[id] == null)
{
this.elements[id] = node;
}
else if (this.elements[id] != node)
{
throw new Error(id + ': Duplicate ID');
}
}
}
node = node.firstChild;
while (node != null)
{
this.addElement(node);
node = node.nextSibling;
}
};
/**
* Function: getId
*
* Returns the ID of the specified object. This implementation
* calls <reference> first and if that returns null handles
* the object as an <mxCell> by returning their IDs using
* <mxCell.getId>. If no ID exists for the given cell, then
* an on-the-fly ID is generated using <mxCellPath.create>.
*
* Parameters:
*
* obj - Object to return the ID for.
*/
mxCodec.prototype.getId = function(obj)
{
var id = null;
if (obj != null)
{
id = this.reference(obj);
if (id == null && obj instanceof mxCell)
{
id = obj.getId();
if (id == null)
{
// Uses an on-the-fly Id
id = mxCellPath.create(obj);
if (id.length == 0)
{
id = 'root';
}
}
}
}
return id;
};
/**
* Function: reference
*
* Hook for subclassers to implement a custom method
* for retrieving IDs from objects. This implementation
* always returns null.
*
* Example:
*
* (code)
* var codec = new mxCodec();
* codec.reference = function(obj)
* {
* return obj.getCustomId();
* };
* (end)
*
* Parameters:
*
* obj - Object whose ID should be returned.
*/
mxCodec.prototype.reference = function(obj)
{
return null;
};
/**
* Function: encode
*
* Encodes the specified object and returns the resulting
* XML node.
*
* Parameters:
*
* obj - Object to be encoded.
*/
mxCodec.prototype.encode = function(obj)
{
var node = null;
if (obj != null && obj.constructor != null)
{
var enc = mxCodecRegistry.getCodec(obj.constructor);
if (enc != null)
{
node = enc.encode(this, obj);
}
else
{
if (mxUtils.isNode(obj))
{
node = mxUtils.importNode(this.document, obj, true);
}
else
{
mxLog.warn('mxCodec.encode: No codec for ' + mxUtils.getFunctionName(obj.constructor));
}
}
}
return node;
};
/**
* Function: decode
*
* Decodes the given XML node. The optional "into"
* argument specifies an existing object to be
* used. If no object is given, then a new instance
* is created using the constructor from the codec.
*
* The function returns the passed in object or
* the new instance if no object was given.
*
* Parameters:
*
* node - XML node to be decoded.
* into - Optional object to be decodec into.
*/
mxCodec.prototype.decode = function(node, into)
{
this.updateElements();
var obj = null;
if (node != null && node.nodeType == mxConstants.NODETYPE_ELEMENT)
{
var ctor = null;
try
{
ctor = window[node.nodeName];
}
catch (err)
{
// ignore
}
var dec = mxCodecRegistry.getCodec(ctor);
if (dec != null)
{
obj = dec.decode(this, node, into);
}
else
{
obj = node.cloneNode(true);
obj.removeAttribute('as');
}
}
return obj;
};
/**
* Function: encodeCell
*
* Encoding of cell hierarchies is built-into the core, but
* is a higher-level function that needs to be explicitely
* used by the respective object encoders (eg. <mxModelCodec>,
* <mxChildChangeCodec> and <mxRootChangeCodec>). This
* implementation writes the given cell and its children as a
* (flat) sequence into the given node. The children are not
* encoded if the optional includeChildren is false. The
* function is in charge of adding the result into the
* given node and has no return value.
*
* Parameters:
*
* cell - <mxCell> to be encoded.
* node - Parent XML node to add the encoded cell into.
* includeChildren - Optional boolean indicating if the
* function should include all descendents. Default is true.
*/
mxCodec.prototype.encodeCell = function(cell, node, includeChildren)
{
node.appendChild(this.encode(cell));
if (includeChildren == null || includeChildren)
{
var childCount = cell.getChildCount();
for (var i = 0; i < childCount; i++)
{
this.encodeCell(cell.getChildAt(i), node);
}
}
};
/**
* Function: isCellCodec
*
* Returns true if the given codec is a cell codec. This uses
* <mxCellCodec.isCellCodec> to check if the codec is of the
* given type.
*/
mxCodec.prototype.isCellCodec = function(codec)
{
if (codec != null && typeof(codec.isCellCodec) == 'function')
{
return codec.isCellCodec();
}
return false;
};
/**
* Function: decodeCell
*
* Decodes cells that have been encoded using inversion, ie.
* where the user object is the enclosing node in the XML,
* and restores the group and graph structure in the cells.
* Returns a new <mxCell> instance that represents the
* given node.
*
* Parameters:
*
* node - XML node that contains the cell data.
* restoreStructures - Optional boolean indicating whether
* the graph structure should be restored by calling insert
* and insertEdge on the parent and terminals, respectively.
* Default is true.
*/
mxCodec.prototype.decodeCell = function(node, restoreStructures)
{
restoreStructures = (restoreStructures != null) ? restoreStructures : true;
var cell = null;
if (node != null && node.nodeType == mxConstants.NODETYPE_ELEMENT)
{
// Tries to find a codec for the given node name. If that does
// not return a codec then the node is the user object (an XML node
// that contains the mxCell, aka inversion).
var decoder = mxCodecRegistry.getCodec(node.nodeName);
// Tries to find the codec for the cell inside the user object.
// This assumes all node names inside the user object are either
// not registered or they correspond to a class for cells.
if (!this.isCellCodec(decoder))
{
var child = node.firstChild;
while (child != null && !this.isCellCodec(decoder))
{
decoder = mxCodecRegistry.getCodec(child.nodeName);
child = child.nextSibling;
}
}
if (!this.isCellCodec(decoder))
{
decoder = mxCodecRegistry.getCodec(mxCell);
}
cell = decoder.decode(this, node);
if (restoreStructures)
{
this.insertIntoGraph(cell);
}
}
return cell;
};
/**
* Function: insertIntoGraph
*
* Inserts the given cell into its parent and terminal cells.
*/
mxCodec.prototype.insertIntoGraph = function(cell)
{
var parent = cell.parent;
var source = cell.getTerminal(true);
var target = cell.getTerminal(false);
// Fixes possible inconsistencies during insert into graph
cell.setTerminal(null, false);
cell.setTerminal(null, true);
cell.parent = null;
if (parent != null)
{
if (parent == cell)
{
throw new Error(parent.id + ': Self Reference');
}
else
{
parent.insert(cell);
}
}
if (source != null)
{
source.insertEdge(cell, true);
}
if (target != null)
{
target.insertEdge(cell, false);
}
};
/**
* Function: setAttribute
*
* Sets the attribute on the specified node to value. This is a
* helper method that makes sure the attribute and value arguments
* are not null.
*
* Parameters:
*
* node - XML node to set the attribute for.
* attributes - Attributename to be set.
* value - New value of the attribute.
*/
mxCodec.prototype.setAttribute = function(node, attribute, value)
{
if (attribute != null && value != null)
{
node.setAttribute(attribute, value);
}
};

View File

@ -0,0 +1,137 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
var mxCodecRegistry =
{
/**
* Class: mxCodecRegistry
*
* Singleton class that acts as a global registry for codecs.
*
* Adding an <mxCodec>:
*
* 1. Define a default codec with a new instance of the
* object to be handled.
*
* (code)
* var codec = new mxObjectCodec(new mxGraphModel());
* (end)
*
* 2. Define the functions required for encoding and decoding
* objects.
*
* (code)
* codec.encode = function(enc, obj) { ... }
* codec.decode = function(dec, node, into) { ... }
* (end)
*
* 3. Register the codec in the <mxCodecRegistry>.
*
* (code)
* mxCodecRegistry.register(codec);
* (end)
*
* <mxObjectCodec.decode> may be used to either create a new
* instance of an object or to configure an existing instance,
* in which case the into argument points to the existing
* object. In this case, we say the codec "configures" the
* object.
*
* Variable: codecs
*
* Maps from constructor names to codecs.
*/
codecs: [],
/**
* Variable: aliases
*
* Maps from classnames to codecnames.
*/
aliases: [],
/**
* Function: register
*
* Registers a new codec and associates the name of the template
* constructor in the codec with the codec object.
*
* Parameters:
*
* codec - <mxObjectCodec> to be registered.
*/
register: function(codec)
{
if (codec != null)
{
var name = codec.getName();
mxCodecRegistry.codecs[name] = codec;
var classname = mxUtils.getFunctionName(codec.template.constructor);
if (classname != name)
{
mxCodecRegistry.addAlias(classname, name);
}
}
return codec;
},
/**
* Function: addAlias
*
* Adds an alias for mapping a classname to a codecname.
*/
addAlias: function(classname, codecname)
{
mxCodecRegistry.aliases[classname] = codecname;
},
/**
* Function: getCodec
*
* Returns a codec that handles objects that are constructed
* using the given constructor.
*
* Parameters:
*
* ctor - JavaScript constructor function.
*/
getCodec: function(ctor)
{
var codec = null;
if (ctor != null)
{
var name = mxUtils.getFunctionName(ctor);
var tmp = mxCodecRegistry.aliases[name];
if (tmp != null)
{
name = tmp;
}
codec = mxCodecRegistry.codecs[name];
// Registers a new default codec for the given constructor
// if no codec has been previously defined.
if (codec == null)
{
try
{
codec = new mxObjectCodec(new ctor());
mxCodecRegistry.register(codec);
}
catch (e)
{
// ignore
}
}
}
return codec;
}
};

View File

@ -0,0 +1,88 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
mxCodecRegistry.register(function()
{
/**
* Class: mxDefaultKeyHandlerCodec
*
* Custom codec for configuring <mxDefaultKeyHandler>s. This class is created
* and registered dynamically at load time and used implicitly via
* <mxCodec> and the <mxCodecRegistry>. This codec only reads configuration
* data for existing key handlers, it does not encode or create key handlers.
*/
var codec = new mxObjectCodec(new mxDefaultKeyHandler());
/**
* Function: encode
*
* Returns null.
*/
codec.encode = function(enc, obj)
{
return null;
};
/**
* Function: decode
*
* Reads a sequence of the following child nodes
* and attributes:
*
* Child Nodes:
*
* add - Binds a keystroke to an actionname.
*
* Attributes:
*
* as - Keycode.
* action - Actionname to execute in editor.
* control - Optional boolean indicating if
* the control key must be pressed.
*
* Example:
*
* (code)
* <mxDefaultKeyHandler as="keyHandler">
* <add as="88" control="true" action="cut"/>
* <add as="67" control="true" action="copy"/>
* <add as="86" control="true" action="paste"/>
* </mxDefaultKeyHandler>
* (end)
*
* The keycodes are for the x, c and v keys.
*
* See also: <mxDefaultKeyHandler.bindAction>,
* http://www.js-examples.com/page/tutorials__key_codes.html
*/
codec.decode = function(dec, node, into)
{
if (into != null)
{
var editor = into.editor;
node = node.firstChild;
while (node != null)
{
if (!this.processInclude(dec, node, into) &&
node.nodeName == 'add')
{
var as = node.getAttribute('as');
var action = node.getAttribute('action');
var control = node.getAttribute('control');
into.bindAction(as, action, control);
}
node = node.nextSibling;
}
}
return into;
};
// Returns the codec into the registry
return codec;
}());

View File

@ -0,0 +1,54 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
mxCodecRegistry.register(function()
{
/**
* Class: mxDefaultPopupMenuCodec
*
* Custom codec for configuring <mxDefaultPopupMenu>s. This class is created
* and registered dynamically at load time and used implicitly via
* <mxCodec> and the <mxCodecRegistry>. This codec only reads configuration
* data for existing popup menus, it does not encode or create menus. Note
* that this codec only passes the configuration node to the popup menu,
* which uses the config to dynamically create menus. See
* <mxDefaultPopupMenu.createMenu>.
*/
var codec = new mxObjectCodec(new mxDefaultPopupMenu());
/**
* Function: encode
*
* Returns null.
*/
codec.encode = function(enc, obj)
{
return null;
};
/**
* Function: decode
*
* Uses the given node as the config for <mxDefaultPopupMenu>.
*/
codec.decode = function(dec, node, into)
{
var inc = node.getElementsByTagName('include')[0];
if (inc != null)
{
this.processInclude(dec, inc, into);
}
else if (into != null)
{
into.config = node;
}
return into;
};
// Returns the codec into the registry
return codec;
}());

View File

@ -0,0 +1,312 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxDefaultToolbarCodec
*
* Custom codec for configuring <mxDefaultToolbar>s. This class is created
* and registered dynamically at load time and used implicitly via
* <mxCodec> and the <mxCodecRegistry>. This codec only reads configuration
* data for existing toolbars handlers, it does not encode or create toolbars.
*/
var mxDefaultToolbarCodec = mxCodecRegistry.register(function()
{
var codec = new mxObjectCodec(new mxDefaultToolbar());
/**
* Function: encode
*
* Returns null.
*/
codec.encode = function(enc, obj)
{
return null;
};
/**
* Function: decode
*
* Reads a sequence of the following child nodes
* and attributes:
*
* Child Nodes:
*
* add - Adds a new item to the toolbar. See below for attributes.
* separator - Adds a vertical separator. No attributes.
* hr - Adds a horizontal separator. No attributes.
* br - Adds a linefeed. No attributes.
*
* Attributes:
*
* as - Resource key for the label.
* action - Name of the action to execute in enclosing editor.
* mode - Modename (see below).
* template - Template name for cell insertion.
* style - Optional style to override the template style.
* icon - Icon (relative/absolute URL).
* pressedIcon - Optional icon for pressed state (relative/absolute URL).
* id - Optional ID to be used for the created DOM element.
* toggle - Optional 0 or 1 to disable toggling of the element. Default is
* 1 (true).
*
* The action, mode and template attributes are mutually exclusive. The
* style can only be used with the template attribute. The add node may
* contain another sequence of add nodes with as and action attributes
* to create a combo box in the toolbar. If the icon is specified then
* a list of the child node is expected to have its template attribute
* set and the action is ignored instead.
*
* Nodes with a specified template may define a function to be used for
* inserting the cloned template into the graph. Here is an example of such
* a node:
*
* (code)
* <add as="Swimlane" template="swimlane" icon="images/swimlane.gif"><![CDATA[
* function (editor, cell, evt, targetCell)
* {
* var pt = mxUtils.convertPoint(
* editor.graph.container, mxEvent.getClientX(evt),
* mxEvent.getClientY(evt));
* return editor.addVertex(targetCell, cell, pt.x, pt.y);
* }
* ]]></add>
* (end)
*
* In the above function, editor is the enclosing <mxEditor> instance, cell
* is the clone of the template, evt is the mouse event that represents the
* drop and targetCell is the cell under the mousepointer where the drop
* occurred. The targetCell is retrieved using <mxGraph.getCellAt>.
*
* Futhermore, nodes with the mode attribute may define a function to
* be executed upon selection of the respective toolbar icon. In the
* example below, the default edge style is set when this specific
* connect-mode is activated:
*
* (code)
* <add as="connect" mode="connect"><![CDATA[
* function (editor)
* {
* if (editor.defaultEdge != null)
* {
* editor.defaultEdge.style = 'straightEdge';
* }
* }
* ]]></add>
* (end)
*
* Both functions require <mxDefaultToolbarCodec.allowEval> to be set to true.
*
* Modes:
*
* select - Left mouse button used for rubberband- & cell-selection.
* connect - Allows connecting vertices by inserting new edges.
* pan - Disables selection and switches to panning on the left button.
*
* Example:
*
* To add items to the toolbar:
*
* (code)
* <mxDefaultToolbar as="toolbar">
* <add as="save" action="save" icon="images/save.gif"/>
* <br/><hr/>
* <add as="select" mode="select" icon="images/select.gif"/>
* <add as="connect" mode="connect" icon="images/connect.gif"/>
* </mxDefaultToolbar>
* (end)
*/
codec.decode = function(dec, node, into)
{
if (into != null)
{
var editor = into.editor;
node = node.firstChild;
while (node != null)
{
if (node.nodeType == mxConstants.NODETYPE_ELEMENT)
{
if (!this.processInclude(dec, node, into))
{
if (node.nodeName == 'separator')
{
into.addSeparator();
}
else if (node.nodeName == 'br')
{
into.toolbar.addBreak();
}
else if (node.nodeName == 'hr')
{
into.toolbar.addLine();
}
else if (node.nodeName == 'add')
{
var as = node.getAttribute('as');
as = mxResources.get(as) || as;
var icon = node.getAttribute('icon');
var pressedIcon = node.getAttribute('pressedIcon');
var action = node.getAttribute('action');
var mode = node.getAttribute('mode');
var template = node.getAttribute('template');
var toggle = node.getAttribute('toggle') != '0';
var text = mxUtils.getTextContent(node);
var elt = null;
if (action != null)
{
elt = into.addItem(as, icon, action, pressedIcon);
}
else if (mode != null)
{
var funct = (mxDefaultToolbarCodec.allowEval) ? mxUtils.eval(text) : null;
elt = into.addMode(as, icon, mode, pressedIcon, funct);
}
else if (template != null || (text != null && text.length > 0))
{
var cell = editor.templates[template];
var style = node.getAttribute('style');
if (cell != null && style != null)
{
cell = editor.graph.cloneCell(cell);
cell.setStyle(style);
}
var insertFunction = null;
if (text != null && text.length > 0 && mxDefaultToolbarCodec.allowEval)
{
insertFunction = mxUtils.eval(text);
}
elt = into.addPrototype(as, icon, cell, pressedIcon, insertFunction, toggle);
}
else
{
var children = mxUtils.getChildNodes(node);
if (children.length > 0)
{
if (icon == null)
{
var combo = into.addActionCombo(as);
for (var i=0; i<children.length; i++)
{
var child = children[i];
if (child.nodeName == 'separator')
{
into.addOption(combo, '---');
}
else if (child.nodeName == 'add')
{
var lab = child.getAttribute('as');
var act = child.getAttribute('action');
into.addActionOption(combo, lab, act);
}
}
}
else
{
var select = null;
var create = function()
{
var template = editor.templates[select.value];
if (template != null)
{
var clone = template.clone();
var style = select.options[select.selectedIndex].cellStyle;
if (style != null)
{
clone.setStyle(style);
}
return clone;
}
else
{
mxLog.warn('Template '+template+' not found');
}
return null;
};
var img = into.addPrototype(as, icon, create, null, null, toggle);
select = into.addCombo();
// Selects the toolbar icon if a selection change
// is made in the corresponding combobox.
mxEvent.addListener(select, 'change', function()
{
into.toolbar.selectMode(img, function(evt)
{
var pt = mxUtils.convertPoint(editor.graph.container,
mxEvent.getClientX(evt), mxEvent.getClientY(evt));
return editor.addVertex(null, funct(), pt.x, pt.y);
});
into.toolbar.noReset = false;
});
// Adds the entries to the combobox
for (var i=0; i<children.length; i++)
{
var child = children[i];
if (child.nodeName == 'separator')
{
into.addOption(select, '---');
}
else if (child.nodeName == 'add')
{
var lab = child.getAttribute('as');
var tmp = child.getAttribute('template');
var option = into.addOption(select, lab, tmp || template);
option.cellStyle = child.getAttribute('style');
}
}
}
}
}
// Assigns an ID to the created element to access it later.
if (elt != null)
{
var id = node.getAttribute('id');
if (id != null && id.length > 0)
{
elt.setAttribute('id', id);
}
}
}
}
}
node = node.nextSibling;
}
}
return into;
};
// Returns the codec into the registry
return codec;
}());
/**
* Variable: allowEval
*
* Static global switch that specifies if the use of eval is allowed for
* evaluating text content. Default is true. Set this to false if stylesheets
* may contain user input
*/
mxDefaultToolbarCodec.allowEval = true;

View File

@ -0,0 +1,245 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
mxCodecRegistry.register(function()
{
/**
* Class: mxEditorCodec
*
* Codec for <mxEditor>s. This class is created and registered
* dynamically at load time and used implicitly via <mxCodec>
* and the <mxCodecRegistry>.
*
* Transient Fields:
*
* - modified
* - lastSnapshot
* - ignoredChanges
* - undoManager
* - graphContainer
* - toolbarContainer
*/
var codec = new mxObjectCodec(new mxEditor(),
['modified', 'lastSnapshot', 'ignoredChanges',
'undoManager', 'graphContainer', 'toolbarContainer']);
/**
* Function: beforeDecode
*
* Decodes the ui-part of the configuration node by reading
* a sequence of the following child nodes and attributes
* and passes the control to the default decoding mechanism:
*
* Child Nodes:
*
* stylesheet - Adds a CSS stylesheet to the document.
* resource - Adds the basename of a resource bundle.
* add - Creates or configures a known UI element.
*
* These elements may appear in any order given that the
* graph UI element is added before the toolbar element
* (see Known Keys).
*
* Attributes:
*
* as - Key for the UI element (see below).
* element - ID for the element in the document.
* style - CSS style to be used for the element or window.
* x - X coordinate for the new window.
* y - Y coordinate for the new window.
* width - Width for the new window.
* height - Optional height for the new window.
* name - Name of the stylesheet (absolute/relative URL).
* basename - Basename of the resource bundle (see <mxResources>).
*
* The x, y, width and height attributes are used to create a new
* <mxWindow> if the element attribute is not specified in an add
* node. The name and basename are only used in the stylesheet and
* resource nodes, respectively.
*
* Known Keys:
*
* graph - Main graph element (see <mxEditor.setGraphContainer>).
* title - Title element (see <mxEditor.setTitleContainer>).
* toolbar - Toolbar element (see <mxEditor.setToolbarContainer>).
* status - Status bar element (see <mxEditor.setStatusContainer>).
*
* Example:
*
* (code)
* <ui>
* <stylesheet name="css/process.css"/>
* <resource basename="resources/app"/>
* <add as="graph" element="graph"
* style="left:70px;right:20px;top:20px;bottom:40px"/>
* <add as="status" element="status"/>
* <add as="toolbar" x="10" y="20" width="54"/>
* </ui>
* (end)
*/
codec.afterDecode = function(dec, node, obj)
{
// Assigns the specified templates for edges
var defaultEdge = node.getAttribute('defaultEdge');
if (defaultEdge != null)
{
node.removeAttribute('defaultEdge');
obj.defaultEdge = obj.templates[defaultEdge];
}
// Assigns the specified templates for groups
var defaultGroup = node.getAttribute('defaultGroup');
if (defaultGroup != null)
{
node.removeAttribute('defaultGroup');
obj.defaultGroup = obj.templates[defaultGroup];
}
return obj;
};
/**
* Function: decodeChild
*
* Overrides decode child to handle special child nodes.
*/
codec.decodeChild = function(dec, child, obj)
{
if (child.nodeName == 'Array')
{
var role = child.getAttribute('as');
if (role == 'templates')
{
this.decodeTemplates(dec, child, obj);
return;
}
}
else if (child.nodeName == 'ui')
{
this.decodeUi(dec, child, obj);
return;
}
mxObjectCodec.prototype.decodeChild.apply(this, arguments);
};
/**
* Function: decodeUi
*
* Decodes the ui elements from the given node.
*/
codec.decodeUi = function(dec, node, editor)
{
var tmp = node.firstChild;
while (tmp != null)
{
if (tmp.nodeName == 'add')
{
var as = tmp.getAttribute('as');
var elt = tmp.getAttribute('element');
var style = tmp.getAttribute('style');
var element = null;
if (elt != null)
{
element = document.getElementById(elt);
if (element != null && style != null)
{
element.style.cssText += ';' + style;
}
}
else
{
var x = parseInt(tmp.getAttribute('x'));
var y = parseInt(tmp.getAttribute('y'));
var width = tmp.getAttribute('width');
var height = tmp.getAttribute('height');
// Creates a new window around the element
element = document.createElement('div');
element.style.cssText = style;
var wnd = new mxWindow(mxResources.get(as) || as,
element, x, y, width, height, false, true);
wnd.setVisible(true);
}
// TODO: Make more generic
if (as == 'graph')
{
editor.setGraphContainer(element);
}
else if (as == 'toolbar')
{
editor.setToolbarContainer(element);
}
else if (as == 'title')
{
editor.setTitleContainer(element);
}
else if (as == 'status')
{
editor.setStatusContainer(element);
}
else if (as == 'map')
{
editor.setMapContainer(element);
}
}
else if (tmp.nodeName == 'resource')
{
mxResources.add(tmp.getAttribute('basename'));
}
else if (tmp.nodeName == 'stylesheet')
{
mxClient.link('stylesheet', tmp.getAttribute('name'));
}
tmp = tmp.nextSibling;
}
};
/**
* Function: decodeTemplates
*
* Decodes the cells from the given node as templates.
*/
codec.decodeTemplates = function(dec, node, editor)
{
if (editor.templates == null)
{
editor.templates = [];
}
var children = mxUtils.getChildNodes(node);
for (var j=0; j<children.length; j++)
{
var name = children[j].getAttribute('as');
var child = children[j].firstChild;
while (child != null && child.nodeType != 1)
{
child = child.nextSibling;
}
if (child != null)
{
// LATER: Only single cells means you need
// to group multiple cells within another
// cell. This should be changed to support
// arrays of cells, or the wrapper must
// be automatically handled in this class.
editor.templates[name] = dec.decodeCell(child);
}
}
};
// Returns the codec into the registry
return codec;
}());

View File

@ -0,0 +1,64 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxGenericChangeCodec
*
* Codec for <mxValueChange>s, <mxStyleChange>s, <mxGeometryChange>s,
* <mxCollapseChange>s and <mxVisibleChange>s. This class is created
* and registered dynamically at load time and used implicitly
* via <mxCodec> and the <mxCodecRegistry>.
*
* Transient Fields:
*
* - model
* - previous
*
* Reference Fields:
*
* - cell
*
* Constructor: mxGenericChangeCodec
*
* Factory function that creates a <mxObjectCodec> for
* the specified change and fieldname.
*
* Parameters:
*
* obj - An instance of the change object.
* variable - The fieldname for the change data.
*/
var mxGenericChangeCodec = function(obj, variable)
{
var codec = new mxObjectCodec(obj, ['model', 'previous'], ['cell']);
/**
* Function: afterDecode
*
* Restores the state by assigning the previous value.
*/
codec.afterDecode = function(dec, node, obj)
{
// Allows forward references in sessions. This is a workaround
// for the sequence of edits in mxGraph.moveCells and cellsAdded.
if (mxUtils.isNode(obj.cell))
{
obj.cell = dec.decodeCell(obj.cell, false);
}
obj.previous = obj[variable];
return obj;
};
return codec;
};
// Registers the codecs
mxCodecRegistry.register(mxGenericChangeCodec(new mxValueChange(), 'value'));
mxCodecRegistry.register(mxGenericChangeCodec(new mxStyleChange(), 'style'));
mxCodecRegistry.register(mxGenericChangeCodec(new mxGeometryChange(), 'geometry'));
mxCodecRegistry.register(mxGenericChangeCodec(new mxCollapseChange(), 'collapsed'));
mxCodecRegistry.register(mxGenericChangeCodec(new mxVisibleChange(), 'visible'));
mxCodecRegistry.register(mxGenericChangeCodec(new mxCellAttributeChange(), 'value'));

View File

@ -0,0 +1,28 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
mxCodecRegistry.register(function()
{
/**
* Class: mxGraphCodec
*
* Codec for <mxGraph>s. This class is created and registered
* dynamically at load time and used implicitly via <mxCodec>
* and the <mxCodecRegistry>.
*
* Transient Fields:
*
* - graphListeners
* - eventListeners
* - view
* - container
* - cellRenderer
* - editor
* - selection
*/
return new mxObjectCodec(new mxGraph(),
['graphListeners', 'eventListeners', 'view', 'container',
'cellRenderer', 'editor', 'selection']);
}());

View File

@ -0,0 +1,197 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
mxCodecRegistry.register(function()
{
/**
* Class: mxGraphViewCodec
*
* Custom encoder for <mxGraphView>s. This class is created
* and registered dynamically at load time and used implicitly via
* <mxCodec> and the <mxCodecRegistry>. This codec only writes views
* into a XML format that can be used to create an image for
* the graph, that is, it contains absolute coordinates with
* computed perimeters, edge styles and cell styles.
*/
var codec = new mxObjectCodec(new mxGraphView());
/**
* Function: encode
*
* Encodes the given <mxGraphView> using <encodeCell>
* starting at the model's root. This returns the
* top-level graph node of the recursive encoding.
*/
codec.encode = function(enc, view)
{
return this.encodeCell(enc, view,
view.graph.getModel().getRoot());
};
/**
* Function: encodeCell
*
* Recursively encodes the specifed cell. Uses layer
* as the default nodename. If the cell's parent is
* null, then graph is used for the nodename. If
* <mxGraphModel.isEdge> returns true for the cell,
* then edge is used for the nodename, else if
* <mxGraphModel.isVertex> returns true for the cell,
* then vertex is used for the nodename.
*
* <mxGraph.getLabel> is used to create the label
* attribute for the cell. For graph nodes and vertices
* the bounds are encoded into x, y, width and height.
* For edges the points are encoded into a points
* attribute as a space-separated list of comma-separated
* coordinate pairs (eg. x0,y0 x1,y1 ... xn,yn). All
* values from the cell style are added as attribute
* values to the node.
*/
codec.encodeCell = function(enc, view, cell)
{
var model = view.graph.getModel();
var state = view.getState(cell);
var parent = model.getParent(cell);
if (parent == null || state != null)
{
var childCount = model.getChildCount(cell);
var geo = view.graph.getCellGeometry(cell);
var name = null;
if (parent == model.getRoot())
{
name = 'layer';
}
else if (parent == null)
{
name = 'graph';
}
else if (model.isEdge(cell))
{
name = 'edge';
}
else if (childCount > 0 && geo != null)
{
name = 'group';
}
else if (model.isVertex(cell))
{
name = 'vertex';
}
if (name != null)
{
var node = enc.document.createElement(name);
var lab = view.graph.getLabel(cell);
if (lab != null)
{
node.setAttribute('label', view.graph.getLabel(cell));
if (view.graph.isHtmlLabel(cell))
{
node.setAttribute('html', true);
}
}
if (parent == null)
{
var bounds = view.getGraphBounds();
if (bounds != null)
{
node.setAttribute('x', Math.round(bounds.x));
node.setAttribute('y', Math.round(bounds.y));
node.setAttribute('width', Math.round(bounds.width));
node.setAttribute('height', Math.round(bounds.height));
}
node.setAttribute('scale', view.scale);
}
else if (state != null && geo != null)
{
// Writes each key, value in the style pair to an attribute
for (var i in state.style)
{
var value = state.style[i];
// Tries to turn objects and functions into strings
if (typeof(value) == 'function' &&
typeof(value) == 'object')
{
value = mxStyleRegistry.getName(value);
}
if (value != null &&
typeof(value) != 'function' &&
typeof(value) != 'object')
{
node.setAttribute(i, value);
}
}
var abs = state.absolutePoints;
// Writes the list of points into one attribute
if (abs != null && abs.length > 0)
{
var pts = Math.round(abs[0].x) + ',' + Math.round(abs[0].y);
for (var i=1; i<abs.length; i++)
{
pts += ' ' + Math.round(abs[i].x) + ',' +
Math.round(abs[i].y);
}
node.setAttribute('points', pts);
}
// Writes the bounds into 4 attributes
else
{
node.setAttribute('x', Math.round(state.x));
node.setAttribute('y', Math.round(state.y));
node.setAttribute('width', Math.round(state.width));
node.setAttribute('height', Math.round(state.height));
}
var offset = state.absoluteOffset;
// Writes the offset into 2 attributes
if (offset != null)
{
if (offset.x != 0)
{
node.setAttribute('dx', Math.round(offset.x));
}
if (offset.y != 0)
{
node.setAttribute('dy', Math.round(offset.y));
}
}
}
for (var i=0; i<childCount; i++)
{
var childNode = this.encodeCell(enc,
view, model.getChildAt(cell, i));
if (childNode != null)
{
node.appendChild(childNode);
}
}
}
}
return node;
};
// Returns the codec into the registry
return codec;
}());

View File

@ -0,0 +1,80 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
mxCodecRegistry.register(function()
{
/**
* Class: mxModelCodec
*
* Codec for <mxGraphModel>s. This class is created and registered
* dynamically at load time and used implicitly via <mxCodec>
* and the <mxCodecRegistry>.
*/
var codec = new mxObjectCodec(new mxGraphModel());
/**
* Function: encodeObject
*
* Encodes the given <mxGraphModel> by writing a (flat) XML sequence of
* cell nodes as produced by the <mxCellCodec>. The sequence is
* wrapped-up in a node with the name root.
*/
codec.encodeObject = function(enc, obj, node)
{
var rootNode = enc.document.createElement('root');
enc.encodeCell(obj.getRoot(), rootNode);
node.appendChild(rootNode);
};
/**
* Function: decodeChild
*
* Overrides decode child to handle special child nodes.
*/
codec.decodeChild = function(dec, child, obj)
{
if (child.nodeName == 'root')
{
this.decodeRoot(dec, child, obj);
}
else
{
mxObjectCodec.prototype.decodeChild.apply(this, arguments);
}
};
/**
* Function: decodeRoot
*
* Reads the cells into the graph model. All cells
* are children of the root element in the node.
*/
codec.decodeRoot = function(dec, root, model)
{
var rootCell = null;
var tmp = root.firstChild;
while (tmp != null)
{
var cell = dec.decodeCell(tmp);
if (cell != null && cell.getParent() == null)
{
rootCell = cell;
}
tmp = tmp.nextSibling;
}
// Sets the root on the model if one has been decoded
if (rootCell != null)
{
model.setRoot(rootCell);
}
};
// Returns the codec into the registry
return codec;
}());

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,83 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
mxCodecRegistry.register(function()
{
/**
* Class: mxRootChangeCodec
*
* Codec for <mxRootChange>s. This class is created and registered
* dynamically at load time and used implicitly via <mxCodec> and
* the <mxCodecRegistry>.
*
* Transient Fields:
*
* - model
* - previous
* - root
*/
var codec = new mxObjectCodec(new mxRootChange(),
['model', 'previous', 'root']);
/**
* Function: onEncode
*
* Encodes the child recursively.
*/
codec.afterEncode = function(enc, obj, node)
{
enc.encodeCell(obj.root, node);
return node;
};
/**
* Function: beforeDecode
*
* Decodes the optional children as cells
* using the respective decoder.
*/
codec.beforeDecode = function(dec, node, obj)
{
if (node.firstChild != null &&
node.firstChild.nodeType == mxConstants.NODETYPE_ELEMENT)
{
// Makes sure the original node isn't modified
node = node.cloneNode(true);
var tmp = node.firstChild;
obj.root = dec.decodeCell(tmp, false);
var tmp2 = tmp.nextSibling;
tmp.parentNode.removeChild(tmp);
tmp = tmp2;
while (tmp != null)
{
tmp2 = tmp.nextSibling;
dec.decodeCell(tmp);
tmp.parentNode.removeChild(tmp);
tmp = tmp2;
}
}
return node;
};
/**
* Function: afterDecode
*
* Restores the state by assigning the previous value.
*/
codec.afterDecode = function(dec, node, obj)
{
obj.previous = obj.root;
return obj;
};
// Returns the codec into the registry
return codec;
}());

View File

@ -0,0 +1,217 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxStylesheetCodec
*
* Codec for <mxStylesheet>s. This class is created and registered
* dynamically at load time and used implicitly via <mxCodec>
* and the <mxCodecRegistry>.
*/
var mxStylesheetCodec = mxCodecRegistry.register(function()
{
var codec = new mxObjectCodec(new mxStylesheet());
/**
* Function: encode
*
* Encodes a stylesheet. See <decode> for a description of the
* format.
*/
codec.encode = function(enc, obj)
{
var node = enc.document.createElement(this.getName());
for (var i in obj.styles)
{
var style = obj.styles[i];
var styleNode = enc.document.createElement('add');
if (i != null)
{
styleNode.setAttribute('as', i);
for (var j in style)
{
var value = this.getStringValue(j, style[j]);
if (value != null)
{
var entry = enc.document.createElement('add');
entry.setAttribute('value', value);
entry.setAttribute('as', j);
styleNode.appendChild(entry);
}
}
if (styleNode.childNodes.length > 0)
{
node.appendChild(styleNode);
}
}
}
return node;
};
/**
* Function: getStringValue
*
* Returns the string for encoding the given value.
*/
codec.getStringValue = function(key, value)
{
var type = typeof(value);
if (type == 'function')
{
value = mxStyleRegistry.getName(value);
}
else if (type == 'object')
{
value = null;
}
return value;
};
/**
* Function: decode
*
* Reads a sequence of the following child nodes
* and attributes:
*
* Child Nodes:
*
* add - Adds a new style.
*
* Attributes:
*
* as - Name of the style.
* extend - Name of the style to inherit from.
*
* Each node contains another sequence of add and remove nodes with the following
* attributes:
*
* as - Name of the style (see <mxConstants>).
* value - Value for the style.
*
* Instead of the value-attribute, one can put Javascript expressions into
* the node as follows if <mxStylesheetCodec.allowEval> is true:
* <add as="perimeter">mxPerimeter.RectanglePerimeter</add>
*
* A remove node will remove the entry with the name given in the as-attribute
* from the style.
*
* Example:
*
* (code)
* <mxStylesheet as="stylesheet">
* <add as="text">
* <add as="fontSize" value="12"/>
* </add>
* <add as="defaultVertex" extend="text">
* <add as="shape" value="rectangle"/>
* </add>
* </mxStylesheet>
* (end)
*/
codec.decode = function(dec, node, into)
{
var obj = into || new this.template.constructor();
var id = node.getAttribute('id');
if (id != null)
{
dec.objects[id] = obj;
}
node = node.firstChild;
while (node != null)
{
if (!this.processInclude(dec, node, obj) && node.nodeName == 'add')
{
var as = node.getAttribute('as');
if (as != null)
{
var extend = node.getAttribute('extend');
var style = (extend != null) ? mxUtils.clone(obj.styles[extend]) : null;
if (style == null)
{
if (extend != null)
{
mxLog.warn('mxStylesheetCodec.decode: stylesheet ' +
extend + ' not found to extend');
}
style = new Object();
}
var entry = node.firstChild;
while (entry != null)
{
if (entry.nodeType == mxConstants.NODETYPE_ELEMENT)
{
var key = entry.getAttribute('as');
if (entry.nodeName == 'add')
{
var text = mxUtils.getTextContent(entry);
var value = null;
if (text != null && text.length > 0 && mxStylesheetCodec.allowEval)
{
value = mxUtils.eval(text);
}
else
{
value = entry.getAttribute('value');
if (mxUtils.isNumeric(value))
{
value = parseFloat(value);
}
}
if (value != null)
{
style[key] = value;
}
}
else if (entry.nodeName == 'remove')
{
delete style[key];
}
}
entry = entry.nextSibling;
}
obj.putCellStyle(as, style);
}
}
node = node.nextSibling;
}
return obj;
};
// Returns the codec into the registry
return codec;
}());
/**
* Variable: allowEval
*
* Static global switch that specifies if the use of eval is allowed for
* evaluating text content. Default is true. Set this to false if stylesheets
* may contain user input.
*/
mxStylesheetCodec.allowEval = true;

View File

@ -0,0 +1,42 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
mxCodecRegistry.register(function()
{
/**
* Class: mxTerminalChangeCodec
*
* Codec for <mxTerminalChange>s. This class is created and registered
* dynamically at load time and used implicitly via <mxCodec> and
* the <mxCodecRegistry>.
*
* Transient Fields:
*
* - model
* - previous
*
* Reference Fields:
*
* - cell
* - terminal
*/
var codec = new mxObjectCodec(new mxTerminalChange(),
['model', 'previous'], ['cell', 'terminal']);
/**
* Function: afterDecode
*
* Restores the state by assigning the previous value.
*/
codec.afterDecode = function(dec, node, obj)
{
obj.previous = obj.terminal;
return obj;
};
// Returns the codec into the registry
return codec;
}());

View File

@ -0,0 +1,200 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxGraphAbstractHierarchyCell
*
* An abstraction of an internal hierarchy node or edge
*
* Constructor: mxGraphAbstractHierarchyCell
*
* Constructs a new hierarchical layout algorithm.
*/
function mxGraphAbstractHierarchyCell()
{
this.x = [];
this.y = [];
this.temp = [];
};
/**
* Variable: maxRank
*
* The maximum rank this cell occupies. Default is -1.
*/
mxGraphAbstractHierarchyCell.prototype.maxRank = -1;
/**
* Variable: minRank
*
* The minimum rank this cell occupies. Default is -1.
*/
mxGraphAbstractHierarchyCell.prototype.minRank = -1;
/**
* Variable: x
*
* The x position of this cell for each layer it occupies
*/
mxGraphAbstractHierarchyCell.prototype.x = null;
/**
* Variable: y
*
* The y position of this cell for each layer it occupies
*/
mxGraphAbstractHierarchyCell.prototype.y = null;
/**
* Variable: width
*
* The width of this cell. Default is 0.
*/
mxGraphAbstractHierarchyCell.prototype.width = 0;
/**
* Variable: height
*
* The height of this cell. Default is 0.
*/
mxGraphAbstractHierarchyCell.prototype.height = 0;
/**
* Variable: nextLayerConnectedCells
*
* A cached version of the cells this cell connects to on the next layer up
*/
mxGraphAbstractHierarchyCell.prototype.nextLayerConnectedCells = null;
/**
* Variable: previousLayerConnectedCells
*
* A cached version of the cells this cell connects to on the next layer down
*/
mxGraphAbstractHierarchyCell.prototype.previousLayerConnectedCells = null;
/**
* Variable: temp
*
* Temporary variable for general use. Generally, try to avoid
* carrying information between stages. Currently, the longest
* path layering sets temp to the rank position in fixRanks()
* and the crossing reduction uses this. This meant temp couldn't
* be used for hashing the nodes in the model dfs and so hashCode
* was created
*/
mxGraphAbstractHierarchyCell.prototype.temp = null;
/**
* Function: getNextLayerConnectedCells
*
* Returns the cells this cell connects to on the next layer up
*/
mxGraphAbstractHierarchyCell.prototype.getNextLayerConnectedCells = function(layer)
{
return null;
};
/**
* Function: getPreviousLayerConnectedCells
*
* Returns the cells this cell connects to on the next layer down
*/
mxGraphAbstractHierarchyCell.prototype.getPreviousLayerConnectedCells = function(layer)
{
return null;
};
/**
* Function: isEdge
*
* Returns whether or not this cell is an edge
*/
mxGraphAbstractHierarchyCell.prototype.isEdge = function()
{
return false;
};
/**
* Function: isVertex
*
* Returns whether or not this cell is a node
*/
mxGraphAbstractHierarchyCell.prototype.isVertex = function()
{
return false;
};
/**
* Function: getGeneralPurposeVariable
*
* Gets the value of temp for the specified layer
*/
mxGraphAbstractHierarchyCell.prototype.getGeneralPurposeVariable = function(layer)
{
return null;
};
/**
* Function: setGeneralPurposeVariable
*
* Set the value of temp for the specified layer
*/
mxGraphAbstractHierarchyCell.prototype.setGeneralPurposeVariable = function(layer, value)
{
return null;
};
/**
* Function: setX
*
* Set the value of x for the specified layer
*/
mxGraphAbstractHierarchyCell.prototype.setX = function(layer, value)
{
if (this.isVertex())
{
this.x[0] = value;
}
else if (this.isEdge())
{
this.x[layer - this.minRank - 1] = value;
}
};
/**
* Function: getX
*
* Gets the value of x on the specified layer
*/
mxGraphAbstractHierarchyCell.prototype.getX = function(layer)
{
if (this.isVertex())
{
return this.x[0];
}
else if (this.isEdge())
{
return this.x[layer - this.minRank - 1];
}
return 0.0;
};
/**
* Function: setY
*
* Set the value of y for the specified layer
*/
mxGraphAbstractHierarchyCell.prototype.setY = function(layer, value)
{
if (this.isVertex())
{
this.y[0] = value;
}
else if (this.isEdge())
{
this.y[layer -this. minRank - 1] = value;
}
};

View File

@ -0,0 +1,187 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxGraphHierarchyEdge
*
* An abstraction of a hierarchical edge for the hierarchy layout
*
* Constructor: mxGraphHierarchyEdge
*
* Constructs a hierarchy edge
*
* Arguments:
*
* edges - a list of real graph edges this abstraction represents
*/
function mxGraphHierarchyEdge(edges)
{
mxGraphAbstractHierarchyCell.apply(this, arguments);
this.edges = edges;
this.ids = [];
for (var i = 0; i < edges.length; i++)
{
this.ids.push(mxObjectIdentity.get(edges[i]));
}
};
/**
* Extends mxGraphAbstractHierarchyCell.
*/
mxGraphHierarchyEdge.prototype = new mxGraphAbstractHierarchyCell();
mxGraphHierarchyEdge.prototype.constructor = mxGraphHierarchyEdge;
/**
* Variable: edges
*
* The graph edge(s) this object represents. Parallel edges are all grouped
* together within one hierarchy edge.
*/
mxGraphHierarchyEdge.prototype.edges = null;
/**
* Variable: ids
*
* The object identities of the wrapped cells
*/
mxGraphHierarchyEdge.prototype.ids = null;
/**
* Variable: source
*
* The node this edge is sourced at
*/
mxGraphHierarchyEdge.prototype.source = null;
/**
* Variable: target
*
* The node this edge targets
*/
mxGraphHierarchyEdge.prototype.target = null;
/**
* Variable: isReversed
*
* Whether or not the direction of this edge has been reversed
* internally to create a DAG for the hierarchical layout
*/
mxGraphHierarchyEdge.prototype.isReversed = false;
/**
* Function: invert
*
* Inverts the direction of this internal edge(s)
*/
mxGraphHierarchyEdge.prototype.invert = function(layer)
{
var temp = this.source;
this.source = this.target;
this.target = temp;
this.isReversed = !this.isReversed;
};
/**
* Function: getNextLayerConnectedCells
*
* Returns the cells this cell connects to on the next layer up
*/
mxGraphHierarchyEdge.prototype.getNextLayerConnectedCells = function(layer)
{
if (this.nextLayerConnectedCells == null)
{
this.nextLayerConnectedCells = [];
for (var i = 0; i < this.temp.length; i++)
{
this.nextLayerConnectedCells[i] = [];
if (i == this.temp.length - 1)
{
this.nextLayerConnectedCells[i].push(this.source);
}
else
{
this.nextLayerConnectedCells[i].push(this);
}
}
}
return this.nextLayerConnectedCells[layer - this.minRank - 1];
};
/**
* Function: getPreviousLayerConnectedCells
*
* Returns the cells this cell connects to on the next layer down
*/
mxGraphHierarchyEdge.prototype.getPreviousLayerConnectedCells = function(layer)
{
if (this.previousLayerConnectedCells == null)
{
this.previousLayerConnectedCells = [];
for (var i = 0; i < this.temp.length; i++)
{
this.previousLayerConnectedCells[i] = [];
if (i == 0)
{
this.previousLayerConnectedCells[i].push(this.target);
}
else
{
this.previousLayerConnectedCells[i].push(this);
}
}
}
return this.previousLayerConnectedCells[layer - this.minRank - 1];
};
/**
* Function: isEdge
*
* Returns true.
*/
mxGraphHierarchyEdge.prototype.isEdge = function()
{
return true;
};
/**
* Function: getGeneralPurposeVariable
*
* Gets the value of temp for the specified layer
*/
mxGraphHierarchyEdge.prototype.getGeneralPurposeVariable = function(layer)
{
return this.temp[layer - this.minRank - 1];
};
/**
* Function: setGeneralPurposeVariable
*
* Set the value of temp for the specified layer
*/
mxGraphHierarchyEdge.prototype.setGeneralPurposeVariable = function(layer, value)
{
this.temp[layer - this.minRank - 1] = value;
};
/**
* Function: getCoreCell
*
* Gets the first core edge associated with this wrapper
*/
mxGraphHierarchyEdge.prototype.getCoreCell = function()
{
if (this.edges != null && this.edges.length > 0)
{
return this.edges[0];
}
return null;
};

View File

@ -0,0 +1,681 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxGraphHierarchyModel
*
* Internal model of a hierarchical graph. This model stores nodes and edges
* equivalent to the real graph nodes and edges, but also stores the rank of the
* cells, the order within the ranks and the new candidate locations of cells.
* The internal model also reverses edge direction were appropriate , ignores
* self-loop and groups parallels together under one edge object.
*
* Constructor: mxGraphHierarchyModel
*
* Creates an internal ordered graph model using the vertices passed in. If
* there are any, leftward edge need to be inverted in the internal model
*
* Arguments:
*
* graph - the facade describing the graph to be operated on
* vertices - the vertices for this hierarchy
* ordered - whether or not the vertices are already ordered
* deterministic - whether or not this layout should be deterministic on each
* tightenToSource - whether or not to tighten vertices towards the sources
* scanRanksFromSinks - Whether rank assignment is from the sinks or sources.
* usage
*/
function mxGraphHierarchyModel(layout, vertices, roots, parent, tightenToSource)
{
var graph = layout.getGraph();
this.tightenToSource = tightenToSource;
this.roots = roots;
this.parent = parent;
// map of cells to internal cell needed for second run through
// to setup the sink of edges correctly
this.vertexMapper = new mxDictionary();
this.edgeMapper = new mxDictionary();
this.maxRank = 0;
var internalVertices = [];
if (vertices == null)
{
vertices = this.graph.getChildVertices(parent);
}
this.maxRank = this.SOURCESCANSTARTRANK;
// map of cells to internal cell needed for second run through
// to setup the sink of edges correctly. Guess size by number
// of edges is roughly same as number of vertices.
this.createInternalCells(layout, vertices, internalVertices);
// Go through edges set their sink values. Also check the
// ordering if and invert edges if necessary
for (var i = 0; i < vertices.length; i++)
{
var edges = internalVertices[i].connectsAsSource;
for (var j = 0; j < edges.length; j++)
{
var internalEdge = edges[j];
var realEdges = internalEdge.edges;
// Only need to process the first real edge, since
// all the edges connect to the same other vertex
if (realEdges != null && realEdges.length > 0)
{
var realEdge = realEdges[0];
var targetCell = layout.getVisibleTerminal(
realEdge, false);
var internalTargetCell = this.vertexMapper.get(targetCell);
if (internalVertices[i] == internalTargetCell)
{
// If there are parallel edges going between two vertices and not all are in the same direction
// you can have navigated across one direction when doing the cycle reversal that isn't the same
// direction as the first real edge in the array above. When that happens the if above catches
// that and we correct the target cell before continuing.
// This branch only detects this single case
targetCell = layout.getVisibleTerminal(
realEdge, true);
internalTargetCell = this.vertexMapper.get(targetCell);
}
if (internalTargetCell != null
&& internalVertices[i] != internalTargetCell)
{
internalEdge.target = internalTargetCell;
if (internalTargetCell.connectsAsTarget.length == 0)
{
internalTargetCell.connectsAsTarget = [];
}
if (mxUtils.indexOf(internalTargetCell.connectsAsTarget, internalEdge) < 0)
{
internalTargetCell.connectsAsTarget.push(internalEdge);
}
}
}
}
// Use the temp variable in the internal nodes to mark this
// internal vertex as having been visited.
internalVertices[i].temp[0] = 1;
}
};
/**
* Variable: maxRank
*
* Stores the largest rank number allocated
*/
mxGraphHierarchyModel.prototype.maxRank = null;
/**
* Variable: vertexMapper
*
* Map from graph vertices to internal model nodes.
*/
mxGraphHierarchyModel.prototype.vertexMapper = null;
/**
* Variable: edgeMapper
*
* Map from graph edges to internal model edges
*/
mxGraphHierarchyModel.prototype.edgeMapper = null;
/**
* Variable: ranks
*
* Mapping from rank number to actual rank
*/
mxGraphHierarchyModel.prototype.ranks = null;
/**
* Variable: roots
*
* Store of roots of this hierarchy model, these are real graph cells, not
* internal cells
*/
mxGraphHierarchyModel.prototype.roots = null;
/**
* Variable: parent
*
* The parent cell whose children are being laid out
*/
mxGraphHierarchyModel.prototype.parent = null;
/**
* Variable: dfsCount
*
* Count of the number of times the ancestor dfs has been used.
*/
mxGraphHierarchyModel.prototype.dfsCount = 0;
/**
* Variable: SOURCESCANSTARTRANK
*
* High value to start source layering scan rank value from.
*/
mxGraphHierarchyModel.prototype.SOURCESCANSTARTRANK = 100000000;
/**
* Variable: tightenToSource
*
* Whether or not to tighten the assigned ranks of vertices up towards
* the source cells.
*/
mxGraphHierarchyModel.prototype.tightenToSource = false;
/**
* Function: createInternalCells
*
* Creates all edges in the internal model
*
* Parameters:
*
* layout - Reference to the <mxHierarchicalLayout> algorithm.
* vertices - Array of <mxCells> that represent the vertices whom are to
* have an internal representation created.
* internalVertices - The array of <mxGraphHierarchyNodes> to have their
* information filled in using the real vertices.
*/
mxGraphHierarchyModel.prototype.createInternalCells = function(layout, vertices, internalVertices)
{
var graph = layout.getGraph();
// Create internal edges
for (var i = 0; i < vertices.length; i++)
{
internalVertices[i] = new mxGraphHierarchyNode(vertices[i]);
this.vertexMapper.put(vertices[i], internalVertices[i]);
// If the layout is deterministic, order the cells
//List outgoingCells = graph.getNeighbours(vertices[i], deterministic);
var conns = layout.getEdges(vertices[i]);
internalVertices[i].connectsAsSource = [];
// Create internal edges, but don't do any rank assignment yet
// First use the information from the greedy cycle remover to
// invert the leftward edges internally
for (var j = 0; j < conns.length; j++)
{
var cell = layout.getVisibleTerminal(conns[j], false);
// Looking for outgoing edges only
if (cell != vertices[i] && layout.graph.model.isVertex(cell) &&
!layout.isVertexIgnored(cell))
{
// We process all edge between this source and its targets
// If there are edges going both ways, we need to collect
// them all into one internal edges to avoid looping problems
// later. We assume this direction (source -> target) is the
// natural direction if at least half the edges are going in
// that direction.
// The check below for edges[0] being in the vertex mapper is
// in case we've processed this the other way around
// (target -> source) and the number of edges in each direction
// are the same. All the graph edges will have been assigned to
// an internal edge going the other way, so we don't want to
// process them again
var undirectedEdges = layout.getEdgesBetween(vertices[i],
cell, false);
var directedEdges = layout.getEdgesBetween(vertices[i],
cell, true);
if (undirectedEdges != null &&
undirectedEdges.length > 0 &&
this.edgeMapper.get(undirectedEdges[0]) == null &&
directedEdges.length * 2 >= undirectedEdges.length)
{
var internalEdge = new mxGraphHierarchyEdge(undirectedEdges);
for (var k = 0; k < undirectedEdges.length; k++)
{
var edge = undirectedEdges[k];
this.edgeMapper.put(edge, internalEdge);
// Resets all point on the edge and disables the edge style
// without deleting it from the cell style
graph.resetEdge(edge);
if (layout.disableEdgeStyle)
{
layout.setEdgeStyleEnabled(edge, false);
layout.setOrthogonalEdge(edge,true);
}
}
internalEdge.source = internalVertices[i];
if (mxUtils.indexOf(internalVertices[i].connectsAsSource, internalEdge) < 0)
{
internalVertices[i].connectsAsSource.push(internalEdge);
}
}
}
}
// Ensure temp variable is cleared from any previous use
internalVertices[i].temp[0] = 0;
}
};
/**
* Function: initialRank
*
* Basic determination of minimum layer ranking by working from from sources
* or sinks and working through each node in the relevant edge direction.
* Starting at the sinks is basically a longest path layering algorithm.
*/
mxGraphHierarchyModel.prototype.initialRank = function()
{
var startNodes = [];
if (this.roots != null)
{
for (var i = 0; i < this.roots.length; i++)
{
var internalNode = this.vertexMapper.get(this.roots[i]);
if (internalNode != null)
{
startNodes.push(internalNode);
}
}
}
var internalNodes = this.vertexMapper.getValues();
for (var i=0; i < internalNodes.length; i++)
{
// Mark the node as not having had a layer assigned
internalNodes[i].temp[0] = -1;
}
var startNodesCopy = startNodes.slice();
while (startNodes.length > 0)
{
var internalNode = startNodes[0];
var layerDeterminingEdges;
var edgesToBeMarked;
layerDeterminingEdges = internalNode.connectsAsTarget;
edgesToBeMarked = internalNode.connectsAsSource;
// flag to keep track of whether or not all layer determining
// edges have been scanned
var allEdgesScanned = true;
// Work out the layer of this node from the layer determining
// edges. The minimum layer number of any node connected by one of
// the layer determining edges variable
var minimumLayer = this.SOURCESCANSTARTRANK;
for (var i = 0; i < layerDeterminingEdges.length; i++)
{
var internalEdge = layerDeterminingEdges[i];
if (internalEdge.temp[0] == 5270620)
{
// This edge has been scanned, get the layer of the
// node on the other end
var otherNode = internalEdge.source;
minimumLayer = Math.min(minimumLayer, otherNode.temp[0] - 1);
}
else
{
allEdgesScanned = false;
break;
}
}
// If all edge have been scanned, assign the layer, mark all
// edges in the other direction and remove from the nodes list
if (allEdgesScanned)
{
internalNode.temp[0] = minimumLayer;
this.maxRank = Math.min(this.maxRank, minimumLayer);
if (edgesToBeMarked != null)
{
for (var i = 0; i < edgesToBeMarked.length; i++)
{
var internalEdge = edgesToBeMarked[i];
// Assign unique stamp ( y/m/d/h )
internalEdge.temp[0] = 5270620;
// Add node on other end of edge to LinkedList of
// nodes to be analysed
var otherNode = internalEdge.target;
// Only add node if it hasn't been assigned a layer
if (otherNode.temp[0] == -1)
{
startNodes.push(otherNode);
// Mark this other node as neither being
// unassigned nor assigned so it isn't
// added to this list again, but it's
// layer isn't used in any calculation.
otherNode.temp[0] = -2;
}
}
}
startNodes.shift();
}
else
{
// Not all the edges have been scanned, get to the back of
// the class and put the dunces cap on
var removedCell = startNodes.shift();
startNodes.push(internalNode);
if (removedCell == internalNode && startNodes.length == 1)
{
// This is an error condition, we can't get out of
// this loop. It could happen for more than one node
// but that's a lot harder to detect. Log the error
// TODO make log comment
break;
}
}
}
// Normalize the ranks down from their large starting value to place
// at least 1 sink on layer 0
for (var i=0; i < internalNodes.length; i++)
{
// Mark the node as not having had a layer assigned
internalNodes[i].temp[0] -= this.maxRank;
}
// Tighten the rank 0 nodes as far as possible
for ( var i = 0; i < startNodesCopy.length; i++)
{
var internalNode = startNodesCopy[i];
var currentMaxLayer = 0;
var layerDeterminingEdges = internalNode.connectsAsSource;
for ( var j = 0; j < layerDeterminingEdges.length; j++)
{
var internalEdge = layerDeterminingEdges[j];
var otherNode = internalEdge.target;
internalNode.temp[0] = Math.max(currentMaxLayer,
otherNode.temp[0] + 1);
currentMaxLayer = internalNode.temp[0];
}
}
// Reset the maxRank to that which would be expected for a from-sink
// scan
this.maxRank = this.SOURCESCANSTARTRANK - this.maxRank;
};
/**
* Function: fixRanks
*
* Fixes the layer assignments to the values stored in the nodes. Also needs
* to create dummy nodes for edges that cross layers.
*/
mxGraphHierarchyModel.prototype.fixRanks = function()
{
var rankList = [];
this.ranks = [];
for (var i = 0; i < this.maxRank + 1; i++)
{
rankList[i] = [];
this.ranks[i] = rankList[i];
}
// Perform a DFS to obtain an initial ordering for each rank.
// Without doing this you would end up having to process
// crossings for a standard tree.
var rootsArray = null;
if (this.roots != null)
{
var oldRootsArray = this.roots;
rootsArray = [];
for (var i = 0; i < oldRootsArray.length; i++)
{
var cell = oldRootsArray[i];
var internalNode = this.vertexMapper.get(cell);
rootsArray[i] = internalNode;
}
}
this.visit(function(parent, node, edge, layer, seen)
{
if (seen == 0 && node.maxRank < 0 && node.minRank < 0)
{
rankList[node.temp[0]].push(node);
node.maxRank = node.temp[0];
node.minRank = node.temp[0];
// Set temp[0] to the nodes position in the rank
node.temp[0] = rankList[node.maxRank].length - 1;
}
if (parent != null && edge != null)
{
var parentToCellRankDifference = parent.maxRank - node.maxRank;
if (parentToCellRankDifference > 1)
{
// There are ranks in between the parent and current cell
edge.maxRank = parent.maxRank;
edge.minRank = node.maxRank;
edge.temp = [];
edge.x = [];
edge.y = [];
for (var i = edge.minRank + 1; i < edge.maxRank; i++)
{
// The connecting edge must be added to the
// appropriate ranks
rankList[i].push(edge);
edge.setGeneralPurposeVariable(i, rankList[i]
.length - 1);
}
}
}
}, rootsArray, false, null);
};
/**
* Function: visit
*
* A depth first search through the internal heirarchy model.
*
* Parameters:
*
* visitor - The visitor function pattern to be called for each node.
* trackAncestors - Whether or not the search is to keep track all nodes
* directly above this one in the search path.
*/
mxGraphHierarchyModel.prototype.visit = function(visitor, dfsRoots, trackAncestors, seenNodes)
{
// Run dfs through on all roots
if (dfsRoots != null)
{
for (var i = 0; i < dfsRoots.length; i++)
{
var internalNode = dfsRoots[i];
if (internalNode != null)
{
if (seenNodes == null)
{
seenNodes = new Object();
}
if (trackAncestors)
{
// Set up hash code for root
internalNode.hashCode = [];
internalNode.hashCode[0] = this.dfsCount;
internalNode.hashCode[1] = i;
this.extendedDfs(null, internalNode, null, visitor, seenNodes,
internalNode.hashCode, i, 0);
}
else
{
this.dfs(null, internalNode, null, visitor, seenNodes, 0);
}
}
}
this.dfsCount++;
}
};
/**
* Function: dfs
*
* Performs a depth first search on the internal hierarchy model
*
* Parameters:
*
* parent - the parent internal node of the current internal node
* root - the current internal node
* connectingEdge - the internal edge connecting the internal node and the parent
* internal node, if any
* visitor - the visitor pattern to be called for each node
* seen - a set of all nodes seen by this dfs a set of all of the
* ancestor node of the current node
* layer - the layer on the dfs tree ( not the same as the model ranks )
*/
mxGraphHierarchyModel.prototype.dfs = function(parent, root, connectingEdge, visitor, seen, layer)
{
if (root != null)
{
var rootId = root.id;
if (seen[rootId] == null)
{
seen[rootId] = root;
visitor(parent, root, connectingEdge, layer, 0);
// Copy the connects as source list so that visitors
// can change the original for edge direction inversions
var outgoingEdges = root.connectsAsSource.slice();
for (var i = 0; i< outgoingEdges.length; i++)
{
var internalEdge = outgoingEdges[i];
var targetNode = internalEdge.target;
// Root check is O(|roots|)
this.dfs(root, targetNode, internalEdge, visitor, seen,
layer + 1);
}
}
else
{
// Use the int field to indicate this node has been seen
visitor(parent, root, connectingEdge, layer, 1);
}
}
};
/**
* Function: extendedDfs
*
* Performs a depth first search on the internal hierarchy model. This dfs
* extends the default version by keeping track of cells ancestors, but it
* should be only used when necessary because of it can be computationally
* intensive for deep searches.
*
* Parameters:
*
* parent - the parent internal node of the current internal node
* root - the current internal node
* connectingEdge - the internal edge connecting the internal node and the parent
* internal node, if any
* visitor - the visitor pattern to be called for each node
* seen - a set of all nodes seen by this dfs
* ancestors - the parent hash code
* childHash - the new hash code for this node
* layer - the layer on the dfs tree ( not the same as the model ranks )
*/
mxGraphHierarchyModel.prototype.extendedDfs = function(parent, root, connectingEdge, visitor, seen, ancestors, childHash, layer)
{
// Explanation of custom hash set. Previously, the ancestors variable
// was passed through the dfs as a HashSet. The ancestors were copied
// into a new HashSet and when the new child was processed it was also
// added to the set. If the current node was in its ancestor list it
// meant there is a cycle in the graph and this information is passed
// to the visitor.visit() in the seen parameter. The HashSet clone was
// very expensive on CPU so a custom hash was developed using primitive
// types. temp[] couldn't be used so hashCode[] was added to each node.
// Each new child adds another int to the array, copying the prefix
// from its parent. Child of the same parent add different ints (the
// limit is therefore 2^32 children per parent...). If a node has a
// child with the hashCode already set then the child code is compared
// to the same portion of the current nodes array. If they match there
// is a loop.
// Note that the basic mechanism would only allow for 1 use of this
// functionality, so the root nodes have two ints. The second int is
// incremented through each node root and the first is incremented
// through each run of the dfs algorithm (therefore the dfs is not
// thread safe). The hash code of each node is set if not already set,
// or if the first int does not match that of the current run.
if (root != null)
{
if (parent != null)
{
// Form this nodes hash code if necessary, that is, if the
// hashCode variable has not been initialized or if the
// start of the parent hash code does not equal the start of
// this nodes hash code, indicating the code was set on a
// previous run of this dfs.
if (root.hashCode == null ||
root.hashCode[0] != parent.hashCode[0])
{
var hashCodeLength = parent.hashCode.length + 1;
root.hashCode = parent.hashCode.slice();
root.hashCode[hashCodeLength - 1] = childHash;
}
}
var rootId = root.id;
if (seen[rootId] == null)
{
seen[rootId] = root;
visitor(parent, root, connectingEdge, layer, 0);
// Copy the connects as source list so that visitors
// can change the original for edge direction inversions
var outgoingEdges = root.connectsAsSource.slice();
for (var i = 0; i < outgoingEdges.length; i++)
{
var internalEdge = outgoingEdges[i];
var targetNode = internalEdge.target;
// Root check is O(|roots|)
this.extendedDfs(root, targetNode, internalEdge, visitor, seen,
root.hashCode, i, layer + 1);
}
}
else
{
// Use the int field to indicate this node has been seen
visitor(parent, root, connectingEdge, layer, 1);
}
}
};

View File

@ -0,0 +1,220 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxGraphHierarchyNode
*
* An abstraction of a hierarchical edge for the hierarchy layout
*
* Constructor: mxGraphHierarchyNode
*
* Constructs an internal node to represent the specified real graph cell
*
* Arguments:
*
* cell - the real graph cell this node represents
*/
function mxGraphHierarchyNode(cell)
{
mxGraphAbstractHierarchyCell.apply(this, arguments);
this.cell = cell;
this.id = mxObjectIdentity.get(cell);
this.connectsAsTarget = [];
this.connectsAsSource = [];
};
/**
* Extends mxGraphAbstractHierarchyCell.
*/
mxGraphHierarchyNode.prototype = new mxGraphAbstractHierarchyCell();
mxGraphHierarchyNode.prototype.constructor = mxGraphHierarchyNode;
/**
* Variable: cell
*
* The graph cell this object represents.
*/
mxGraphHierarchyNode.prototype.cell = null;
/**
* Variable: id
*
* The object identity of the wrapped cell
*/
mxGraphHierarchyNode.prototype.id = null;
/**
* Variable: connectsAsTarget
*
* Collection of hierarchy edges that have this node as a target
*/
mxGraphHierarchyNode.prototype.connectsAsTarget = null;
/**
* Variable: connectsAsSource
*
* Collection of hierarchy edges that have this node as a source
*/
mxGraphHierarchyNode.prototype.connectsAsSource = null;
/**
* Variable: hashCode
*
* Assigns a unique hashcode for each node. Used by the model dfs instead
* of copying HashSets
*/
mxGraphHierarchyNode.prototype.hashCode = false;
/**
* Function: getRankValue
*
* Returns the integer value of the layer that this node resides in
*/
mxGraphHierarchyNode.prototype.getRankValue = function(layer)
{
return this.maxRank;
};
/**
* Function: getNextLayerConnectedCells
*
* Returns the cells this cell connects to on the next layer up
*/
mxGraphHierarchyNode.prototype.getNextLayerConnectedCells = function(layer)
{
if (this.nextLayerConnectedCells == null)
{
this.nextLayerConnectedCells = [];
this.nextLayerConnectedCells[0] = [];
for (var i = 0; i < this.connectsAsTarget.length; i++)
{
var edge = this.connectsAsTarget[i];
if (edge.maxRank == -1 || edge.maxRank == layer + 1)
{
// Either edge is not in any rank or
// no dummy nodes in edge, add node of other side of edge
this.nextLayerConnectedCells[0].push(edge.source);
}
else
{
// Edge spans at least two layers, add edge
this.nextLayerConnectedCells[0].push(edge);
}
}
}
return this.nextLayerConnectedCells[0];
};
/**
* Function: getPreviousLayerConnectedCells
*
* Returns the cells this cell connects to on the next layer down
*/
mxGraphHierarchyNode.prototype.getPreviousLayerConnectedCells = function(layer)
{
if (this.previousLayerConnectedCells == null)
{
this.previousLayerConnectedCells = [];
this.previousLayerConnectedCells[0] = [];
for (var i = 0; i < this.connectsAsSource.length; i++)
{
var edge = this.connectsAsSource[i];
if (edge.minRank == -1 || edge.minRank == layer - 1)
{
// No dummy nodes in edge, add node of other side of edge
this.previousLayerConnectedCells[0].push(edge.target);
}
else
{
// Edge spans at least two layers, add edge
this.previousLayerConnectedCells[0].push(edge);
}
}
}
return this.previousLayerConnectedCells[0];
};
/**
* Function: isVertex
*
* Returns true.
*/
mxGraphHierarchyNode.prototype.isVertex = function()
{
return true;
};
/**
* Function: getGeneralPurposeVariable
*
* Gets the value of temp for the specified layer
*/
mxGraphHierarchyNode.prototype.getGeneralPurposeVariable = function(layer)
{
return this.temp[0];
};
/**
* Function: setGeneralPurposeVariable
*
* Set the value of temp for the specified layer
*/
mxGraphHierarchyNode.prototype.setGeneralPurposeVariable = function(layer, value)
{
this.temp[0] = value;
};
/**
* Function: isAncestor
*/
mxGraphHierarchyNode.prototype.isAncestor = function(otherNode)
{
// Firstly, the hash code of this node needs to be shorter than the
// other node
if (otherNode != null && this.hashCode != null && otherNode.hashCode != null
&& this.hashCode.length < otherNode.hashCode.length)
{
if (this.hashCode == otherNode.hashCode)
{
return true;
}
if (this.hashCode == null || this.hashCode == null)
{
return false;
}
// Secondly, this hash code must match the start of the other
// node's hash code. Arrays.equals cannot be used here since
// the arrays are different length, and we do not want to
// perform another array copy.
for (var i = 0; i < this.hashCode.length; i++)
{
if (this.hashCode[i] != otherNode.hashCode[i])
{
return false;
}
}
return true;
}
return false;
};
/**
* Function: getCoreCell
*
* Gets the core vertex associated with this wrapper
*/
mxGraphHierarchyNode.prototype.getCoreCell = function()
{
return this.cell;
};

View File

@ -0,0 +1,801 @@
/**
* Copyright (c) 2006-2018, JGraph Ltd
* Copyright (c) 2006-2018, Gaudenz Alder
*/
/**
* Class: mxSwimlaneModel
*
* Internal model of a hierarchical graph. This model stores nodes and edges
* equivalent to the real graph nodes and edges, but also stores the rank of the
* cells, the order within the ranks and the new candidate locations of cells.
* The internal model also reverses edge direction were appropriate , ignores
* self-loop and groups parallels together under one edge object.
*
* Constructor: mxSwimlaneModel
*
* Creates an internal ordered graph model using the vertices passed in. If
* there are any, leftward edge need to be inverted in the internal model
*
* Arguments:
*
* graph - the facade describing the graph to be operated on
* vertices - the vertices for this hierarchy
* ordered - whether or not the vertices are already ordered
* deterministic - whether or not this layout should be deterministic on each
* tightenToSource - whether or not to tighten vertices towards the sources
* scanRanksFromSinks - Whether rank assignment is from the sinks or sources.
* usage
*/
function mxSwimlaneModel(layout, vertices, roots, parent, tightenToSource)
{
var graph = layout.getGraph();
this.tightenToSource = tightenToSource;
this.roots = roots;
this.parent = parent;
// map of cells to internal cell needed for second run through
// to setup the sink of edges correctly
this.vertexMapper = new mxDictionary();
this.edgeMapper = new mxDictionary();
this.maxRank = 0;
var internalVertices = [];
if (vertices == null)
{
vertices = this.graph.getChildVertices(parent);
}
this.maxRank = this.SOURCESCANSTARTRANK;
// map of cells to internal cell needed for second run through
// to setup the sink of edges correctly. Guess size by number
// of edges is roughly same as number of vertices.
this.createInternalCells(layout, vertices, internalVertices);
// Go through edges set their sink values. Also check the
// ordering if and invert edges if necessary
for (var i = 0; i < vertices.length; i++)
{
var edges = internalVertices[i].connectsAsSource;
for (var j = 0; j < edges.length; j++)
{
var internalEdge = edges[j];
var realEdges = internalEdge.edges;
// Only need to process the first real edge, since
// all the edges connect to the same other vertex
if (realEdges != null && realEdges.length > 0)
{
var realEdge = realEdges[0];
var targetCell = layout.getVisibleTerminal(
realEdge, false);
var internalTargetCell = this.vertexMapper.get(targetCell);
if (internalVertices[i] == internalTargetCell)
{
// If there are parallel edges going between two vertices and not all are in the same direction
// you can have navigated across one direction when doing the cycle reversal that isn't the same
// direction as the first real edge in the array above. When that happens the if above catches
// that and we correct the target cell before continuing.
// This branch only detects this single case
targetCell = layout.getVisibleTerminal(
realEdge, true);
internalTargetCell = this.vertexMapper.get(targetCell);
}
if (internalTargetCell != null
&& internalVertices[i] != internalTargetCell)
{
internalEdge.target = internalTargetCell;
if (internalTargetCell.connectsAsTarget.length == 0)
{
internalTargetCell.connectsAsTarget = [];
}
if (mxUtils.indexOf(internalTargetCell.connectsAsTarget, internalEdge) < 0)
{
internalTargetCell.connectsAsTarget.push(internalEdge);
}
}
}
}
// Use the temp variable in the internal nodes to mark this
// internal vertex as having been visited.
internalVertices[i].temp[0] = 1;
}
};
/**
* Variable: maxRank
*
* Stores the largest rank number allocated
*/
mxSwimlaneModel.prototype.maxRank = null;
/**
* Variable: vertexMapper
*
* Map from graph vertices to internal model nodes.
*/
mxSwimlaneModel.prototype.vertexMapper = null;
/**
* Variable: edgeMapper
*
* Map from graph edges to internal model edges
*/
mxSwimlaneModel.prototype.edgeMapper = null;
/**
* Variable: ranks
*
* Mapping from rank number to actual rank
*/
mxSwimlaneModel.prototype.ranks = null;
/**
* Variable: roots
*
* Store of roots of this hierarchy model, these are real graph cells, not
* internal cells
*/
mxSwimlaneModel.prototype.roots = null;
/**
* Variable: parent
*
* The parent cell whose children are being laid out
*/
mxSwimlaneModel.prototype.parent = null;
/**
* Variable: dfsCount
*
* Count of the number of times the ancestor dfs has been used.
*/
mxSwimlaneModel.prototype.dfsCount = 0;
/**
* Variable: SOURCESCANSTARTRANK
*
* High value to start source layering scan rank value from.
*/
mxSwimlaneModel.prototype.SOURCESCANSTARTRANK = 100000000;
/**
* Variable: tightenToSource
*
* Whether or not to tighten the assigned ranks of vertices up towards
* the source cells.
*/
mxSwimlaneModel.prototype.tightenToSource = false;
/**
* Variable: ranksPerGroup
*
* An array of the number of ranks within each swimlane
*/
mxSwimlaneModel.prototype.ranksPerGroup = null;
/**
* Function: createInternalCells
*
* Creates all edges in the internal model
*
* Parameters:
*
* layout - Reference to the <mxHierarchicalLayout> algorithm.
* vertices - Array of <mxCells> that represent the vertices whom are to
* have an internal representation created.
* internalVertices - The array of <mxGraphHierarchyNodes> to have their
* information filled in using the real vertices.
*/
mxSwimlaneModel.prototype.createInternalCells = function(layout, vertices, internalVertices)
{
var graph = layout.getGraph();
var swimlanes = layout.swimlanes;
// Create internal edges
for (var i = 0; i < vertices.length; i++)
{
internalVertices[i] = new mxGraphHierarchyNode(vertices[i]);
this.vertexMapper.put(vertices[i], internalVertices[i]);
internalVertices[i].swimlaneIndex = -1;
for (var ii = 0; ii < swimlanes.length; ii++)
{
if (graph.model.getParent(vertices[i]) == swimlanes[ii])
{
internalVertices[i].swimlaneIndex = ii;
break;
}
}
// If the layout is deterministic, order the cells
//List outgoingCells = graph.getNeighbours(vertices[i], deterministic);
var conns = layout.getEdges(vertices[i]);
internalVertices[i].connectsAsSource = [];
// Create internal edges, but don't do any rank assignment yet
// First use the information from the greedy cycle remover to
// invert the leftward edges internally
for (var j = 0; j < conns.length; j++)
{
var cell = layout.getVisibleTerminal(conns[j], false);
// Looking for outgoing edges only
if (cell != vertices[i] && layout.graph.model.isVertex(cell) &&
!layout.isVertexIgnored(cell))
{
// We process all edge between this source and its targets
// If there are edges going both ways, we need to collect
// them all into one internal edges to avoid looping problems
// later. We assume this direction (source -> target) is the
// natural direction if at least half the edges are going in
// that direction.
// The check below for edges[0] being in the vertex mapper is
// in case we've processed this the other way around
// (target -> source) and the number of edges in each direction
// are the same. All the graph edges will have been assigned to
// an internal edge going the other way, so we don't want to
// process them again
var undirectedEdges = layout.getEdgesBetween(vertices[i],
cell, false);
var directedEdges = layout.getEdgesBetween(vertices[i],
cell, true);
if (undirectedEdges != null &&
undirectedEdges.length > 0 &&
this.edgeMapper.get(undirectedEdges[0]) == null &&
directedEdges.length * 2 >= undirectedEdges.length)
{
var internalEdge = new mxGraphHierarchyEdge(undirectedEdges);
for (var k = 0; k < undirectedEdges.length; k++)
{
var edge = undirectedEdges[k];
this.edgeMapper.put(edge, internalEdge);
// Resets all point on the edge and disables the edge style
// without deleting it from the cell style
graph.resetEdge(edge);
if (layout.disableEdgeStyle)
{
layout.setEdgeStyleEnabled(edge, false);
layout.setOrthogonalEdge(edge,true);
}
}
internalEdge.source = internalVertices[i];
if (mxUtils.indexOf(internalVertices[i].connectsAsSource, internalEdge) < 0)
{
internalVertices[i].connectsAsSource.push(internalEdge);
}
}
}
}
// Ensure temp variable is cleared from any previous use
internalVertices[i].temp[0] = 0;
}
};
/**
* Function: initialRank
*
* Basic determination of minimum layer ranking by working from from sources
* or sinks and working through each node in the relevant edge direction.
* Starting at the sinks is basically a longest path layering algorithm.
*/
mxSwimlaneModel.prototype.initialRank = function()
{
this.ranksPerGroup = [];
var startNodes = [];
var seen = new Object();
if (this.roots != null)
{
for (var i = 0; i < this.roots.length; i++)
{
var internalNode = this.vertexMapper.get(this.roots[i]);
this.maxChainDfs(null, internalNode, null, seen, 0);
if (internalNode != null)
{
startNodes.push(internalNode);
}
}
}
// Calculate the lower and upper rank bounds of each swimlane
var lowerRank = [];
var upperRank = [];
for (var i = this.ranksPerGroup.length - 1; i >= 0; i--)
{
if (i == this.ranksPerGroup.length - 1)
{
lowerRank[i] = 0;
}
else
{
lowerRank[i] = upperRank[i+1] + 1;
}
upperRank[i] = lowerRank[i] + this.ranksPerGroup[i];
}
this.maxRank = upperRank[0];
var internalNodes = this.vertexMapper.getValues();
for (var i=0; i < internalNodes.length; i++)
{
// Mark the node as not having had a layer assigned
internalNodes[i].temp[0] = -1;
}
var startNodesCopy = startNodes.slice();
while (startNodes.length > 0)
{
var internalNode = startNodes[0];
var layerDeterminingEdges;
var edgesToBeMarked;
layerDeterminingEdges = internalNode.connectsAsTarget;
edgesToBeMarked = internalNode.connectsAsSource;
// flag to keep track of whether or not all layer determining
// edges have been scanned
var allEdgesScanned = true;
// Work out the layer of this node from the layer determining
// edges. The minimum layer number of any node connected by one of
// the layer determining edges variable
var minimumLayer = upperRank[0];
for (var i = 0; i < layerDeterminingEdges.length; i++)
{
var internalEdge = layerDeterminingEdges[i];
if (internalEdge.temp[0] == 5270620)
{
// This edge has been scanned, get the layer of the
// node on the other end
var otherNode = internalEdge.source;
minimumLayer = Math.min(minimumLayer, otherNode.temp[0] - 1);
}
else
{
allEdgesScanned = false;
break;
}
}
// If all edge have been scanned, assign the layer, mark all
// edges in the other direction and remove from the nodes list
if (allEdgesScanned)
{
if (minimumLayer > upperRank[internalNode.swimlaneIndex])
{
minimumLayer = upperRank[internalNode.swimlaneIndex];
}
internalNode.temp[0] = minimumLayer;
if (edgesToBeMarked != null)
{
for (var i = 0; i < edgesToBeMarked.length; i++)
{
var internalEdge = edgesToBeMarked[i];
// Assign unique stamp ( y/m/d/h )
internalEdge.temp[0] = 5270620;
// Add node on other end of edge to LinkedList of
// nodes to be analysed
var otherNode = internalEdge.target;
// Only add node if it hasn't been assigned a layer
if (otherNode.temp[0] == -1)
{
startNodes.push(otherNode);
// Mark this other node as neither being
// unassigned nor assigned so it isn't
// added to this list again, but it's
// layer isn't used in any calculation.
otherNode.temp[0] = -2;
}
}
}
startNodes.shift();
}
else
{
// Not all the edges have been scanned, get to the back of
// the class and put the dunces cap on
var removedCell = startNodes.shift();
startNodes.push(internalNode);
if (removedCell == internalNode && startNodes.length == 1)
{
// This is an error condition, we can't get out of
// this loop. It could happen for more than one node
// but that's a lot harder to detect. Log the error
// TODO make log comment
break;
}
}
}
// Normalize the ranks down from their large starting value to place
// at least 1 sink on layer 0
// for (var key in this.vertexMapper)
// {
// var internalNode = this.vertexMapper[key];
// // Mark the node as not having had a layer assigned
// internalNode.temp[0] -= this.maxRank;
// }
// Tighten the rank 0 nodes as far as possible
// for ( var i = 0; i < startNodesCopy.length; i++)
// {
// var internalNode = startNodesCopy[i];
// var currentMaxLayer = 0;
// var layerDeterminingEdges = internalNode.connectsAsSource;
//
// for ( var j = 0; j < layerDeterminingEdges.length; j++)
// {
// var internalEdge = layerDeterminingEdges[j];
// var otherNode = internalEdge.target;
// internalNode.temp[0] = Math.max(currentMaxLayer,
// otherNode.temp[0] + 1);
// currentMaxLayer = internalNode.temp[0];
// }
// }
};
/**
* Function: maxChainDfs
*
* Performs a depth first search on the internal hierarchy model. This dfs
* extends the default version by keeping track of chains within groups.
* Any cycles should be removed prior to running, but previously seen cells
* are ignored.
*
* Parameters:
*
* parent - the parent internal node of the current internal node
* root - the current internal node
* connectingEdge - the internal edge connecting the internal node and the parent
* internal node, if any
* seen - a set of all nodes seen by this dfs
* chainCount - the number of edges in the chain of vertices going through
* the current swimlane
*/
mxSwimlaneModel.prototype.maxChainDfs = function(parent, root, connectingEdge, seen, chainCount)
{
if (root != null)
{
var rootId = mxCellPath.create(root.cell);
if (seen[rootId] == null)
{
seen[rootId] = root;
var slIndex = root.swimlaneIndex;
if (this.ranksPerGroup[slIndex] == null || this.ranksPerGroup[slIndex] < chainCount)
{
this.ranksPerGroup[slIndex] = chainCount;
}
// Copy the connects as source list so that visitors
// can change the original for edge direction inversions
var outgoingEdges = root.connectsAsSource.slice();
for (var i = 0; i < outgoingEdges.length; i++)
{
var internalEdge = outgoingEdges[i];
var targetNode = internalEdge.target;
// Only navigate in source->target direction within the same
// swimlane, or from a lower index swimlane to a higher one
if (root.swimlaneIndex < targetNode.swimlaneIndex)
{
this.maxChainDfs(root, targetNode, internalEdge, mxUtils.clone(seen, null , true), 0);
}
else if (root.swimlaneIndex == targetNode.swimlaneIndex)
{
this.maxChainDfs(root, targetNode, internalEdge, mxUtils.clone(seen, null , true), chainCount + 1);
}
}
}
}
};
/**
* Function: fixRanks
*
* Fixes the layer assignments to the values stored in the nodes. Also needs
* to create dummy nodes for edges that cross layers.
*/
mxSwimlaneModel.prototype.fixRanks = function()
{
var rankList = [];
this.ranks = [];
for (var i = 0; i < this.maxRank + 1; i++)
{
rankList[i] = [];
this.ranks[i] = rankList[i];
}
// Perform a DFS to obtain an initial ordering for each rank.
// Without doing this you would end up having to process
// crossings for a standard tree.
var rootsArray = null;
if (this.roots != null)
{
var oldRootsArray = this.roots;
rootsArray = [];
for (var i = 0; i < oldRootsArray.length; i++)
{
var cell = oldRootsArray[i];
var internalNode = this.vertexMapper.get(cell);
rootsArray[i] = internalNode;
}
}
this.visit(function(parent, node, edge, layer, seen)
{
if (seen == 0 && node.maxRank < 0 && node.minRank < 0)
{
rankList[node.temp[0]].push(node);
node.maxRank = node.temp[0];
node.minRank = node.temp[0];
// Set temp[0] to the nodes position in the rank
node.temp[0] = rankList[node.maxRank].length - 1;
}
if (parent != null && edge != null)
{
var parentToCellRankDifference = parent.maxRank - node.maxRank;
if (parentToCellRankDifference > 1)
{
// There are ranks in between the parent and current cell
edge.maxRank = parent.maxRank;
edge.minRank = node.maxRank;
edge.temp = [];
edge.x = [];
edge.y = [];
for (var i = edge.minRank + 1; i < edge.maxRank; i++)
{
// The connecting edge must be added to the
// appropriate ranks
rankList[i].push(edge);
edge.setGeneralPurposeVariable(i, rankList[i]
.length - 1);
}
}
}
}, rootsArray, false, null);
};
/**
* Function: visit
*
* A depth first search through the internal heirarchy model.
*
* Parameters:
*
* visitor - The visitor function pattern to be called for each node.
* trackAncestors - Whether or not the search is to keep track all nodes
* directly above this one in the search path.
*/
mxSwimlaneModel.prototype.visit = function(visitor, dfsRoots, trackAncestors, seenNodes)
{
// Run dfs through on all roots
if (dfsRoots != null)
{
for (var i = 0; i < dfsRoots.length; i++)
{
var internalNode = dfsRoots[i];
if (internalNode != null)
{
if (seenNodes == null)
{
seenNodes = new Object();
}
if (trackAncestors)
{
// Set up hash code for root
internalNode.hashCode = [];
internalNode.hashCode[0] = this.dfsCount;
internalNode.hashCode[1] = i;
this.extendedDfs(null, internalNode, null, visitor, seenNodes,
internalNode.hashCode, i, 0);
}
else
{
this.dfs(null, internalNode, null, visitor, seenNodes, 0);
}
}
}
this.dfsCount++;
}
};
/**
* Function: dfs
*
* Performs a depth first search on the internal hierarchy model
*
* Parameters:
*
* parent - the parent internal node of the current internal node
* root - the current internal node
* connectingEdge - the internal edge connecting the internal node and the parent
* internal node, if any
* visitor - the visitor pattern to be called for each node
* seen - a set of all nodes seen by this dfs a set of all of the
* ancestor node of the current node
* layer - the layer on the dfs tree ( not the same as the model ranks )
*/
mxSwimlaneModel.prototype.dfs = function(parent, root, connectingEdge, visitor, seen, layer)
{
if (root != null)
{
var rootId = root.id;
if (seen[rootId] == null)
{
seen[rootId] = root;
visitor(parent, root, connectingEdge, layer, 0);
// Copy the connects as source list so that visitors
// can change the original for edge direction inversions
var outgoingEdges = root.connectsAsSource.slice();
for (var i = 0; i< outgoingEdges.length; i++)
{
var internalEdge = outgoingEdges[i];
var targetNode = internalEdge.target;
// Root check is O(|roots|)
this.dfs(root, targetNode, internalEdge, visitor, seen,
layer + 1);
}
}
else
{
// Use the int field to indicate this node has been seen
visitor(parent, root, connectingEdge, layer, 1);
}
}
};
/**
* Function: extendedDfs
*
* Performs a depth first search on the internal hierarchy model. This dfs
* extends the default version by keeping track of cells ancestors, but it
* should be only used when necessary because of it can be computationally
* intensive for deep searches.
*
* Parameters:
*
* parent - the parent internal node of the current internal node
* root - the current internal node
* connectingEdge - the internal edge connecting the internal node and the parent
* internal node, if any
* visitor - the visitor pattern to be called for each node
* seen - a set of all nodes seen by this dfs
* ancestors - the parent hash code
* childHash - the new hash code for this node
* layer - the layer on the dfs tree ( not the same as the model ranks )
*/
mxSwimlaneModel.prototype.extendedDfs = function(parent, root, connectingEdge, visitor, seen, ancestors, childHash, layer)
{
// Explanation of custom hash set. Previously, the ancestors variable
// was passed through the dfs as a HashSet. The ancestors were copied
// into a new HashSet and when the new child was processed it was also
// added to the set. If the current node was in its ancestor list it
// meant there is a cycle in the graph and this information is passed
// to the visitor.visit() in the seen parameter. The HashSet clone was
// very expensive on CPU so a custom hash was developed using primitive
// types. temp[] couldn't be used so hashCode[] was added to each node.
// Each new child adds another int to the array, copying the prefix
// from its parent. Child of the same parent add different ints (the
// limit is therefore 2^32 children per parent...). If a node has a
// child with the hashCode already set then the child code is compared
// to the same portion of the current nodes array. If they match there
// is a loop.
// Note that the basic mechanism would only allow for 1 use of this
// functionality, so the root nodes have two ints. The second int is
// incremented through each node root and the first is incremented
// through each run of the dfs algorithm (therefore the dfs is not
// thread safe). The hash code of each node is set if not already set,
// or if the first int does not match that of the current run.
if (root != null)
{
if (parent != null)
{
// Form this nodes hash code if necessary, that is, if the
// hashCode variable has not been initialized or if the
// start of the parent hash code does not equal the start of
// this nodes hash code, indicating the code was set on a
// previous run of this dfs.
if (root.hashCode == null ||
root.hashCode[0] != parent.hashCode[0])
{
var hashCodeLength = parent.hashCode.length + 1;
root.hashCode = parent.hashCode.slice();
root.hashCode[hashCodeLength - 1] = childHash;
}
}
var rootId = root.id;
if (seen[rootId] == null)
{
seen[rootId] = root;
visitor(parent, root, connectingEdge, layer, 0);
// Copy the connects as source list so that visitors
// can change the original for edge direction inversions
var outgoingEdges = root.connectsAsSource.slice();
var incomingEdges = root.connectsAsTarget.slice();
for (var i = 0; i < outgoingEdges.length; i++)
{
var internalEdge = outgoingEdges[i];
var targetNode = internalEdge.target;
// Only navigate in source->target direction within the same
// swimlane, or from a lower index swimlane to a higher one
if (root.swimlaneIndex <= targetNode.swimlaneIndex)
{
this.extendedDfs(root, targetNode, internalEdge, visitor, seen,
root.hashCode, i, layer + 1);
}
}
for (var i = 0; i < incomingEdges.length; i++)
{
var internalEdge = incomingEdges[i];
var targetNode = internalEdge.source;
// Only navigate in target->source direction from a lower index
// swimlane to a higher one
if (root.swimlaneIndex < targetNode.swimlaneIndex)
{
this.extendedDfs(root, targetNode, internalEdge, visitor, seen,
root.hashCode, i, layer + 1);
}
}
}
else
{
// Use the int field to indicate this node has been seen
visitor(parent, root, connectingEdge, layer, 1);
}
}
};

View File

@ -0,0 +1,851 @@
/**
* Copyright (c) 2006-2018, JGraph Ltd
* Copyright (c) 2006-2018, Gaudenz Alder
*/
/**
* Class: mxHierarchicalLayout
*
* A hierarchical layout algorithm.
*
* Constructor: mxHierarchicalLayout
*
* Constructs a new hierarchical layout algorithm.
*
* Arguments:
*
* graph - Reference to the enclosing <mxGraph>.
* orientation - Optional constant that defines the orientation of this
* layout.
* deterministic - Optional boolean that specifies if this layout should be
* deterministic. Default is true.
*/
function mxHierarchicalLayout(graph, orientation, deterministic)
{
mxGraphLayout.call(this, graph);
this.orientation = (orientation != null) ? orientation : mxConstants.DIRECTION_NORTH;
this.deterministic = (deterministic != null) ? deterministic : true;
};
var mxHierarchicalEdgeStyle =
{
ORTHOGONAL: 1,
POLYLINE: 2,
STRAIGHT: 3,
CURVE: 4
};
/**
* Extends mxGraphLayout.
*/
mxHierarchicalLayout.prototype = new mxGraphLayout();
mxHierarchicalLayout.prototype.constructor = mxHierarchicalLayout;
/**
* Variable: roots
*
* Holds the array of <mxCell> that this layout contains.
*/
mxHierarchicalLayout.prototype.roots = null;
/**
* Variable: resizeParent
*
* Specifies if the parent should be resized after the layout so that it
* contains all the child cells. Default is false. See also <parentBorder>.
*/
mxHierarchicalLayout.prototype.resizeParent = false;
/**
* Variable: maintainParentLocation
*
* Specifies if the parent location should be maintained, so that the
* top, left corner stays the same before and after execution of
* the layout. Default is false for backwards compatibility.
*/
mxHierarchicalLayout.prototype.maintainParentLocation = false;
/**
* Variable: moveParent
*
* Specifies if the parent should be moved if <resizeParent> is enabled.
* Default is false.
*/
mxHierarchicalLayout.prototype.moveParent = false;
/**
* Variable: parentBorder
*
* The border to be added around the children if the parent is to be
* resized using <resizeParent>. Default is 0.
*/
mxHierarchicalLayout.prototype.parentBorder = 0;
/**
* Variable: intraCellSpacing
*
* The spacing buffer added between cells on the same layer. Default is 30.
*/
mxHierarchicalLayout.prototype.intraCellSpacing = 30;
/**
* Variable: interRankCellSpacing
*
* The spacing buffer added between cell on adjacent layers. Default is 100.
*/
mxHierarchicalLayout.prototype.interRankCellSpacing = 100;
/**
* Variable: interHierarchySpacing
*
* The spacing buffer between unconnected hierarchies. Default is 60.
*/
mxHierarchicalLayout.prototype.interHierarchySpacing = 60;
/**
* Variable: parallelEdgeSpacing
*
* The distance between each parallel edge on each ranks for long edges.
* Default is 10.
*/
mxHierarchicalLayout.prototype.parallelEdgeSpacing = 10;
/**
* Variable: orientation
*
* The position of the root node(s) relative to the laid out graph in.
* Default is <mxConstants.DIRECTION_NORTH>.
*/
mxHierarchicalLayout.prototype.orientation = mxConstants.DIRECTION_NORTH;
/**
* Variable: fineTuning
*
* Whether or not to perform local optimisations and iterate multiple times
* through the algorithm. Default is true.
*/
mxHierarchicalLayout.prototype.fineTuning = true;
/**
*
* Variable: tightenToSource
*
* Whether or not to tighten the assigned ranks of vertices up towards
* the source cells. Default is true.
*/
mxHierarchicalLayout.prototype.tightenToSource = true;
/**
* Variable: disableEdgeStyle
*
* Specifies if the STYLE_NOEDGESTYLE flag should be set on edges that are
* modified by the result. Default is true.
*/
mxHierarchicalLayout.prototype.disableEdgeStyle = true;
/**
* Variable: traverseAncestors
*
* Whether or not to drill into child cells and layout in reverse
* group order. This also cause the layout to navigate edges whose
* terminal vertices have different parents but are in the same
* ancestry chain. Default is true.
*/
mxHierarchicalLayout.prototype.traverseAncestors = true;
/**
* Variable: model
*
* The internal <mxGraphHierarchyModel> formed of the layout.
*/
mxHierarchicalLayout.prototype.model = null;
/**
* Variable: edgesSet
*
* A cache of edges whose source terminal is the key
*/
mxHierarchicalLayout.prototype.edgesCache = null;
/**
* Variable: edgesSet
*
* A cache of edges whose source terminal is the key
*/
mxHierarchicalLayout.prototype.edgeSourceTermCache = null;
/**
* Variable: edgesSet
*
* A cache of edges whose source terminal is the key
*/
mxHierarchicalLayout.prototype.edgesTargetTermCache = null;
/**
* Variable: edgeStyle
*
* The style to apply between cell layers to edge segments.
* Default is <mxHierarchicalEdgeStyle.POLYLINE>.
*/
mxHierarchicalLayout.prototype.edgeStyle = mxHierarchicalEdgeStyle.POLYLINE;
/**
* Function: getModel
*
* Returns the internal <mxGraphHierarchyModel> for this layout algorithm.
*/
mxHierarchicalLayout.prototype.getModel = function()
{
return this.model;
};
/**
* Function: execute
*
* Executes the layout for the children of the specified parent.
*
* Parameters:
*
* parent - Parent <mxCell> that contains the children to be laid out.
* roots - Optional starting roots of the layout.
*/
mxHierarchicalLayout.prototype.execute = function(parent, roots)
{
this.parent = parent;
var model = this.graph.model;
this.edgesCache = new mxDictionary();
this.edgeSourceTermCache = new mxDictionary();
this.edgesTargetTermCache = new mxDictionary();
if (roots != null && !(roots instanceof Array))
{
roots = [roots];
}
// If the roots are set and the parent is set, only
// use the roots that are some dependent of the that
// parent.
// If just the root are set, use them as-is
// If just the parent is set use it's immediate
// children as the initial set
if (roots == null && parent == null)
{
// TODO indicate the problem
return;
}
// Maintaining parent location
this.parentX = null;
this.parentY = null;
if (parent != this.root && model.isVertex(parent) != null && this.maintainParentLocation)
{
var geo = this.graph.getCellGeometry(parent);
if (geo != null)
{
this.parentX = geo.x;
this.parentY = geo.y;
}
}
if (roots != null)
{
var rootsCopy = [];
for (var i = 0; i < roots.length; i++)
{
var ancestor = parent != null ? model.isAncestor(parent, roots[i]) : true;
if (ancestor && model.isVertex(roots[i]))
{
rootsCopy.push(roots[i]);
}
}
this.roots = rootsCopy;
}
model.beginUpdate();
try
{
this.run(parent);
if (this.resizeParent && !this.graph.isCellCollapsed(parent))
{
this.graph.updateGroupBounds([parent], this.parentBorder, this.moveParent);
}
// Maintaining parent location
if (this.parentX != null && this.parentY != null)
{
var geo = this.graph.getCellGeometry(parent);
if (geo != null)
{
geo = geo.clone();
geo.x = this.parentX;
geo.y = this.parentY;
model.setGeometry(parent, geo);
}
}
}
finally
{
model.endUpdate();
}
};
/**
* Function: findRoots
*
* Returns all visible children in the given parent which do not have
* incoming edges. If the result is empty then the children with the
* maximum difference between incoming and outgoing edges are returned.
* This takes into account edges that are being promoted to the given
* root due to invisible children or collapsed cells.
*
* Parameters:
*
* parent - <mxCell> whose children should be checked.
* vertices - array of vertices to limit search to
*/
mxHierarchicalLayout.prototype.findRoots = function(parent, vertices)
{
var roots = [];
if (parent != null && vertices != null)
{
var model = this.graph.model;
var best = null;
var maxDiff = -100000;
for (var i in vertices)
{
var cell = vertices[i];
if (model.isVertex(cell) && this.graph.isCellVisible(cell))
{
var conns = this.getEdges(cell);
var fanOut = 0;
var fanIn = 0;
for (var k = 0; k < conns.length; k++)
{
var src = this.getVisibleTerminal(conns[k], true);
if (src == cell)
{
fanOut++;
}
else
{
fanIn++;
}
}
if (fanIn == 0 && fanOut > 0)
{
roots.push(cell);
}
var diff = fanOut - fanIn;
if (diff > maxDiff)
{
maxDiff = diff;
best = cell;
}
}
}
if (roots.length == 0 && best != null)
{
roots.push(best);
}
}
return roots;
};
/**
* Function: getEdges
*
* Returns the connected edges for the given cell.
*
* Parameters:
*
* cell - <mxCell> whose edges should be returned.
*/
mxHierarchicalLayout.prototype.getEdges = function(cell)
{
var cachedEdges = this.edgesCache.get(cell);
if (cachedEdges != null)
{
return cachedEdges;
}
var model = this.graph.model;
var edges = [];
var isCollapsed = this.graph.isCellCollapsed(cell);
var childCount = model.getChildCount(cell);
for (var i = 0; i < childCount; i++)
{
var child = model.getChildAt(cell, i);
if (this.isPort(child))
{
edges = edges.concat(model.getEdges(child, true, true));
}
else if (isCollapsed || !this.graph.isCellVisible(child))
{
edges = edges.concat(model.getEdges(child, true, true));
}
}
edges = edges.concat(model.getEdges(cell, true, true));
var result = [];
for (var i = 0; i < edges.length; i++)
{
var source = this.getVisibleTerminal(edges[i], true);
var target = this.getVisibleTerminal(edges[i], false);
if ((source == target) ||
((source != target) &&
((target == cell && (this.parent == null || this.isAncestor(this.parent, source, this.traverseAncestors))) ||
(source == cell && (this.parent == null || this.isAncestor(this.parent, target, this.traverseAncestors))))))
{
result.push(edges[i]);
}
}
this.edgesCache.put(cell, result);
return result;
};
/**
* Function: getVisibleTerminal
*
* Helper function to return visible terminal for edge allowing for ports
*
* Parameters:
*
* edge - <mxCell> whose edges should be returned.
* source - Boolean that specifies whether the source or target terminal is to be returned
*/
mxHierarchicalLayout.prototype.getVisibleTerminal = function(edge, source)
{
var terminalCache = this.edgesTargetTermCache;
if (source)
{
terminalCache = this.edgeSourceTermCache;
}
var term = terminalCache.get(edge);
if (term != null)
{
return term;
}
var state = this.graph.view.getState(edge);
var terminal = (state != null) ? state.getVisibleTerminal(source) : this.graph.view.getVisibleTerminal(edge, source);
if (terminal == null)
{
terminal = (state != null) ? state.getVisibleTerminal(source) : this.graph.view.getVisibleTerminal(edge, source);
}
if (terminal != null)
{
if (this.isPort(terminal))
{
terminal = this.graph.model.getParent(terminal);
}
terminalCache.put(edge, terminal);
}
return terminal;
};
/**
* Function: run
*
* The API method used to exercise the layout upon the graph description
* and produce a separate description of the vertex position and edge
* routing changes made. It runs each stage of the layout that has been
* created.
*/
mxHierarchicalLayout.prototype.run = function(parent)
{
// Separate out unconnected hierarchies
var hierarchyVertices = [];
var allVertexSet = [];
if (this.roots == null && parent != null)
{
var filledVertexSet = Object();
this.filterDescendants(parent, filledVertexSet);
this.roots = [];
var filledVertexSetEmpty = true;
// Poor man's isSetEmpty
for (var key in filledVertexSet)
{
if (filledVertexSet[key] != null)
{
filledVertexSetEmpty = false;
break;
}
}
while (!filledVertexSetEmpty)
{
var candidateRoots = this.findRoots(parent, filledVertexSet);
// If the candidate root is an unconnected group cell, remove it from
// the layout. We may need a custom set that holds such groups and forces
// them to be processed for resizing and/or moving.
for (var i = 0; i < candidateRoots.length; i++)
{
var vertexSet = Object();
hierarchyVertices.push(vertexSet);
this.traverse(candidateRoots[i], true, null, allVertexSet, vertexSet,
hierarchyVertices, filledVertexSet);
}
for (var i = 0; i < candidateRoots.length; i++)
{
this.roots.push(candidateRoots[i]);
}
filledVertexSetEmpty = true;
// Poor man's isSetEmpty
for (var key in filledVertexSet)
{
if (filledVertexSet[key] != null)
{
filledVertexSetEmpty = false;
break;
}
}
}
}
else
{
// Find vertex set as directed traversal from roots
for (var i = 0; i < this.roots.length; i++)
{
var vertexSet = Object();
hierarchyVertices.push(vertexSet);
this.traverse(this.roots[i], true, null, allVertexSet, vertexSet,
hierarchyVertices, null);
}
}
// Iterate through the result removing parents who have children in this layout
// Perform a layout for each seperate hierarchy
// Track initial coordinate x-positioning
var initialX = 0;
for (var i = 0; i < hierarchyVertices.length; i++)
{
var vertexSet = hierarchyVertices[i];
var tmp = [];
for (var key in vertexSet)
{
tmp.push(vertexSet[key]);
}
this.model = new mxGraphHierarchyModel(this, tmp, this.roots,
parent, this.tightenToSource);
this.cycleStage(parent);
this.layeringStage();
this.crossingStage(parent);
initialX = this.placementStage(initialX, parent);
}
};
/**
* Function: filterDescendants
*
* Creates an array of descendant cells
*/
mxHierarchicalLayout.prototype.filterDescendants = function(cell, result)
{
var model = this.graph.model;
if (model.isVertex(cell) && cell != this.parent && this.graph.isCellVisible(cell))
{
result[mxObjectIdentity.get(cell)] = cell;
}
if (this.traverseAncestors || cell == this.parent
&& this.graph.isCellVisible(cell))
{
var childCount = model.getChildCount(cell);
for (var i = 0; i < childCount; i++)
{
var child = model.getChildAt(cell, i);
// Ignore ports in the layout vertex list, they are dealt with
// in the traversal mechanisms
if (!this.isPort(child))
{
this.filterDescendants(child, result);
}
}
}
};
/**
* Function: isPort
*
* Returns true if the given cell is a "port", that is, when connecting to
* it, its parent is the connecting vertex in terms of graph traversal
*
* Parameters:
*
* cell - <mxCell> that represents the port.
*/
mxHierarchicalLayout.prototype.isPort = function(cell)
{
if (cell != null && cell.geometry != null)
{
return cell.geometry.relative;
}
else
{
return false;
}
};
/**
* Function: getEdgesBetween
*
* Returns the edges between the given source and target. This takes into
* account collapsed and invisible cells and ports.
*
* Parameters:
*
* source -
* target -
* directed -
*/
mxHierarchicalLayout.prototype.getEdgesBetween = function(source, target, directed)
{
directed = (directed != null) ? directed : false;
var edges = this.getEdges(source);
var result = [];
// Checks if the edge is connected to the correct
// cell and returns the first match
for (var i = 0; i < edges.length; i++)
{
var src = this.getVisibleTerminal(edges[i], true);
var trg = this.getVisibleTerminal(edges[i], false);
if ((src == source && trg == target) || (!directed && src == target && trg == source))
{
result.push(edges[i]);
}
}
return result;
};
/**
* Traverses the (directed) graph invoking the given function for each
* visited vertex and edge. The function is invoked with the current vertex
* and the incoming edge as a parameter. This implementation makes sure
* each vertex is only visited once. The function may return false if the
* traversal should stop at the given vertex.
*
* Parameters:
*
* vertex - <mxCell> that represents the vertex where the traversal starts.
* directed - boolean indicating if edges should only be traversed
* from source to target. Default is true.
* edge - Optional <mxCell> that represents the incoming edge. This is
* null for the first step of the traversal.
* allVertices - Array of cell paths for the visited cells.
*/
mxHierarchicalLayout.prototype.traverse = function(vertex, directed, edge, allVertices, currentComp,
hierarchyVertices, filledVertexSet)
{
if (vertex != null && allVertices != null)
{
// Has this vertex been seen before in any traversal
// And if the filled vertex set is populated, only
// process vertices in that it contains
var vertexID = mxObjectIdentity.get(vertex);
if ((allVertices[vertexID] == null)
&& (filledVertexSet == null ? true : filledVertexSet[vertexID] != null))
{
if (currentComp[vertexID] == null)
{
currentComp[vertexID] = vertex;
}
if (allVertices[vertexID] == null)
{
allVertices[vertexID] = vertex;
}
if (filledVertexSet !== null)
{
delete filledVertexSet[vertexID];
}
var edges = this.getEdges(vertex);
var edgeIsSource = [];
for (var i = 0; i < edges.length; i++)
{
edgeIsSource[i] = (this.getVisibleTerminal(edges[i], true) == vertex);
}
for (var i = 0; i < edges.length; i++)
{
if (!directed || edgeIsSource[i])
{
var next = this.getVisibleTerminal(edges[i], !edgeIsSource[i]);
// Check whether there are more edges incoming from the target vertex than outgoing
// The hierarchical model treats bi-directional parallel edges as being sourced
// from the more "sourced" terminal. If the directions are equal in number, the direction
// is that of the natural direction from the roots of the layout.
// The checks below are slightly more verbose than need be for performance reasons
var netCount = 1;
for (var j = 0; j < edges.length; j++)
{
if (j == i)
{
continue;
}
else
{
var isSource2 = edgeIsSource[j];
var otherTerm = this.getVisibleTerminal(edges[j], !isSource2);
if (otherTerm == next)
{
if (isSource2)
{
netCount++;
}
else
{
netCount--;
}
}
}
}
if (netCount >= 0)
{
currentComp = this.traverse(next, directed, edges[i], allVertices,
currentComp, hierarchyVertices,
filledVertexSet);
}
}
}
}
else
{
if (currentComp[vertexID] == null)
{
// We've seen this vertex before, but not in the current component
// This component and the one it's in need to be merged
for (var i = 0; i < hierarchyVertices.length; i++)
{
var comp = hierarchyVertices[i];
if (comp[vertexID] != null)
{
for (var key in comp)
{
currentComp[key] = comp[key];
}
// Remove the current component from the hierarchy set
hierarchyVertices.splice(i, 1);
return currentComp;
}
}
}
}
}
return currentComp;
};
/**
* Function: cycleStage
*
* Executes the cycle stage using mxMinimumCycleRemover.
*/
mxHierarchicalLayout.prototype.cycleStage = function(parent)
{
var cycleStage = new mxMinimumCycleRemover(this);
cycleStage.execute(parent);
};
/**
* Function: layeringStage
*
* Implements first stage of a Sugiyama layout.
*/
mxHierarchicalLayout.prototype.layeringStage = function()
{
this.model.initialRank();
this.model.fixRanks();
};
/**
* Function: crossingStage
*
* Executes the crossing stage using mxMedianHybridCrossingReduction.
*/
mxHierarchicalLayout.prototype.crossingStage = function(parent)
{
var crossingStage = new mxMedianHybridCrossingReduction(this);
crossingStage.execute(parent);
};
/**
* Function: placementStage
*
* Executes the placement stage using mxCoordinateAssignment.
*/
mxHierarchicalLayout.prototype.placementStage = function(initialX, parent)
{
var placementStage = new mxCoordinateAssignment(this, this.intraCellSpacing,
this.interRankCellSpacing, this.orientation, initialX,
this.parallelEdgeSpacing);
placementStage.fineTuning = this.fineTuning;
placementStage.execute(parent);
return placementStage.limitX + this.interHierarchySpacing;
};

View File

@ -0,0 +1,933 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxSwimlaneLayout
*
* A hierarchical layout algorithm.
*
* Constructor: mxSwimlaneLayout
*
* Constructs a new hierarchical layout algorithm.
*
* Arguments:
*
* graph - Reference to the enclosing <mxGraph>.
* orientation - Optional constant that defines the orientation of this
* layout.
* deterministic - Optional boolean that specifies if this layout should be
* deterministic. Default is true.
*/
function mxSwimlaneLayout(graph, orientation, deterministic)
{
mxGraphLayout.call(this, graph);
this.orientation = (orientation != null) ? orientation : mxConstants.DIRECTION_NORTH;
this.deterministic = (deterministic != null) ? deterministic : true;
};
/**
* Extends mxGraphLayout.
*/
mxSwimlaneLayout.prototype = new mxGraphLayout();
mxSwimlaneLayout.prototype.constructor = mxSwimlaneLayout;
/**
* Variable: roots
*
* Holds the array of <mxCell> that this layout contains.
*/
mxSwimlaneLayout.prototype.roots = null;
/**
* Variable: swimlanes
*
* Holds the array of <mxCell> of the ordered swimlanes to lay out
*/
mxSwimlaneLayout.prototype.swimlanes = null;
/**
* Variable: dummyVertexWidth
*
* The cell width of any dummy vertices inserted
*/
mxSwimlaneLayout.prototype.dummyVertexWidth = 50;
/**
* Variable: resizeParent
*
* Specifies if the parent should be resized after the layout so that it
* contains all the child cells. Default is false. See also <parentBorder>.
*/
mxSwimlaneLayout.prototype.resizeParent = false;
/**
* Variable: maintainParentLocation
*
* Specifies if the parent location should be maintained, so that the
* top, left corner stays the same before and after execution of
* the layout. Default is false for backwards compatibility.
*/
mxSwimlaneLayout.prototype.maintainParentLocation = false;
/**
* Variable: moveParent
*
* Specifies if the parent should be moved if <resizeParent> is enabled.
* Default is false.
*/
mxSwimlaneLayout.prototype.moveParent = false;
/**
* Variable: parentBorder
*
* The border to be added around the children if the parent is to be
* resized using <resizeParent>. Default is 30.
*/
mxSwimlaneLayout.prototype.parentBorder = 30;
/**
* Variable: intraCellSpacing
*
* The spacing buffer added between cells on the same layer. Default is 30.
*/
mxSwimlaneLayout.prototype.intraCellSpacing = 30;
/**
* Variable: interRankCellSpacing
*
* The spacing buffer added between cell on adjacent layers. Default is 100.
*/
mxSwimlaneLayout.prototype.interRankCellSpacing = 100;
/**
* Variable: interHierarchySpacing
*
* The spacing buffer between unconnected hierarchies. Default is 60.
*/
mxSwimlaneLayout.prototype.interHierarchySpacing = 60;
/**
* Variable: parallelEdgeSpacing
*
* The distance between each parallel edge on each ranks for long edges.
* Default is 10.
*/
mxSwimlaneLayout.prototype.parallelEdgeSpacing = 10;
/**
* Variable: orientation
*
* The position of the root node(s) relative to the laid out graph in.
* Default is <mxConstants.DIRECTION_NORTH>.
*/
mxSwimlaneLayout.prototype.orientation = mxConstants.DIRECTION_NORTH;
/**
* Variable: fineTuning
*
* Whether or not to perform local optimisations and iterate multiple times
* through the algorithm. Default is true.
*/
mxSwimlaneLayout.prototype.fineTuning = true;
/**
* Variable: tightenToSource
*
* Whether or not to tighten the assigned ranks of vertices up towards
* the source cells. Default is true.
*/
mxSwimlaneLayout.prototype.tightenToSource = true;
/**
* Variable: disableEdgeStyle
*
* Specifies if the STYLE_NOEDGESTYLE flag should be set on edges that are
* modified by the result. Default is true.
*/
mxSwimlaneLayout.prototype.disableEdgeStyle = true;
/**
* Variable: traverseAncestors
*
* Whether or not to drill into child cells and layout in reverse
* group order. This also cause the layout to navigate edges whose
* terminal vertices have different parents but are in the same
* ancestry chain. Default is true.
*/
mxSwimlaneLayout.prototype.traverseAncestors = true;
/**
* Variable: model
*
* The internal <mxSwimlaneModel> formed of the layout.
*/
mxSwimlaneLayout.prototype.model = null;
/**
* Variable: edgesSet
*
* A cache of edges whose source terminal is the key
*/
mxSwimlaneLayout.prototype.edgesCache = null;
/**
* Variable: edgesSet
*
* A cache of edges whose source terminal is the key
*/
mxHierarchicalLayout.prototype.edgeSourceTermCache = null;
/**
* Variable: edgesSet
*
* A cache of edges whose source terminal is the key
*/
mxHierarchicalLayout.prototype.edgesTargetTermCache = null;
/**
* Variable: edgeStyle
*
* The style to apply between cell layers to edge segments.
* Default is <mxHierarchicalEdgeStyle.POLYLINE>.
*/
mxHierarchicalLayout.prototype.edgeStyle = mxHierarchicalEdgeStyle.POLYLINE;
/**
* Function: getModel
*
* Returns the internal <mxSwimlaneModel> for this layout algorithm.
*/
mxSwimlaneLayout.prototype.getModel = function()
{
return this.model;
};
/**
* Function: execute
*
* Executes the layout for the children of the specified parent.
*
* Parameters:
*
* parent - Parent <mxCell> that contains the children to be laid out.
* swimlanes - Ordered array of swimlanes to be laid out
*/
mxSwimlaneLayout.prototype.execute = function(parent, swimlanes)
{
this.parent = parent;
var model = this.graph.model;
this.edgesCache = new mxDictionary();
this.edgeSourceTermCache = new mxDictionary();
this.edgesTargetTermCache = new mxDictionary();
// If the roots are set and the parent is set, only
// use the roots that are some dependent of the that
// parent.
// If just the root are set, use them as-is
// If just the parent is set use it's immediate
// children as the initial set
if (swimlanes == null || swimlanes.length < 1)
{
// TODO indicate the problem
return;
}
if (parent == null)
{
parent = model.getParent(swimlanes[0]);
}
// Maintaining parent location
this.parentX = null;
this.parentY = null;
if (parent != this.root && model.isVertex(parent) != null && this.maintainParentLocation)
{
var geo = this.graph.getCellGeometry(parent);
if (geo != null)
{
this.parentX = geo.x;
this.parentY = geo.y;
}
}
this.swimlanes = swimlanes;
var dummyVertices = [];
// Check the swimlanes all have vertices
// in them
for (var i = 0; i < swimlanes.length; i++)
{
var children = this.graph.getChildCells(swimlanes[i]);
if (children == null || children.length == 0)
{
var vertex = this.graph.insertVertex(swimlanes[i], null, null, 0, 0, this.dummyVertexWidth, 0);
dummyVertices.push(vertex);
}
}
model.beginUpdate();
try
{
this.run(parent);
if (this.resizeParent && !this.graph.isCellCollapsed(parent))
{
this.graph.updateGroupBounds([parent], this.parentBorder, this.moveParent);
}
// Maintaining parent location
if (this.parentX != null && this.parentY != null)
{
var geo = this.graph.getCellGeometry(parent);
if (geo != null)
{
geo = geo.clone();
geo.x = this.parentX;
geo.y = this.parentY;
model.setGeometry(parent, geo);
}
}
this.graph.removeCells(dummyVertices);
}
finally
{
model.endUpdate();
}
};
/**
* Function: updateGroupBounds
*
* Updates the bounds of the given array of groups so that it includes
* all child vertices.
*
*/
mxSwimlaneLayout.prototype.updateGroupBounds = function()
{
// Get all vertices and edge in the layout
var cells = [];
var model = this.model;
for (var key in model.edgeMapper)
{
var edge = model.edgeMapper[key];
for (var i = 0; i < edge.edges.length; i++)
{
cells.push(edge.edges[i]);
}
}
var layoutBounds = this.graph.getBoundingBoxFromGeometry(cells, true);
var childBounds = [];
for (var i = 0; i < this.swimlanes.length; i++)
{
var lane = this.swimlanes[i];
var geo = this.graph.getCellGeometry(lane);
if (geo != null)
{
var children = this.graph.getChildCells(lane);
var size = (this.graph.isSwimlane(lane)) ?
this.graph.getStartSize(lane) : new mxRectangle();
var bounds = this.graph.getBoundingBoxFromGeometry(children);
childBounds[i] = bounds;
var childrenY = bounds.y + geo.y - size.height - this.parentBorder;
var maxChildrenY = bounds.y + geo.y + bounds.height;
if (layoutBounds == null)
{
layoutBounds = new mxRectangle(0, childrenY, 0, maxChildrenY - childrenY);
}
else
{
layoutBounds.y = Math.min(layoutBounds.y, childrenY);
var maxY = Math.max(layoutBounds.y + layoutBounds.height, maxChildrenY);
layoutBounds.height = maxY - layoutBounds.y;
}
}
}
for (var i = 0; i < this.swimlanes.length; i++)
{
var lane = this.swimlanes[i];
var geo = this.graph.getCellGeometry(lane);
if (geo != null)
{
var children = this.graph.getChildCells(lane);
var size = (this.graph.isSwimlane(lane)) ?
this.graph.getStartSize(lane) : new mxRectangle();
var newGeo = geo.clone();
var leftGroupBorder = (i == 0) ? this.parentBorder : this.interRankCellSpacing/2;
var w = size.width + leftGroupBorder;
var x = childBounds[i].x - w;
var y = layoutBounds.y - this.parentBorder;
newGeo.x += x;
newGeo.y = y;
newGeo.width = childBounds[i].width + w + this.interRankCellSpacing/2;
newGeo.height = layoutBounds.height + size.height + 2 * this.parentBorder;
this.graph.model.setGeometry(lane, newGeo);
this.graph.moveCells(children, -x, geo.y - y);
}
}
};
/**
* Function: findRoots
*
* Returns all visible children in the given parent which do not have
* incoming edges. If the result is empty then the children with the
* maximum difference between incoming and outgoing edges are returned.
* This takes into account edges that are being promoted to the given
* root due to invisible children or collapsed cells.
*
* Parameters:
*
* parent - <mxCell> whose children should be checked.
* vertices - array of vertices to limit search to
*/
mxSwimlaneLayout.prototype.findRoots = function(parent, vertices)
{
var roots = [];
if (parent != null && vertices != null)
{
var model = this.graph.model;
var best = null;
var maxDiff = -100000;
for (var i in vertices)
{
var cell = vertices[i];
if (cell != null && model.isVertex(cell) && this.graph.isCellVisible(cell) && model.isAncestor(parent, cell))
{
var conns = this.getEdges(cell);
var fanOut = 0;
var fanIn = 0;
for (var k = 0; k < conns.length; k++)
{
var src = this.getVisibleTerminal(conns[k], true);
if (src == cell)
{
// Only count connection within this swimlane
var other = this.getVisibleTerminal(conns[k], false);
if (model.isAncestor(parent, other))
{
fanOut++;
}
}
else if (model.isAncestor(parent, src))
{
fanIn++;
}
}
if (fanIn == 0 && fanOut > 0)
{
roots.push(cell);
}
var diff = fanOut - fanIn;
if (diff > maxDiff)
{
maxDiff = diff;
best = cell;
}
}
}
if (roots.length == 0 && best != null)
{
roots.push(best);
}
}
return roots;
};
/**
* Function: getEdges
*
* Returns the connected edges for the given cell.
*
* Parameters:
*
* cell - <mxCell> whose edges should be returned.
*/
mxSwimlaneLayout.prototype.getEdges = function(cell)
{
var cachedEdges = this.edgesCache.get(cell);
if (cachedEdges != null)
{
return cachedEdges;
}
var model = this.graph.model;
var edges = [];
var isCollapsed = this.graph.isCellCollapsed(cell);
var childCount = model.getChildCount(cell);
for (var i = 0; i < childCount; i++)
{
var child = model.getChildAt(cell, i);
if (this.isPort(child))
{
edges = edges.concat(model.getEdges(child, true, true));
}
else if (isCollapsed || !this.graph.isCellVisible(child))
{
edges = edges.concat(model.getEdges(child, true, true));
}
}
edges = edges.concat(model.getEdges(cell, true, true));
var result = [];
for (var i = 0; i < edges.length; i++)
{
var source = this.getVisibleTerminal(edges[i], true);
var target = this.getVisibleTerminal(edges[i], false);
if ((source == target) || ((source != target) && ((target == cell && (this.parent == null || this.graph.isValidAncestor(source, this.parent, this.traverseAncestors))) ||
(source == cell && (this.parent == null ||
this.graph.isValidAncestor(target, this.parent, this.traverseAncestors))))))
{
result.push(edges[i]);
}
}
this.edgesCache.put(cell, result);
return result;
};
/**
* Function: getVisibleTerminal
*
* Helper function to return visible terminal for edge allowing for ports
*
* Parameters:
*
* edge - <mxCell> whose edges should be returned.
* source - Boolean that specifies whether the source or target terminal is to be returned
*/
mxSwimlaneLayout.prototype.getVisibleTerminal = function(edge, source)
{
var terminalCache = this.edgesTargetTermCache;
if (source)
{
terminalCache = this.edgeSourceTermCache;
}
var term = terminalCache.get(edge);
if (term != null)
{
return term;
}
var state = this.graph.view.getState(edge);
var terminal = (state != null) ? state.getVisibleTerminal(source) : this.graph.view.getVisibleTerminal(edge, source);
if (terminal == null)
{
terminal = (state != null) ? state.getVisibleTerminal(source) : this.graph.view.getVisibleTerminal(edge, source);
}
if (terminal != null)
{
if (this.isPort(terminal))
{
terminal = this.graph.model.getParent(terminal);
}
terminalCache.put(edge, terminal);
}
return terminal;
};
/**
* Function: run
*
* The API method used to exercise the layout upon the graph description
* and produce a separate description of the vertex position and edge
* routing changes made. It runs each stage of the layout that has been
* created.
*/
mxSwimlaneLayout.prototype.run = function(parent)
{
// Separate out unconnected hierarchies
var hierarchyVertices = [];
var allVertexSet = Object();
if (this.swimlanes != null && this.swimlanes.length > 0 && parent != null)
{
var filledVertexSet = Object();
for (var i = 0; i < this.swimlanes.length; i++)
{
this.filterDescendants(this.swimlanes[i], filledVertexSet);
}
this.roots = [];
var filledVertexSetEmpty = true;
// Poor man's isSetEmpty
for (var key in filledVertexSet)
{
if (filledVertexSet[key] != null)
{
filledVertexSetEmpty = false;
break;
}
}
// Only test for candidates in each swimlane in order
var laneCounter = 0;
while (!filledVertexSetEmpty && laneCounter < this.swimlanes.length)
{
var candidateRoots = this.findRoots(this.swimlanes[laneCounter], filledVertexSet);
if (candidateRoots.length == 0)
{
laneCounter++;
continue;
}
// If the candidate root is an unconnected group cell, remove it from
// the layout. We may need a custom set that holds such groups and forces
// them to be processed for resizing and/or moving.
for (var i = 0; i < candidateRoots.length; i++)
{
var vertexSet = Object();
hierarchyVertices.push(vertexSet);
this.traverse(candidateRoots[i], true, null, allVertexSet, vertexSet,
hierarchyVertices, filledVertexSet, laneCounter);
}
for (var i = 0; i < candidateRoots.length; i++)
{
this.roots.push(candidateRoots[i]);
}
filledVertexSetEmpty = true;
// Poor man's isSetEmpty
for (var key in filledVertexSet)
{
if (filledVertexSet[key] != null)
{
filledVertexSetEmpty = false;
break;
}
}
}
}
else
{
// Find vertex set as directed traversal from roots
for (var i = 0; i < this.roots.length; i++)
{
var vertexSet = Object();
hierarchyVertices.push(vertexSet);
this.traverse(this.roots[i], true, null, allVertexSet, vertexSet,
hierarchyVertices, null);
}
}
var tmp = [];
for (var key in allVertexSet)
{
tmp.push(allVertexSet[key]);
}
this.model = new mxSwimlaneModel(this, tmp, this.roots,
parent, this.tightenToSource);
this.cycleStage(parent);
this.layeringStage();
this.crossingStage(parent);
this.placementStage(0, parent);
};
/**
* Function: filterDescendants
*
* Creates an array of descendant cells
*/
mxSwimlaneLayout.prototype.filterDescendants = function(cell, result)
{
var model = this.graph.model;
if (model.isVertex(cell) && cell != this.parent && model.getParent(cell) != this.parent && this.graph.isCellVisible(cell))
{
result[mxObjectIdentity.get(cell)] = cell;
}
if (this.traverseAncestors || cell == this.parent
&& this.graph.isCellVisible(cell))
{
var childCount = model.getChildCount(cell);
for (var i = 0; i < childCount; i++)
{
var child = model.getChildAt(cell, i);
// Ignore ports in the layout vertex list, they are dealt with
// in the traversal mechanisms
if (!this.isPort(child))
{
this.filterDescendants(child, result);
}
}
}
};
/**
* Function: isPort
*
* Returns true if the given cell is a "port", that is, when connecting to
* it, its parent is the connecting vertex in terms of graph traversal
*
* Parameters:
*
* cell - <mxCell> that represents the port.
*/
mxSwimlaneLayout.prototype.isPort = function(cell)
{
if (cell.geometry.relative)
{
return true;
}
return false;
};
/**
* Function: getEdgesBetween
*
* Returns the edges between the given source and target. This takes into
* account collapsed and invisible cells and ports.
*
* Parameters:
*
* source -
* target -
* directed -
*/
mxSwimlaneLayout.prototype.getEdgesBetween = function(source, target, directed)
{
directed = (directed != null) ? directed : false;
var edges = this.getEdges(source);
var result = [];
// Checks if the edge is connected to the correct
// cell and returns the first match
for (var i = 0; i < edges.length; i++)
{
var src = this.getVisibleTerminal(edges[i], true);
var trg = this.getVisibleTerminal(edges[i], false);
if ((src == source && trg == target) || (!directed && src == target && trg == source))
{
result.push(edges[i]);
}
}
return result;
};
/**
* Traverses the (directed) graph invoking the given function for each
* visited vertex and edge. The function is invoked with the current vertex
* and the incoming edge as a parameter. This implementation makes sure
* each vertex is only visited once. The function may return false if the
* traversal should stop at the given vertex.
*
* Parameters:
*
* vertex - <mxCell> that represents the vertex where the traversal starts.
* directed - boolean indicating if edges should only be traversed
* from source to target. Default is true.
* edge - Optional <mxCell> that represents the incoming edge. This is
* null for the first step of the traversal.
* allVertices - Array of cell paths for the visited cells.
* swimlaneIndex - the laid out order index of the swimlane vertex is contained in
*/
mxSwimlaneLayout.prototype.traverse = function(vertex, directed, edge, allVertices, currentComp,
hierarchyVertices, filledVertexSet, swimlaneIndex)
{
if (vertex != null && allVertices != null)
{
// Has this vertex been seen before in any traversal
// And if the filled vertex set is populated, only
// process vertices in that it contains
var vertexID = mxObjectIdentity.get(vertex);
if ((allVertices[vertexID] == null)
&& (filledVertexSet == null ? true : filledVertexSet[vertexID] != null))
{
if (currentComp[vertexID] == null)
{
currentComp[vertexID] = vertex;
}
if (allVertices[vertexID] == null)
{
allVertices[vertexID] = vertex;
}
if (filledVertexSet !== null)
{
delete filledVertexSet[vertexID];
}
var edges = this.getEdges(vertex);
var model = this.graph.model;
for (var i = 0; i < edges.length; i++)
{
var otherVertex = this.getVisibleTerminal(edges[i], true);
var isSource = otherVertex == vertex;
if (isSource)
{
otherVertex = this.getVisibleTerminal(edges[i], false);
}
var otherIndex = 0;
// Get the swimlane index of the other terminal
for (otherIndex = 0; otherIndex < this.swimlanes.length; otherIndex++)
{
if (model.isAncestor(this.swimlanes[otherIndex], otherVertex))
{
break;
}
}
if (otherIndex >= this.swimlanes.length)
{
continue;
}
// Traverse if the other vertex is within the same swimlane as
// as the current vertex, or if the swimlane index of the other
// vertex is greater than that of this vertex
if ((otherIndex > swimlaneIndex) ||
((!directed || isSource) && otherIndex == swimlaneIndex))
{
currentComp = this.traverse(otherVertex, directed, edges[i], allVertices,
currentComp, hierarchyVertices,
filledVertexSet, otherIndex);
}
}
}
else
{
if (currentComp[vertexID] == null)
{
// We've seen this vertex before, but not in the current component
// This component and the one it's in need to be merged
for (var i = 0; i < hierarchyVertices.length; i++)
{
var comp = hierarchyVertices[i];
if (comp[vertexID] != null)
{
for (var key in comp)
{
currentComp[key] = comp[key];
}
// Remove the current component from the hierarchy set
hierarchyVertices.splice(i, 1);
return currentComp;
}
}
}
}
}
return currentComp;
};
/**
* Function: cycleStage
*
* Executes the cycle stage using mxMinimumCycleRemover.
*/
mxSwimlaneLayout.prototype.cycleStage = function(parent)
{
var cycleStage = new mxSwimlaneOrdering(this);
cycleStage.execute(parent);
};
/**
* Function: layeringStage
*
* Implements first stage of a Sugiyama layout.
*/
mxSwimlaneLayout.prototype.layeringStage = function()
{
this.model.initialRank();
this.model.fixRanks();
};
/**
* Function: crossingStage
*
* Executes the crossing stage using mxMedianHybridCrossingReduction.
*/
mxSwimlaneLayout.prototype.crossingStage = function(parent)
{
var crossingStage = new mxMedianHybridCrossingReduction(this);
crossingStage.execute(parent);
};
/**
* Function: placementStage
*
* Executes the placement stage using mxCoordinateAssignment.
*/
mxSwimlaneLayout.prototype.placementStage = function(initialX, parent)
{
var placementStage = new mxCoordinateAssignment(this, this.intraCellSpacing,
this.interRankCellSpacing, this.orientation, initialX,
this.parallelEdgeSpacing);
placementStage.fineTuning = this.fineTuning;
placementStage.execute(parent);
return placementStage.limitX + this.interHierarchySpacing;
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,25 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxHierarchicalLayoutStage
*
* The specific layout interface for hierarchical layouts. It adds a
* <code>run</code> method with a parameter for the hierarchical layout model
* that is shared between the layout stages.
*
* Constructor: mxHierarchicalLayoutStage
*
* Constructs a new hierarchical layout stage.
*/
function mxHierarchicalLayoutStage() { };
/**
* Function: execute
*
* Takes the graph detail and configuration information within the facade
* and creates the resulting laid out graph within that facade for further
* use.
*/
mxHierarchicalLayoutStage.prototype.execute = function(parent) { };

View File

@ -0,0 +1,675 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxMedianHybridCrossingReduction
*
* Sets the horizontal locations of node and edge dummy nodes on each layer.
* Uses median down and up weighings as well heuristic to straighten edges as
* far as possible.
*
* Constructor: mxMedianHybridCrossingReduction
*
* Creates a coordinate assignment.
*
* Arguments:
*
* intraCellSpacing - the minimum buffer between cells on the same rank
* interRankCellSpacing - the minimum distance between cells on adjacent ranks
* orientation - the position of the root node(s) relative to the graph
* initialX - the leftmost coordinate node placement starts at
*/
function mxMedianHybridCrossingReduction(layout)
{
this.layout = layout;
};
/**
* Extends mxMedianHybridCrossingReduction.
*/
mxMedianHybridCrossingReduction.prototype = new mxHierarchicalLayoutStage();
mxMedianHybridCrossingReduction.prototype.constructor = mxMedianHybridCrossingReduction;
/**
* Variable: layout
*
* Reference to the enclosing <mxHierarchicalLayout>.
*/
mxMedianHybridCrossingReduction.prototype.layout = null;
/**
* Variable: maxIterations
*
* The maximum number of iterations to perform whilst reducing edge
* crossings. Default is 24.
*/
mxMedianHybridCrossingReduction.prototype.maxIterations = 24;
/**
* Variable: nestedBestRanks
*
* Stores each rank as a collection of cells in the best order found for
* each layer so far
*/
mxMedianHybridCrossingReduction.prototype.nestedBestRanks = null;
/**
* Variable: currentBestCrossings
*
* The total number of crossings found in the best configuration so far
*/
mxMedianHybridCrossingReduction.prototype.currentBestCrossings = 0;
/**
* Variable: iterationsWithoutImprovement
*
* The total number of crossings found in the best configuration so far
*/
mxMedianHybridCrossingReduction.prototype.iterationsWithoutImprovement = 0;
/**
* Variable: maxNoImprovementIterations
*
* The total number of crossings found in the best configuration so far
*/
mxMedianHybridCrossingReduction.prototype.maxNoImprovementIterations = 2;
/**
* Function: execute
*
* Performs a vertex ordering within ranks as described by Gansner et al
* 1993
*/
mxMedianHybridCrossingReduction.prototype.execute = function(parent)
{
var model = this.layout.getModel();
// Stores initial ordering as being the best one found so far
this.nestedBestRanks = [];
for (var i = 0; i < model.ranks.length; i++)
{
this.nestedBestRanks[i] = model.ranks[i].slice();
}
var iterationsWithoutImprovement = 0;
var currentBestCrossings = this.calculateCrossings(model);
for (var i = 0; i < this.maxIterations &&
iterationsWithoutImprovement < this.maxNoImprovementIterations; i++)
{
this.weightedMedian(i, model);
this.transpose(i, model);
var candidateCrossings = this.calculateCrossings(model);
if (candidateCrossings < currentBestCrossings)
{
currentBestCrossings = candidateCrossings;
iterationsWithoutImprovement = 0;
// Store the current rankings as the best ones
for (var j = 0; j < this.nestedBestRanks.length; j++)
{
var rank = model.ranks[j];
for (var k = 0; k < rank.length; k++)
{
var cell = rank[k];
this.nestedBestRanks[j][cell.getGeneralPurposeVariable(j)] = cell;
}
}
}
else
{
// Increase count of iterations where we haven't improved the
// layout
iterationsWithoutImprovement++;
// Restore the best values to the cells
for (var j = 0; j < this.nestedBestRanks.length; j++)
{
var rank = model.ranks[j];
for (var k = 0; k < rank.length; k++)
{
var cell = rank[k];
cell.setGeneralPurposeVariable(j, k);
}
}
}
if (currentBestCrossings == 0)
{
// Do nothing further
break;
}
}
// Store the best rankings but in the model
var ranks = [];
var rankList = [];
for (var i = 0; i < model.maxRank + 1; i++)
{
rankList[i] = [];
ranks[i] = rankList[i];
}
for (var i = 0; i < this.nestedBestRanks.length; i++)
{
for (var j = 0; j < this.nestedBestRanks[i].length; j++)
{
rankList[i].push(this.nestedBestRanks[i][j]);
}
}
model.ranks = ranks;
};
/**
* Function: calculateCrossings
*
* Calculates the total number of edge crossing in the current graph.
* Returns the current number of edge crossings in the hierarchy graph
* model in the current candidate layout
*
* Parameters:
*
* model - the internal model describing the hierarchy
*/
mxMedianHybridCrossingReduction.prototype.calculateCrossings = function(model)
{
var numRanks = model.ranks.length;
var totalCrossings = 0;
for (var i = 1; i < numRanks; i++)
{
totalCrossings += this.calculateRankCrossing(i, model);
}
return totalCrossings;
};
/**
* Function: calculateRankCrossing
*
* Calculates the number of edges crossings between the specified rank and
* the rank below it. Returns the number of edges crossings with the rank
* beneath
*
* Parameters:
*
* i - the topmost rank of the pair ( higher rank value )
* model - the internal model describing the hierarchy
*/
mxMedianHybridCrossingReduction.prototype.calculateRankCrossing = function(i, model)
{
var totalCrossings = 0;
var rank = model.ranks[i];
var previousRank = model.ranks[i - 1];
var tmpIndices = [];
// Iterate over the top rank and fill in the connection information
for (var j = 0; j < rank.length; j++)
{
var node = rank[j];
var rankPosition = node.getGeneralPurposeVariable(i);
var connectedCells = node.getPreviousLayerConnectedCells(i);
var nodeIndices = [];
for (var k = 0; k < connectedCells.length; k++)
{
var connectedNode = connectedCells[k];
var otherCellRankPosition = connectedNode.getGeneralPurposeVariable(i - 1);
nodeIndices.push(otherCellRankPosition);
}
nodeIndices.sort(function(x, y) { return x - y; });
tmpIndices[rankPosition] = nodeIndices;
}
var indices = [];
for (var j = 0; j < tmpIndices.length; j++)
{
indices = indices.concat(tmpIndices[j]);
}
var firstIndex = 1;
while (firstIndex < previousRank.length)
{
firstIndex <<= 1;
}
var treeSize = 2 * firstIndex - 1;
firstIndex -= 1;
var tree = [];
for (var j = 0; j < treeSize; ++j)
{
tree[j] = 0;
}
for (var j = 0; j < indices.length; j++)
{
var index = indices[j];
var treeIndex = index + firstIndex;
++tree[treeIndex];
while (treeIndex > 0)
{
if (treeIndex % 2)
{
totalCrossings += tree[treeIndex + 1];
}
treeIndex = (treeIndex - 1) >> 1;
++tree[treeIndex];
}
}
return totalCrossings;
};
/**
* Function: transpose
*
* Takes each possible adjacent cell pair on each rank and checks if
* swapping them around reduces the number of crossing
*
* Parameters:
*
* mainLoopIteration - the iteration number of the main loop
* model - the internal model describing the hierarchy
*/
mxMedianHybridCrossingReduction.prototype.transpose = function(mainLoopIteration, model)
{
var improved = true;
// Track the number of iterations in case of looping
var count = 0;
var maxCount = 10;
while (improved && count++ < maxCount)
{
// On certain iterations allow allow swapping of cell pairs with
// equal edge crossings switched or not switched. This help to
// nudge a stuck layout into a lower crossing total.
var nudge = mainLoopIteration % 2 == 1 && count % 2 == 1;
improved = false;
for (var i = 0; i < model.ranks.length; i++)
{
var rank = model.ranks[i];
var orderedCells = [];
for (var j = 0; j < rank.length; j++)
{
var cell = rank[j];
var tempRank = cell.getGeneralPurposeVariable(i);
// FIXME: Workaround to avoid negative tempRanks
if (tempRank < 0)
{
tempRank = j;
}
orderedCells[tempRank] = cell;
}
var leftCellAboveConnections = null;
var leftCellBelowConnections = null;
var rightCellAboveConnections = null;
var rightCellBelowConnections = null;
var leftAbovePositions = null;
var leftBelowPositions = null;
var rightAbovePositions = null;
var rightBelowPositions = null;
var leftCell = null;
var rightCell = null;
for (var j = 0; j < (rank.length - 1); j++)
{
// For each intra-rank adjacent pair of cells
// see if swapping them around would reduce the
// number of edges crossing they cause in total
// On every cell pair except the first on each rank, we
// can save processing using the previous values for the
// right cell on the new left cell
if (j == 0)
{
leftCell = orderedCells[j];
leftCellAboveConnections = leftCell
.getNextLayerConnectedCells(i);
leftCellBelowConnections = leftCell
.getPreviousLayerConnectedCells(i);
leftAbovePositions = [];
leftBelowPositions = [];
for (var k = 0; k < leftCellAboveConnections.length; k++)
{
leftAbovePositions[k] = leftCellAboveConnections[k].getGeneralPurposeVariable(i + 1);
}
for (var k = 0; k < leftCellBelowConnections.length; k++)
{
leftBelowPositions[k] = leftCellBelowConnections[k].getGeneralPurposeVariable(i - 1);
}
}
else
{
leftCellAboveConnections = rightCellAboveConnections;
leftCellBelowConnections = rightCellBelowConnections;
leftAbovePositions = rightAbovePositions;
leftBelowPositions = rightBelowPositions;
leftCell = rightCell;
}
rightCell = orderedCells[j + 1];
rightCellAboveConnections = rightCell
.getNextLayerConnectedCells(i);
rightCellBelowConnections = rightCell
.getPreviousLayerConnectedCells(i);
rightAbovePositions = [];
rightBelowPositions = [];
for (var k = 0; k < rightCellAboveConnections.length; k++)
{
rightAbovePositions[k] = rightCellAboveConnections[k].getGeneralPurposeVariable(i + 1);
}
for (var k = 0; k < rightCellBelowConnections.length; k++)
{
rightBelowPositions[k] = rightCellBelowConnections[k].getGeneralPurposeVariable(i - 1);
}
var totalCurrentCrossings = 0;
var totalSwitchedCrossings = 0;
for (var k = 0; k < leftAbovePositions.length; k++)
{
for (var ik = 0; ik < rightAbovePositions.length; ik++)
{
if (leftAbovePositions[k] > rightAbovePositions[ik])
{
totalCurrentCrossings++;
}
if (leftAbovePositions[k] < rightAbovePositions[ik])
{
totalSwitchedCrossings++;
}
}
}
for (var k = 0; k < leftBelowPositions.length; k++)
{
for (var ik = 0; ik < rightBelowPositions.length; ik++)
{
if (leftBelowPositions[k] > rightBelowPositions[ik])
{
totalCurrentCrossings++;
}
if (leftBelowPositions[k] < rightBelowPositions[ik])
{
totalSwitchedCrossings++;
}
}
}
if ((totalSwitchedCrossings < totalCurrentCrossings) ||
(totalSwitchedCrossings == totalCurrentCrossings &&
nudge))
{
var temp = leftCell.getGeneralPurposeVariable(i);
leftCell.setGeneralPurposeVariable(i, rightCell
.getGeneralPurposeVariable(i));
rightCell.setGeneralPurposeVariable(i, temp);
// With this pair exchanged we have to switch all of
// values for the left cell to the right cell so the
// next iteration for this rank uses it as the left
// cell again
rightCellAboveConnections = leftCellAboveConnections;
rightCellBelowConnections = leftCellBelowConnections;
rightAbovePositions = leftAbovePositions;
rightBelowPositions = leftBelowPositions;
rightCell = leftCell;
if (!nudge)
{
// Don't count nudges as improvement or we'll end
// up stuck in two combinations and not finishing
// as early as we should
improved = true;
}
}
}
}
}
};
/**
* Function: weightedMedian
*
* Sweeps up or down the layout attempting to minimise the median placement
* of connected cells on adjacent ranks
*
* Parameters:
*
* iteration - the iteration number of the main loop
* model - the internal model describing the hierarchy
*/
mxMedianHybridCrossingReduction.prototype.weightedMedian = function(iteration, model)
{
// Reverse sweep direction each time through this method
var downwardSweep = (iteration % 2 == 0);
if (downwardSweep)
{
for (var j = model.maxRank - 1; j >= 0; j--)
{
this.medianRank(j, downwardSweep);
}
}
else
{
for (var j = 1; j < model.maxRank; j++)
{
this.medianRank(j, downwardSweep);
}
}
};
/**
* Function: medianRank
*
* Attempts to minimise the median placement of connected cells on this rank
* and one of the adjacent ranks
*
* Parameters:
*
* rankValue - the layer number of this rank
* downwardSweep - whether or not this is a downward sweep through the graph
*/
mxMedianHybridCrossingReduction.prototype.medianRank = function(rankValue, downwardSweep)
{
var numCellsForRank = this.nestedBestRanks[rankValue].length;
var medianValues = [];
var reservedPositions = [];
for (var i = 0; i < numCellsForRank; i++)
{
var cell = this.nestedBestRanks[rankValue][i];
var sorterEntry = new MedianCellSorter();
sorterEntry.cell = cell;
// Flip whether or not equal medians are flipped on up and down
// sweeps
// TODO re-implement some kind of nudge
// medianValues[i].nudge = !downwardSweep;
var nextLevelConnectedCells;
if (downwardSweep)
{
nextLevelConnectedCells = cell
.getNextLayerConnectedCells(rankValue);
}
else
{
nextLevelConnectedCells = cell
.getPreviousLayerConnectedCells(rankValue);
}
var nextRankValue;
if (downwardSweep)
{
nextRankValue = rankValue + 1;
}
else
{
nextRankValue = rankValue - 1;
}
if (nextLevelConnectedCells != null
&& nextLevelConnectedCells.length != 0)
{
sorterEntry.medianValue = this.medianValue(
nextLevelConnectedCells, nextRankValue);
medianValues.push(sorterEntry);
}
else
{
// Nodes with no adjacent vertices are flagged in the reserved array
// to indicate they should be left in their current position.
reservedPositions[cell.getGeneralPurposeVariable(rankValue)] = true;
}
}
medianValues.sort(MedianCellSorter.prototype.compare);
// Set the new position of each node within the rank using
// its temp variable
for (var i = 0; i < numCellsForRank; i++)
{
if (reservedPositions[i] == null)
{
var cell = medianValues.shift().cell;
cell.setGeneralPurposeVariable(rankValue, i);
}
}
};
/**
* Function: medianValue
*
* Calculates the median rank order positioning for the specified cell using
* the connected cells on the specified rank. Returns the median rank
* ordering value of the connected cells
*
* Parameters:
*
* connectedCells - the cells on the specified rank connected to the
* specified cell
* rankValue - the rank that the connected cell lie upon
*/
mxMedianHybridCrossingReduction.prototype.medianValue = function(connectedCells, rankValue)
{
var medianValues = [];
var arrayCount = 0;
for (var i = 0; i < connectedCells.length; i++)
{
var cell = connectedCells[i];
medianValues[arrayCount++] = cell.getGeneralPurposeVariable(rankValue);
}
// Sort() sorts lexicographically by default (i.e. 11 before 9) so force
// numerical order sort
medianValues.sort(function(a,b){return a - b;});
if (arrayCount % 2 == 1)
{
// For odd numbers of adjacent vertices return the median
return medianValues[Math.floor(arrayCount / 2)];
}
else if (arrayCount == 2)
{
return ((medianValues[0] + medianValues[1]) / 2.0);
}
else
{
var medianPoint = arrayCount / 2;
var leftMedian = medianValues[medianPoint - 1] - medianValues[0];
var rightMedian = medianValues[arrayCount - 1]
- medianValues[medianPoint];
return (medianValues[medianPoint - 1] * rightMedian + medianValues[medianPoint]
* leftMedian)
/ (leftMedian + rightMedian);
}
};
/**
* Class: MedianCellSorter
*
* A utility class used to track cells whilst sorting occurs on the median
* values. Does not violate (x.compareTo(y)==0) == (x.equals(y))
*
* Constructor: MedianCellSorter
*
* Constructs a new median cell sorter.
*/
function MedianCellSorter()
{
// empty
};
/**
* Variable: medianValue
*
* The weighted value of the cell stored.
*/
MedianCellSorter.prototype.medianValue = 0;
/**
* Variable: cell
*
* The cell whose median value is being calculated
*/
MedianCellSorter.prototype.cell = false;
/**
* Function: compare
*
* Compares two MedianCellSorters.
*/
MedianCellSorter.prototype.compare = function(a, b)
{
if (a != null && b != null)
{
if (b.medianValue > a.medianValue)
{
return -1;
}
else if (b.medianValue < a.medianValue)
{
return 1;
}
else
{
return 0;
}
}
else
{
return 0;
}
};

View File

@ -0,0 +1,108 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxMinimumCycleRemover
*
* An implementation of the first stage of the Sugiyama layout. Straightforward
* longest path calculation of layer assignment
*
* Constructor: mxMinimumCycleRemover
*
* Creates a cycle remover for the given internal model.
*/
function mxMinimumCycleRemover(layout)
{
this.layout = layout;
};
/**
* Extends mxHierarchicalLayoutStage.
*/
mxMinimumCycleRemover.prototype = new mxHierarchicalLayoutStage();
mxMinimumCycleRemover.prototype.constructor = mxMinimumCycleRemover;
/**
* Variable: layout
*
* Reference to the enclosing <mxHierarchicalLayout>.
*/
mxMinimumCycleRemover.prototype.layout = null;
/**
* Function: execute
*
* Takes the graph detail and configuration information within the facade
* and creates the resulting laid out graph within that facade for further
* use.
*/
mxMinimumCycleRemover.prototype.execute = function(parent)
{
var model = this.layout.getModel();
var seenNodes = new Object();
var unseenNodesArray = model.vertexMapper.getValues();
var unseenNodes = new Object();
for (var i = 0; i < unseenNodesArray.length; i++)
{
unseenNodes[unseenNodesArray[i].id] = unseenNodesArray[i];
}
// Perform a dfs through the internal model. If a cycle is found,
// reverse it.
var rootsArray = null;
if (model.roots != null)
{
var modelRoots = model.roots;
rootsArray = [];
for (var i = 0; i < modelRoots.length; i++)
{
rootsArray[i] = model.vertexMapper.get(modelRoots[i]);
}
}
model.visit(function(parent, node, connectingEdge, layer, seen)
{
// Check if the cell is in it's own ancestor list, if so
// invert the connecting edge and reverse the target/source
// relationship to that edge in the parent and the cell
if (node.isAncestor(parent))
{
connectingEdge.invert();
mxUtils.remove(connectingEdge, parent.connectsAsSource);
parent.connectsAsTarget.push(connectingEdge);
mxUtils.remove(connectingEdge, node.connectsAsTarget);
node.connectsAsSource.push(connectingEdge);
}
seenNodes[node.id] = node;
delete unseenNodes[node.id];
}, rootsArray, true, null);
// If there are any nodes that should be nodes that the dfs can miss
// these need to be processed with the dfs and the roots assigned
// correctly to form a correct internal model
var seenNodesCopy = mxUtils.clone(seenNodes, null, true);
// Pick a random cell and dfs from it
model.visit(function(parent, node, connectingEdge, layer, seen)
{
// Check if the cell is in it's own ancestor list, if so
// invert the connecting edge and reverse the target/source
// relationship to that edge in the parent and the cell
if (node.isAncestor(parent))
{
connectingEdge.invert();
mxUtils.remove(connectingEdge, parent.connectsAsSource);
node.connectsAsSource.push(connectingEdge);
parent.connectsAsTarget.push(connectingEdge);
mxUtils.remove(connectingEdge, node.connectsAsTarget);
}
seenNodes[node.id] = node;
delete unseenNodes[node.id];
}, unseenNodes, true, seenNodesCopy);
};

View File

@ -0,0 +1,95 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxSwimlaneOrdering
*
* An implementation of the first stage of the Sugiyama layout. Straightforward
* longest path calculation of layer assignment
*
* Constructor: mxSwimlaneOrdering
*
* Creates a cycle remover for the given internal model.
*/
function mxSwimlaneOrdering(layout)
{
this.layout = layout;
};
/**
* Extends mxHierarchicalLayoutStage.
*/
mxSwimlaneOrdering.prototype = new mxHierarchicalLayoutStage();
mxSwimlaneOrdering.prototype.constructor = mxSwimlaneOrdering;
/**
* Variable: layout
*
* Reference to the enclosing <mxHierarchicalLayout>.
*/
mxSwimlaneOrdering.prototype.layout = null;
/**
* Function: execute
*
* Takes the graph detail and configuration information within the facade
* and creates the resulting laid out graph within that facade for further
* use.
*/
mxSwimlaneOrdering.prototype.execute = function(parent)
{
var model = this.layout.getModel();
var seenNodes = new Object();
var unseenNodes = mxUtils.clone(model.vertexMapper, null, true);
// Perform a dfs through the internal model. If a cycle is found,
// reverse it.
var rootsArray = null;
if (model.roots != null)
{
var modelRoots = model.roots;
rootsArray = [];
for (var i = 0; i < modelRoots.length; i++)
{
rootsArray[i] = model.vertexMapper.get(modelRoots[i]);
}
}
model.visit(function(parent, node, connectingEdge, layer, seen)
{
// Check if the cell is in it's own ancestor list, if so
// invert the connecting edge and reverse the target/source
// relationship to that edge in the parent and the cell
// Ancestor hashes only line up within a swimlane
var isAncestor = parent != null && parent.swimlaneIndex == node.swimlaneIndex && node.isAncestor(parent);
// If the source->target swimlane indices go from higher to
// lower, the edge is reverse
var reversedOverSwimlane = parent != null && connectingEdge != null &&
parent.swimlaneIndex < node.swimlaneIndex && connectingEdge.source == node;
if (isAncestor)
{
connectingEdge.invert();
mxUtils.remove(connectingEdge, parent.connectsAsSource);
node.connectsAsSource.push(connectingEdge);
parent.connectsAsTarget.push(connectingEdge);
mxUtils.remove(connectingEdge, node.connectsAsTarget);
}
else if (reversedOverSwimlane)
{
connectingEdge.invert();
mxUtils.remove(connectingEdge, parent.connectsAsTarget);
node.connectsAsTarget.push(connectingEdge);
parent.connectsAsSource.push(connectingEdge);
mxUtils.remove(connectingEdge, node.connectsAsSource);
}
var cellId = mxCellPath.create(node.cell);
seenNodes[cellId] = node;
delete unseenNodes[cellId];
}, rootsArray, true, null);
};

View File

@ -0,0 +1,203 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxCircleLayout
*
* Extends <mxGraphLayout> to implement a circluar layout for a given radius.
* The vertices do not need to be connected for this layout to work and all
* connections between vertices are not taken into account.
*
* Example:
*
* (code)
* var layout = new mxCircleLayout(graph);
* layout.execute(graph.getDefaultParent());
* (end)
*
* Constructor: mxCircleLayout
*
* Constructs a new circular layout for the specified radius.
*
* Arguments:
*
* graph - <mxGraph> that contains the cells.
* radius - Optional radius as an int. Default is 100.
*/
function mxCircleLayout(graph, radius)
{
mxGraphLayout.call(this, graph);
this.radius = (radius != null) ? radius : 100;
};
/**
* Extends mxGraphLayout.
*/
mxCircleLayout.prototype = new mxGraphLayout();
mxCircleLayout.prototype.constructor = mxCircleLayout;
/**
* Variable: radius
*
* Integer specifying the size of the radius. Default is 100.
*/
mxCircleLayout.prototype.radius = null;
/**
* Variable: moveCircle
*
* Boolean specifying if the circle should be moved to the top,
* left corner specified by <x0> and <y0>. Default is false.
*/
mxCircleLayout.prototype.moveCircle = false;
/**
* Variable: x0
*
* Integer specifying the left coordinate of the circle.
* Default is 0.
*/
mxCircleLayout.prototype.x0 = 0;
/**
* Variable: y0
*
* Integer specifying the top coordinate of the circle.
* Default is 0.
*/
mxCircleLayout.prototype.y0 = 0;
/**
* Variable: resetEdges
*
* Specifies if all edge points of traversed edges should be removed.
* Default is true.
*/
mxCircleLayout.prototype.resetEdges = true;
/**
* Variable: disableEdgeStyle
*
* Specifies if the STYLE_NOEDGESTYLE flag should be set on edges that are
* modified by the result. Default is true.
*/
mxCircleLayout.prototype.disableEdgeStyle = true;
/**
* Function: execute
*
* Implements <mxGraphLayout.execute>.
*/
mxCircleLayout.prototype.execute = function(parent)
{
var model = this.graph.getModel();
// Moves the vertices to build a circle. Makes sure the
// radius is large enough for the vertices to not
// overlap
model.beginUpdate();
try
{
// Gets all vertices inside the parent and finds
// the maximum dimension of the largest vertex
var max = 0;
var top = null;
var left = null;
var vertices = [];
var childCount = model.getChildCount(parent);
for (var i = 0; i < childCount; i++)
{
var cell = model.getChildAt(parent, i);
if (!this.isVertexIgnored(cell))
{
vertices.push(cell);
var bounds = this.getVertexBounds(cell);
if (top == null)
{
top = bounds.y;
}
else
{
top = Math.min(top, bounds.y);
}
if (left == null)
{
left = bounds.x;
}
else
{
left = Math.min(left, bounds.x);
}
max = Math.max(max, Math.max(bounds.width, bounds.height));
}
else if (!this.isEdgeIgnored(cell))
{
// Resets the points on the traversed edge
if (this.resetEdges)
{
this.graph.resetEdge(cell);
}
if (this.disableEdgeStyle)
{
this.setEdgeStyleEnabled(cell, false);
}
}
}
var r = this.getRadius(vertices.length, max);
// Moves the circle to the specified origin
if (this.moveCircle)
{
left = this.x0;
top = this.y0;
}
this.circle(vertices, r, left, top);
}
finally
{
model.endUpdate();
}
};
/**
* Function: getRadius
*
* Returns the radius to be used for the given vertex count. Max is the maximum
* width or height of all vertices in the layout.
*/
mxCircleLayout.prototype.getRadius = function(count, max)
{
return Math.max(count * max / Math.PI, this.radius);
};
/**
* Function: circle
*
* Executes the circular layout for the specified array
* of vertices and the given radius. This is called from
* <execute>.
*/
mxCircleLayout.prototype.circle = function(vertices, r, left, top)
{
var vertexCount = vertices.length;
var phi = 2 * Math.PI / vertexCount;
for (var i = 0; i < vertexCount; i++)
{
if (this.isVertexMovable(vertices[i]))
{
this.setVertexLocation(vertices[i],
Math.round(left + r + r * Math.sin(i * phi)),
Math.round(top + r + r * Math.cos(i * phi)));
}
}
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,101 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxCompositeLayout
*
* Allows to compose multiple layouts into a single layout. The master layout
* is the layout that handles move operations if another layout than the first
* element in <layouts> should be used. The <master> layout is not executed as
* the code assumes that it is part of <layouts>.
*
* Example:
* (code)
* var first = new mxFastOrganicLayout(graph);
* var second = new mxParallelEdgeLayout(graph);
* var layout = new mxCompositeLayout(graph, [first, second], first);
* layout.execute(graph.getDefaultParent());
* (end)
*
* Constructor: mxCompositeLayout
*
* Constructs a new layout using the given layouts. The graph instance is
* required for creating the transaction that contains all layouts.
*
* Arguments:
*
* graph - Reference to the enclosing <mxGraph>.
* layouts - Array of <mxGraphLayouts>.
* master - Optional layout that handles moves. If no layout is given then
* the first layout of the above array is used to handle moves.
*/
function mxCompositeLayout(graph, layouts, master)
{
mxGraphLayout.call(this, graph);
this.layouts = layouts;
this.master = master;
};
/**
* Extends mxGraphLayout.
*/
mxCompositeLayout.prototype = new mxGraphLayout();
mxCompositeLayout.prototype.constructor = mxCompositeLayout;
/**
* Variable: layouts
*
* Holds the array of <mxGraphLayouts> that this layout contains.
*/
mxCompositeLayout.prototype.layouts = null;
/**
* Variable: master
*
* Reference to the <mxGraphLayouts> that handles moves. If this is null
* then the first layout in <layouts> is used.
*/
mxCompositeLayout.prototype.master = null;
/**
* Function: moveCell
*
* Implements <mxGraphLayout.moveCell> by calling move on <master> or the first
* layout in <layouts>.
*/
mxCompositeLayout.prototype.moveCell = function(cell, x, y)
{
if (this.master != null)
{
this.master.moveCell.apply(this.master, arguments);
}
else
{
this.layouts[0].moveCell.apply(this.layouts[0], arguments);
}
};
/**
* Function: execute
*
* Implements <mxGraphLayout.execute> by executing all <layouts> in a
* single transaction.
*/
mxCompositeLayout.prototype.execute = function(parent)
{
var model = this.graph.getModel();
model.beginUpdate();
try
{
for (var i = 0; i < this.layouts.length; i++)
{
this.layouts[i].execute.apply(this.layouts[i], arguments);
}
}
finally
{
model.endUpdate();
}
};

View File

@ -0,0 +1,165 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxEdgeLabelLayout
*
* Extends <mxGraphLayout> to implement an edge label layout. This layout
* makes use of cell states, which means the graph must be validated in
* a graph view (so that the label bounds are available) before this layout
* can be executed.
*
* Example:
*
* (code)
* var layout = new mxEdgeLabelLayout(graph);
* layout.execute(graph.getDefaultParent());
* (end)
*
* Constructor: mxEdgeLabelLayout
*
* Constructs a new edge label layout.
*
* Arguments:
*
* graph - <mxGraph> that contains the cells.
*/
function mxEdgeLabelLayout(graph, radius)
{
mxGraphLayout.call(this, graph);
};
/**
* Extends mxGraphLayout.
*/
mxEdgeLabelLayout.prototype = new mxGraphLayout();
mxEdgeLabelLayout.prototype.constructor = mxEdgeLabelLayout;
/**
* Function: execute
*
* Implements <mxGraphLayout.execute>.
*/
mxEdgeLabelLayout.prototype.execute = function(parent)
{
var view = this.graph.view;
var model = this.graph.getModel();
// Gets all vertices and edges inside the parent
var edges = [];
var vertices = [];
var childCount = model.getChildCount(parent);
for (var i = 0; i < childCount; i++)
{
var cell = model.getChildAt(parent, i);
var state = view.getState(cell);
if (state != null)
{
if (!this.isVertexIgnored(cell))
{
vertices.push(state);
}
else if (!this.isEdgeIgnored(cell))
{
edges.push(state);
}
}
}
this.placeLabels(vertices, edges);
};
/**
* Function: placeLabels
*
* Places the labels of the given edges.
*/
mxEdgeLabelLayout.prototype.placeLabels = function(v, e)
{
var model = this.graph.getModel();
// Moves the vertices to build a circle. Makes sure the
// radius is large enough for the vertices to not
// overlap
model.beginUpdate();
try
{
for (var i = 0; i < e.length; i++)
{
var edge = e[i];
if (edge != null && edge.text != null &&
edge.text.boundingBox != null)
{
for (var j = 0; j < v.length; j++)
{
var vertex = v[j];
if (vertex != null)
{
this.avoid(edge, vertex);
}
}
}
}
}
finally
{
model.endUpdate();
}
};
/**
* Function: avoid
*
* Places the labels of the given edges.
*/
mxEdgeLabelLayout.prototype.avoid = function(edge, vertex)
{
var model = this.graph.getModel();
var labRect = edge.text.boundingBox;
if (mxUtils.intersects(labRect, vertex))
{
var dy1 = -labRect.y - labRect.height + vertex.y;
var dy2 = -labRect.y + vertex.y + vertex.height;
var dy = (Math.abs(dy1) < Math.abs(dy2)) ? dy1 : dy2;
var dx1 = -labRect.x - labRect.width + vertex.x;
var dx2 = -labRect.x + vertex.x + vertex.width;
var dx = (Math.abs(dx1) < Math.abs(dx2)) ? dx1 : dx2;
if (Math.abs(dx) < Math.abs(dy))
{
dy = 0;
}
else
{
dx = 0;
}
var g = model.getGeometry(edge.cell);
if (g != null)
{
g = g.clone();
if (g.offset != null)
{
g.offset.x += dx;
g.offset.y += dy;
}
else
{
g.offset = new mxPoint(dx, dy);
}
model.setGeometry(edge.cell, g);
}
}
};

View File

@ -0,0 +1,591 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxFastOrganicLayout
*
* Extends <mxGraphLayout> to implement a fast organic layout algorithm.
* The vertices need to be connected for this layout to work, vertices
* with no connections are ignored.
*
* Example:
*
* (code)
* var layout = new mxFastOrganicLayout(graph);
* layout.execute(graph.getDefaultParent());
* (end)
*
* Constructor: mxCompactTreeLayout
*
* Constructs a new fast organic layout for the specified graph.
*/
function mxFastOrganicLayout(graph)
{
mxGraphLayout.call(this, graph);
};
/**
* Extends mxGraphLayout.
*/
mxFastOrganicLayout.prototype = new mxGraphLayout();
mxFastOrganicLayout.prototype.constructor = mxFastOrganicLayout;
/**
* Variable: useInputOrigin
*
* Specifies if the top left corner of the input cells should be the origin
* of the layout result. Default is true.
*/
mxFastOrganicLayout.prototype.useInputOrigin = true;
/**
* Variable: resetEdges
*
* Specifies if all edge points of traversed edges should be removed.
* Default is true.
*/
mxFastOrganicLayout.prototype.resetEdges = true;
/**
* Variable: disableEdgeStyle
*
* Specifies if the STYLE_NOEDGESTYLE flag should be set on edges that are
* modified by the result. Default is true.
*/
mxFastOrganicLayout.prototype.disableEdgeStyle = true;
/**
* Variable: forceConstant
*
* The force constant by which the attractive forces are divided and the
* replusive forces are multiple by the square of. The value equates to the
* average radius there is of free space around each node. Default is 50.
*/
mxFastOrganicLayout.prototype.forceConstant = 50;
/**
* Variable: forceConstantSquared
*
* Cache of <forceConstant>^2 for performance.
*/
mxFastOrganicLayout.prototype.forceConstantSquared = 0;
/**
* Variable: minDistanceLimit
*
* Minimal distance limit. Default is 2. Prevents of
* dividing by zero.
*/
mxFastOrganicLayout.prototype.minDistanceLimit = 2;
/**
* Variable: maxDistanceLimit
*
* Maximal distance limit. Default is 500. Prevents of
* dividing by zero.
*/
mxFastOrganicLayout.prototype.maxDistanceLimit = 500;
/**
* Variable: minDistanceLimitSquared
*
* Cached version of <minDistanceLimit> squared.
*/
mxFastOrganicLayout.prototype.minDistanceLimitSquared = 4;
/**
* Variable: initialTemp
*
* Start value of temperature. Default is 200.
*/
mxFastOrganicLayout.prototype.initialTemp = 200;
/**
* Variable: temperature
*
* Temperature to limit displacement at later stages of layout.
*/
mxFastOrganicLayout.prototype.temperature = 0;
/**
* Variable: maxIterations
*
* Total number of iterations to run the layout though.
*/
mxFastOrganicLayout.prototype.maxIterations = 0;
/**
* Variable: iteration
*
* Current iteration count.
*/
mxFastOrganicLayout.prototype.iteration = 0;
/**
* Variable: vertexArray
*
* An array of all vertices to be laid out.
*/
mxFastOrganicLayout.prototype.vertexArray;
/**
* Variable: dispX
*
* An array of locally stored X co-ordinate displacements for the vertices.
*/
mxFastOrganicLayout.prototype.dispX;
/**
* Variable: dispY
*
* An array of locally stored Y co-ordinate displacements for the vertices.
*/
mxFastOrganicLayout.prototype.dispY;
/**
* Variable: cellLocation
*
* An array of locally stored co-ordinate positions for the vertices.
*/
mxFastOrganicLayout.prototype.cellLocation;
/**
* Variable: radius
*
* The approximate radius of each cell, nodes only.
*/
mxFastOrganicLayout.prototype.radius;
/**
* Variable: radiusSquared
*
* The approximate radius squared of each cell, nodes only.
*/
mxFastOrganicLayout.prototype.radiusSquared;
/**
* Variable: isMoveable
*
* Array of booleans representing the movable states of the vertices.
*/
mxFastOrganicLayout.prototype.isMoveable;
/**
* Variable: neighbours
*
* Local copy of cell neighbours.
*/
mxFastOrganicLayout.prototype.neighbours;
/**
* Variable: indices
*
* Hashtable from cells to local indices.
*/
mxFastOrganicLayout.prototype.indices;
/**
* Variable: allowedToRun
*
* Boolean flag that specifies if the layout is allowed to run. If this is
* set to false, then the layout exits in the following iteration.
*/
mxFastOrganicLayout.prototype.allowedToRun = true;
/**
* Function: isVertexIgnored
*
* Returns a boolean indicating if the given <mxCell> should be ignored as a
* vertex. This returns true if the cell has no connections.
*
* Parameters:
*
* vertex - <mxCell> whose ignored state should be returned.
*/
mxFastOrganicLayout.prototype.isVertexIgnored = function(vertex)
{
return mxGraphLayout.prototype.isVertexIgnored.apply(this, arguments) ||
this.graph.getConnections(vertex).length == 0;
};
/**
* Function: execute
*
* Implements <mxGraphLayout.execute>. This operates on all children of the
* given parent where <isVertexIgnored> returns false.
*/
mxFastOrganicLayout.prototype.execute = function(parent)
{
var model = this.graph.getModel();
this.vertexArray = [];
var cells = this.graph.getChildVertices(parent);
for (var i = 0; i < cells.length; i++)
{
if (!this.isVertexIgnored(cells[i]))
{
this.vertexArray.push(cells[i]);
}
}
var initialBounds = (this.useInputOrigin) ?
this.graph.getBoundingBoxFromGeometry(this.vertexArray) :
null;
var n = this.vertexArray.length;
this.indices = [];
this.dispX = [];
this.dispY = [];
this.cellLocation = [];
this.isMoveable = [];
this.neighbours = [];
this.radius = [];
this.radiusSquared = [];
if (this.forceConstant < 0.001)
{
this.forceConstant = 0.001;
}
this.forceConstantSquared = this.forceConstant * this.forceConstant;
// Create a map of vertices first. This is required for the array of
// arrays called neighbours which holds, for each vertex, a list of
// ints which represents the neighbours cells to that vertex as
// the indices into vertexArray
for (var i = 0; i < this.vertexArray.length; i++)
{
var vertex = this.vertexArray[i];
this.cellLocation[i] = [];
// Set up the mapping from array indices to cells
var id = mxObjectIdentity.get(vertex);
this.indices[id] = i;
var bounds = this.getVertexBounds(vertex);
// Set the X,Y value of the internal version of the cell to
// the center point of the vertex for better positioning
var width = bounds.width;
var height = bounds.height;
// Randomize (0, 0) locations
var x = bounds.x;
var y = bounds.y;
this.cellLocation[i][0] = x + width / 2.0;
this.cellLocation[i][1] = y + height / 2.0;
this.radius[i] = Math.min(width, height);
this.radiusSquared[i] = this.radius[i] * this.radius[i];
}
// Moves cell location back to top-left from center locations used in
// algorithm, resetting the edge points is part of the transaction
model.beginUpdate();
try
{
for (var i = 0; i < n; i++)
{
this.dispX[i] = 0;
this.dispY[i] = 0;
this.isMoveable[i] = this.isVertexMovable(this.vertexArray[i]);
// Get lists of neighbours to all vertices, translate the cells
// obtained in indices into vertexArray and store as an array
// against the orginial cell index
var edges = this.graph.getConnections(this.vertexArray[i], parent);
var cells = this.graph.getOpposites(edges, this.vertexArray[i]);
this.neighbours[i] = [];
for (var j = 0; j < cells.length; j++)
{
// Resets the points on the traversed edge
if (this.resetEdges)
{
this.graph.resetEdge(edges[j]);
}
if (this.disableEdgeStyle)
{
this.setEdgeStyleEnabled(edges[j], false);
}
// Looks the cell up in the indices dictionary
var id = mxObjectIdentity.get(cells[j]);
var index = this.indices[id];
// Check the connected cell in part of the vertex list to be
// acted on by this layout
if (index != null)
{
this.neighbours[i][j] = index;
}
// Else if index of the other cell doesn't correspond to
// any cell listed to be acted upon in this layout. Set
// the index to the value of this vertex (a dummy self-loop)
// so the attraction force of the edge is not calculated
else
{
this.neighbours[i][j] = i;
}
}
}
this.temperature = this.initialTemp;
// If max number of iterations has not been set, guess it
if (this.maxIterations == 0)
{
this.maxIterations = 20 * Math.sqrt(n);
}
// Main iteration loop
for (this.iteration = 0; this.iteration < this.maxIterations; this.iteration++)
{
if (!this.allowedToRun)
{
return;
}
// Calculate repulsive forces on all vertices
this.calcRepulsion();
// Calculate attractive forces through edges
this.calcAttraction();
this.calcPositions();
this.reduceTemperature();
}
var minx = null;
var miny = null;
for (var i = 0; i < this.vertexArray.length; i++)
{
var vertex = this.vertexArray[i];
if (this.isVertexMovable(vertex))
{
var bounds = this.getVertexBounds(vertex);
if (bounds != null)
{
this.cellLocation[i][0] -= bounds.width / 2.0;
this.cellLocation[i][1] -= bounds.height / 2.0;
var x = this.graph.snap(Math.round(this.cellLocation[i][0]));
var y = this.graph.snap(Math.round(this.cellLocation[i][1]));
this.setVertexLocation(vertex, x, y);
if (minx == null)
{
minx = x;
}
else
{
minx = Math.min(minx, x);
}
if (miny == null)
{
miny = y;
}
else
{
miny = Math.min(miny, y);
}
}
}
}
// Modifies the cloned geometries in-place. Not needed
// to clone the geometries again as we're in the same
// undoable change.
var dx = -(minx || 0) + 1;
var dy = -(miny || 0) + 1;
if (initialBounds != null)
{
dx += initialBounds.x;
dy += initialBounds.y;
}
this.graph.moveCells(this.vertexArray, dx, dy);
}
finally
{
model.endUpdate();
}
};
/**
* Function: calcPositions
*
* Takes the displacements calculated for each cell and applies them to the
* local cache of cell positions. Limits the displacement to the current
* temperature.
*/
mxFastOrganicLayout.prototype.calcPositions = function()
{
for (var index = 0; index < this.vertexArray.length; index++)
{
if (this.isMoveable[index])
{
// Get the distance of displacement for this node for this
// iteration
var deltaLength = Math.sqrt(this.dispX[index] * this.dispX[index] +
this.dispY[index] * this.dispY[index]);
if (deltaLength < 0.001)
{
deltaLength = 0.001;
}
// Scale down by the current temperature if less than the
// displacement distance
var newXDisp = this.dispX[index] / deltaLength
* Math.min(deltaLength, this.temperature);
var newYDisp = this.dispY[index] / deltaLength
* Math.min(deltaLength, this.temperature);
// reset displacements
this.dispX[index] = 0;
this.dispY[index] = 0;
// Update the cached cell locations
this.cellLocation[index][0] += newXDisp;
this.cellLocation[index][1] += newYDisp;
}
}
};
/**
* Function: calcAttraction
*
* Calculates the attractive forces between all laid out nodes linked by
* edges
*/
mxFastOrganicLayout.prototype.calcAttraction = function()
{
// Check the neighbours of each vertex and calculate the attractive
// force of the edge connecting them
for (var i = 0; i < this.vertexArray.length; i++)
{
for (var k = 0; k < this.neighbours[i].length; k++)
{
// Get the index of the othe cell in the vertex array
var j = this.neighbours[i][k];
// Do not proceed self-loops
if (i != j &&
this.isMoveable[i] &&
this.isMoveable[j])
{
var xDelta = this.cellLocation[i][0] - this.cellLocation[j][0];
var yDelta = this.cellLocation[i][1] - this.cellLocation[j][1];
// The distance between the nodes
var deltaLengthSquared = xDelta * xDelta + yDelta
* yDelta - this.radiusSquared[i] - this.radiusSquared[j];
if (deltaLengthSquared < this.minDistanceLimitSquared)
{
deltaLengthSquared = this.minDistanceLimitSquared;
}
var deltaLength = Math.sqrt(deltaLengthSquared);
var force = (deltaLengthSquared) / this.forceConstant;
var displacementX = (xDelta / deltaLength) * force;
var displacementY = (yDelta / deltaLength) * force;
this.dispX[i] -= displacementX;
this.dispY[i] -= displacementY;
this.dispX[j] += displacementX;
this.dispY[j] += displacementY;
}
}
}
};
/**
* Function: calcRepulsion
*
* Calculates the repulsive forces between all laid out nodes
*/
mxFastOrganicLayout.prototype.calcRepulsion = function()
{
var vertexCount = this.vertexArray.length;
for (var i = 0; i < vertexCount; i++)
{
for (var j = i; j < vertexCount; j++)
{
// Exits if the layout is no longer allowed to run
if (!this.allowedToRun)
{
return;
}
if (j != i &&
this.isMoveable[i] &&
this.isMoveable[j])
{
var xDelta = this.cellLocation[i][0] - this.cellLocation[j][0];
var yDelta = this.cellLocation[i][1] - this.cellLocation[j][1];
if (xDelta == 0)
{
xDelta = 0.01 + Math.random();
}
if (yDelta == 0)
{
yDelta = 0.01 + Math.random();
}
// Distance between nodes
var deltaLength = Math.sqrt((xDelta * xDelta)
+ (yDelta * yDelta));
var deltaLengthWithRadius = deltaLength - this.radius[i]
- this.radius[j];
if (deltaLengthWithRadius > this.maxDistanceLimit)
{
// Ignore vertices too far apart
continue;
}
if (deltaLengthWithRadius < this.minDistanceLimit)
{
deltaLengthWithRadius = this.minDistanceLimit;
}
var force = this.forceConstantSquared / deltaLengthWithRadius;
var displacementX = (xDelta / deltaLength) * force;
var displacementY = (yDelta / deltaLength) * force;
this.dispX[i] += displacementX;
this.dispY[i] += displacementY;
this.dispX[j] -= displacementX;
this.dispY[j] -= displacementY;
}
}
}
};
/**
* Function: reduceTemperature
*
* Reduces the temperature of the layout from an initial setting in a linear
* fashion to zero.
*/
mxFastOrganicLayout.prototype.reduceTemperature = function()
{
this.temperature = this.initialTemp * (1.0 - this.iteration / this.maxIterations);
};

View File

@ -0,0 +1,591 @@
/**
* Copyright (c) 2006-2018, JGraph Ltd
* Copyright (c) 2006-2018, Gaudenz Alder
*/
/**
* Class: mxGraphLayout
*
* Base class for all layout algorithms in mxGraph. Main public functions are
* <moveCell> for handling a moved cell within a layouted parent, and <execute> for
* running the layout on a given parent cell.
*
* Known Subclasses:
*
* <mxCircleLayout>, <mxCompactTreeLayout>, <mxCompositeLayout>,
* <mxFastOrganicLayout>, <mxParallelEdgeLayout>, <mxPartitionLayout>,
* <mxStackLayout>
*
* Constructor: mxGraphLayout
*
* Constructs a new layout using the given layouts.
*
* Arguments:
*
* graph - Enclosing
*/
function mxGraphLayout(graph)
{
this.graph = graph;
};
/**
* Variable: graph
*
* Reference to the enclosing <mxGraph>.
*/
mxGraphLayout.prototype.graph = null;
/**
* Variable: useBoundingBox
*
* Boolean indicating if the bounding box of the label should be used if
* its available. Default is true.
*/
mxGraphLayout.prototype.useBoundingBox = true;
/**
* Variable: parent
*
* The parent cell of the layout, if any
*/
mxGraphLayout.prototype.parent = null;
/**
* Function: moveCell
*
* Notified when a cell is being moved in a parent that has automatic
* layout to update the cell state (eg. index) so that the outcome of the
* layout will position the vertex as close to the point (x, y) as
* possible.
*
* Empty implementation.
*
* Parameters:
*
* cell - <mxCell> which has been moved.
* x - X-coordinate of the new cell location.
* y - Y-coordinate of the new cell location.
*/
mxGraphLayout.prototype.moveCell = function(cell, x, y) { };
/**
* Function: resizeCell
*
* Notified when a cell is being resized in a parent that has automatic
* layout to update the other cells in the layout.
*
* Empty implementation.
*
* Parameters:
*
* cell - <mxCell> which has been moved.
* bounds - <mxRectangle> that represents the new cell bounds.
*/
mxGraphLayout.prototype.resizeCell = function(cell, bounds) { };
/**
* Function: execute
*
* Executes the layout algorithm for the children of the given parent.
*
* Parameters:
*
* parent - <mxCell> whose children should be layed out.
*/
mxGraphLayout.prototype.execute = function(parent) { };
/**
* Function: getGraph
*
* Returns the graph that this layout operates on.
*/
mxGraphLayout.prototype.getGraph = function()
{
return this.graph;
};
/**
* Function: getConstraint
*
* Returns the constraint for the given key and cell. The optional edge and
* source arguments are used to return inbound and outgoing routing-
* constraints for the given edge and vertex. This implementation always
* returns the value for the given key in the style of the given cell.
*
* Parameters:
*
* key - Key of the constraint to be returned.
* cell - <mxCell> whose constraint should be returned.
* edge - Optional <mxCell> that represents the connection whose constraint
* should be returned. Default is null.
* source - Optional boolean that specifies if the connection is incoming
* or outgoing. Default is null.
*/
mxGraphLayout.prototype.getConstraint = function(key, cell, edge, source)
{
return this.graph.getCurrentCellStyle(cell)[key]
};
/**
* Function: traverse
*
* Traverses the (directed) graph invoking the given function for each
* visited vertex and edge. The function is invoked with the current vertex
* and the incoming edge as a parameter. This implementation makes sure
* each vertex is only visited once. The function may return false if the
* traversal should stop at the given vertex.
*
* Example:
*
* (code)
* mxLog.show();
* var cell = graph.getSelectionCell();
* graph.traverse(cell, false, function(vertex, edge)
* {
* mxLog.debug(graph.getLabel(vertex));
* });
* (end)
*
* Parameters:
*
* vertex - <mxCell> that represents the vertex where the traversal starts.
* directed - Optional boolean indicating if edges should only be traversed
* from source to target. Default is true.
* func - Visitor function that takes the current vertex and the incoming
* edge as arguments. The traversal stops if the function returns false.
* edge - Optional <mxCell> that represents the incoming edge. This is
* null for the first step of the traversal.
* visited - Optional <mxDictionary> of cell paths for the visited cells.
*/
mxGraphLayout.traverse = function(vertex, directed, func, edge, visited)
{
if (func != null && vertex != null)
{
directed = (directed != null) ? directed : true;
visited = visited || new mxDictionary();
if (!visited.get(vertex))
{
visited.put(vertex, true);
var result = func(vertex, edge);
if (result == null || result)
{
var edgeCount = this.graph.model.getEdgeCount(vertex);
if (edgeCount > 0)
{
for (var i = 0; i < edgeCount; i++)
{
var e = this.graph.model.getEdgeAt(vertex, i);
var isSource = this.graph.model.getTerminal(e, true) == vertex;
if (!directed || isSource)
{
var next = this.graph.view.getVisibleTerminal(e, !isSource);
this.traverse(next, directed, func, e, visited);
}
}
}
}
}
}
};
/**
* Function: isAncestor
*
* Returns true if the given parent is an ancestor of the given child.
*
* Parameters:
*
* parent - <mxCell> that specifies the parent.
* child - <mxCell> that specifies the child.
* traverseAncestors - boolean whether to
*/
mxGraphLayout.prototype.isAncestor = function(parent, child, traverseAncestors)
{
if (!traverseAncestors)
{
return (this.graph.model.getParent(child) == parent);
}
if (child == parent)
{
return false;
}
while (child != null && child != parent)
{
child = this.graph.model.getParent(child);
}
return child == parent;
};
/**
* Function: isVertexMovable
*
* Returns a boolean indicating if the given <mxCell> is movable or
* bendable by the algorithm. This implementation returns true if the given
* cell is movable in the graph.
*
* Parameters:
*
* cell - <mxCell> whose movable state should be returned.
*/
mxGraphLayout.prototype.isVertexMovable = function(cell)
{
return this.graph.isCellMovable(cell);
};
/**
* Function: isVertexIgnored
*
* Returns a boolean indicating if the given <mxCell> should be ignored by
* the algorithm. This implementation returns false for all vertices.
*
* Parameters:
*
* vertex - <mxCell> whose ignored state should be returned.
*/
mxGraphLayout.prototype.isVertexIgnored = function(vertex)
{
return !this.graph.getModel().isVertex(vertex) ||
!this.graph.isCellVisible(vertex);
};
/**
* Function: isEdgeIgnored
*
* Returns a boolean indicating if the given <mxCell> should be ignored by
* the algorithm. This implementation returns false for all vertices.
*
* Parameters:
*
* cell - <mxCell> whose ignored state should be returned.
*/
mxGraphLayout.prototype.isEdgeIgnored = function(edge)
{
var model = this.graph.getModel();
return !model.isEdge(edge) ||
!this.graph.isCellVisible(edge) ||
model.getTerminal(edge, true) == null ||
model.getTerminal(edge, false) == null;
};
/**
* Function: setEdgeStyleEnabled
*
* Disables or enables the edge style of the given edge.
*/
mxGraphLayout.prototype.setEdgeStyleEnabled = function(edge, value)
{
this.graph.setCellStyles(mxConstants.STYLE_NOEDGESTYLE,
(value) ? '0' : '1', [edge]);
};
/**
* Function: setOrthogonalEdge
*
* Disables or enables orthogonal end segments of the given edge.
*/
mxGraphLayout.prototype.setOrthogonalEdge = function(edge, value)
{
this.graph.setCellStyles(mxConstants.STYLE_ORTHOGONAL,
(value) ? '1' : '0', [edge]);
};
/**
* Function: getParentOffset
*
* Determines the offset of the given parent to the parent
* of the layout
*/
mxGraphLayout.prototype.getParentOffset = function(parent)
{
var result = new mxPoint();
if (parent != null && parent != this.parent)
{
var model = this.graph.getModel();
if (model.isAncestor(this.parent, parent))
{
var parentGeo = model.getGeometry(parent);
while (parent != this.parent)
{
result.x = result.x + parentGeo.x;
result.y = result.y + parentGeo.y;
parent = model.getParent(parent);;
parentGeo = model.getGeometry(parent);
}
}
}
return result;
};
/**
* Function: setEdgePoints
*
* Replaces the array of mxPoints in the geometry of the given edge
* with the given array of mxPoints.
*/
mxGraphLayout.prototype.setEdgePoints = function(edge, points)
{
if (edge != null)
{
var model = this.graph.model;
var geometry = model.getGeometry(edge);
if (geometry == null)
{
geometry = new mxGeometry();
geometry.setRelative(true);
}
else
{
geometry = geometry.clone();
}
if (this.parent != null && points != null)
{
var parent = model.getParent(edge);
var parentOffset = this.getParentOffset(parent);
for (var i = 0; i < points.length; i++)
{
points[i].x = points[i].x - parentOffset.x;
points[i].y = points[i].y - parentOffset.y;
}
}
geometry.points = points;
model.setGeometry(edge, geometry);
}
};
/**
* Function: setVertexLocation
*
* Sets the new position of the given cell taking into account the size of
* the bounding box if <useBoundingBox> is true. The change is only carried
* out if the new location is not equal to the existing location, otherwise
* the geometry is not replaced with an updated instance. The new or old
* bounds are returned (including overlapping labels).
*
* Parameters:
*
* cell - <mxCell> whose geometry is to be set.
* x - Integer that defines the x-coordinate of the new location.
* y - Integer that defines the y-coordinate of the new location.
*/
mxGraphLayout.prototype.setVertexLocation = function(cell, x, y)
{
var model = this.graph.getModel();
var geometry = model.getGeometry(cell);
var result = null;
if (geometry != null)
{
result = new mxRectangle(x, y, geometry.width, geometry.height);
// Checks for oversize labels and shifts the result
// TODO: Use mxUtils.getStringSize for label bounds
if (this.useBoundingBox)
{
var state = this.graph.getView().getState(cell);
if (state != null && state.text != null && state.text.boundingBox != null)
{
var scale = this.graph.getView().scale;
var box = state.text.boundingBox;
if (state.text.boundingBox.x < state.x)
{
x += (state.x - box.x) / scale;
result.width = box.width;
}
if (state.text.boundingBox.y < state.y)
{
y += (state.y - box.y) / scale;
result.height = box.height;
}
}
}
if (this.parent != null)
{
var parent = model.getParent(cell);
if (parent != null && parent != this.parent)
{
var parentOffset = this.getParentOffset(parent);
x = x - parentOffset.x;
y = y - parentOffset.y;
}
}
if (geometry.x != x || geometry.y != y)
{
geometry = geometry.clone();
geometry.x = x;
geometry.y = y;
model.setGeometry(cell, geometry);
}
}
return result;
};
/**
* Function: getVertexBounds
*
* Returns an <mxRectangle> that defines the bounds of the given cell or
* the bounding box if <useBoundingBox> is true.
*/
mxGraphLayout.prototype.getVertexBounds = function(cell)
{
var geo = this.graph.getModel().getGeometry(cell);
// Checks for oversize label bounding box and corrects
// the return value accordingly
// TODO: Use mxUtils.getStringSize for label bounds
if (this.useBoundingBox)
{
var state = this.graph.getView().getState(cell);
if (state != null && state.text != null && state.text.boundingBox != null)
{
var scale = this.graph.getView().scale;
var tmp = state.text.boundingBox;
var dx0 = Math.max(state.x - tmp.x, 0) / scale;
var dy0 = Math.max(state.y - tmp.y, 0) / scale;
var dx1 = Math.max((tmp.x + tmp.width) - (state.x + state.width), 0) / scale;
var dy1 = Math.max((tmp.y + tmp.height) - (state.y + state.height), 0) / scale;
geo = new mxRectangle(geo.x - dx0, geo.y - dy0, geo.width + dx0 + dx1, geo.height + dy0 + dy1);
}
}
if (this.parent != null)
{
var parent = this.graph.getModel().getParent(cell);
geo = geo.clone();
if (parent != null && parent != this.parent)
{
var parentOffset = this.getParentOffset(parent);
geo.x = geo.x + parentOffset.x;
geo.y = geo.y + parentOffset.y;
}
}
return new mxRectangle(geo.x, geo.y, geo.width, geo.height);
};
/**
* Function: arrangeGroups
*
* Shortcut to <mxGraph.updateGroupBounds> with moveGroup set to true.
*/
mxGraphLayout.prototype.arrangeGroups = function(cells, border, topBorder, rightBorder, bottomBorder, leftBorder)
{
return this.graph.updateGroupBounds(cells, border, true, topBorder, rightBorder, bottomBorder, leftBorder);
};
/**
* Class: WeightedCellSorter
*
* A utility class used to track cells whilst sorting occurs on the weighted
* sum of their connected edges. Does not violate (x.compareTo(y)==0) ==
* (x.equals(y))
*
* Constructor: WeightedCellSorter
*
* Constructs a new weighted cell sorted for the given cell and weight.
*/
function WeightedCellSorter(cell, weightedValue)
{
this.cell = cell;
this.weightedValue = weightedValue;
};
/**
* Variable: weightedValue
*
* The weighted value of the cell stored.
*/
WeightedCellSorter.prototype.weightedValue = 0;
/**
* Variable: nudge
*
* Whether or not to flip equal weight values.
*/
WeightedCellSorter.prototype.nudge = false;
/**
* Variable: visited
*
* Whether or not this cell has been visited in the current assignment.
*/
WeightedCellSorter.prototype.visited = false;
/**
* Variable: rankIndex
*
* The index this cell is in the model rank.
*/
WeightedCellSorter.prototype.rankIndex = null;
/**
* Variable: cell
*
* The cell whose median value is being calculated.
*/
WeightedCellSorter.prototype.cell = null;
/**
* Function: compare
*
* Compares two WeightedCellSorters.
*/
WeightedCellSorter.prototype.compare = function(a, b)
{
if (a != null && b != null)
{
if (b.weightedValue > a.weightedValue)
{
return -1;
}
else if (b.weightedValue < a.weightedValue)
{
return 1;
}
else
{
if (b.nudge)
{
return -1;
}
else
{
return 1;
}
}
}
else
{
return 0;
}
};

View File

@ -0,0 +1,270 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxParallelEdgeLayout
*
* Extends <mxGraphLayout> for arranging parallel edges. This layout works
* on edges for all pairs of vertices where there is more than one edge
* connecting the latter.
*
* Example:
*
* (code)
* var layout = new mxParallelEdgeLayout(graph);
* layout.execute(graph.getDefaultParent());
* (end)
*
* To run the layout for the parallel edges of a changed edge only, the
* following code can be used.
*
* (code)
* var layout = new mxParallelEdgeLayout(graph);
*
* graph.addListener(mxEvent.CELL_CONNECTED, function(sender, evt)
* {
* var model = graph.getModel();
* var edge = evt.getProperty('edge');
* var src = model.getTerminal(edge, true);
* var trg = model.getTerminal(edge, false);
*
* layout.isEdgeIgnored = function(edge2)
* {
* var src2 = model.getTerminal(edge2, true);
* var trg2 = model.getTerminal(edge2, false);
*
* return !(model.isEdge(edge2) && ((src == src2 && trg == trg2) || (src == trg2 && trg == src2)));
* };
*
* layout.execute(graph.getDefaultParent());
* });
* (end)
*
* Constructor: mxParallelEdgeLayout
*
* Constructs a new parallel edge layout for the specified graph.
*/
function mxParallelEdgeLayout(graph)
{
mxGraphLayout.call(this, graph);
};
/**
* Extends mxGraphLayout.
*/
mxParallelEdgeLayout.prototype = new mxGraphLayout();
mxParallelEdgeLayout.prototype.constructor = mxParallelEdgeLayout;
/**
* Variable: spacing
*
* Defines the spacing between the parallels. Default is 20.
*/
mxParallelEdgeLayout.prototype.spacing = 20;
/**
* Variable: checkOverlap
*
* Specifies if only overlapping edges should be considered
* parallel. Default is false.
*/
mxParallelEdgeLayout.prototype.checkOverlap = false;
/**
* Function: execute
*
* Implements <mxGraphLayout.execute>.
*/
mxParallelEdgeLayout.prototype.execute = function(parent, cells)
{
var lookup = this.findParallels(parent, cells);
this.graph.model.beginUpdate();
try
{
for (var i in lookup)
{
var parallels = lookup[i];
if (parallels.length > 1)
{
this.layout(parallels);
}
}
}
finally
{
this.graph.model.endUpdate();
}
};
/**
* Function: findParallels
*
* Finds the parallel edges in the given parent.
*/
mxParallelEdgeLayout.prototype.findParallels = function(parent, cells)
{
var lookup = [];
var addCell = mxUtils.bind(this, function(cell)
{
if (!this.isEdgeIgnored(cell))
{
var id = this.getEdgeId(cell);
if (id != null)
{
if (lookup[id] == null)
{
lookup[id] = [];
}
lookup[id].push(cell);
}
}
});
if (cells != null)
{
for (var i = 0; i < cells.length; i++)
{
addCell(cells[i]);
}
}
else
{
var model = this.graph.getModel();
var childCount = model.getChildCount(parent);
for (var i = 0; i < childCount; i++)
{
addCell(model.getChildAt(parent, i));
}
}
return lookup;
};
/**
* Function: getEdgeId
*
* Returns a unique ID for the given edge. The id is independent of the
* edge direction and is built using the visible terminal of the given
* edge.
*/
mxParallelEdgeLayout.prototype.getEdgeId = function(edge)
{
var view = this.graph.getView();
// Cannot used cached visible terminal because this could be triggered in BEFORE_UNDO
var src = view.getVisibleTerminal(edge, true);
var trg = view.getVisibleTerminal(edge, false);
var pts = '';
if (src != null && trg != null)
{
src = mxObjectIdentity.get(src);
trg = mxObjectIdentity.get(trg);
if (this.checkOverlap)
{
var state = this.graph.view.getState(edge);
if (state != null && state.absolutePoints != null)
{
var tmp = [];
for (var i = 0; i < state.absolutePoints.length; i++)
{
var pt = state.absolutePoints[i];
if (pt != null)
{
tmp.push(pt.x, pt.y);
}
}
pts = tmp.join(',');
}
};
return ((src > trg) ? trg + '-' + src : src + '-' + trg) + pts;
}
return null;
};
/**
* Function: layout
*
* Lays out the parallel edges in the given array.
*/
mxParallelEdgeLayout.prototype.layout = function(parallels)
{
var edge = parallels[0];
var view = this.graph.getView();
var model = this.graph.getModel();
var src = model.getGeometry(view.getVisibleTerminal(edge, true));
var trg = model.getGeometry(view.getVisibleTerminal(edge, false));
// Routes multiple loops
if (src == trg)
{
var x0 = src.x + src.width + this.spacing;
var y0 = src.y + src.height / 2;
for (var i = 0; i < parallels.length; i++)
{
this.route(parallels[i], x0, y0);
x0 += this.spacing;
}
}
else if (src != null && trg != null)
{
// Routes parallel edges
var scx = src.x + src.width / 2;
var scy = src.y + src.height / 2;
var tcx = trg.x + trg.width / 2;
var tcy = trg.y + trg.height / 2;
var dx = tcx - scx;
var dy = tcy - scy;
var len = Math.sqrt(dx * dx + dy * dy);
if (len > 0)
{
var x0 = scx + dx / 2;
var y0 = scy + dy / 2;
var nx = dy * this.spacing / len;
var ny = dx * this.spacing / len;
x0 += nx * (parallels.length - 1) / 2;
y0 -= ny * (parallels.length - 1) / 2;
for (var i = 0; i < parallels.length; i++)
{
this.route(parallels[i], x0, y0);
x0 -= nx;
y0 += ny;
}
}
}
};
/**
* Function: route
*
* Routes the given edge via the given point.
*/
mxParallelEdgeLayout.prototype.route = function(edge, x, y)
{
if (this.graph.isCellMovable(edge))
{
this.setEdgePoints(edge, [new mxPoint(x, y)]);
}
};

View File

@ -0,0 +1,240 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxPartitionLayout
*
* Extends <mxGraphLayout> for partitioning the parent cell vertically or
* horizontally by filling the complete area with the child cells. A horizontal
* layout partitions the height of the given parent whereas a a non-horizontal
* layout partitions the width. If the parent is a layer (that is, a child of
* the root node), then the current graph size is partitioned. The children do
* not need to be connected for this layout to work.
*
* Example:
*
* (code)
* var layout = new mxPartitionLayout(graph, true, 10, 20);
* layout.execute(graph.getDefaultParent());
* (end)
*
* Constructor: mxPartitionLayout
*
* Constructs a new stack layout layout for the specified graph,
* spacing, orientation and offset.
*/
function mxPartitionLayout(graph, horizontal, spacing, border)
{
mxGraphLayout.call(this, graph);
this.horizontal = (horizontal != null) ? horizontal : true;
this.spacing = spacing || 0;
this.border = border || 0;
};
/**
* Extends mxGraphLayout.
*/
mxPartitionLayout.prototype = new mxGraphLayout();
mxPartitionLayout.prototype.constructor = mxPartitionLayout;
/**
* Variable: horizontal
*
* Boolean indicating the direction in which the space is partitioned.
* Default is true.
*/
mxPartitionLayout.prototype.horizontal = null;
/**
* Variable: spacing
*
* Integer that specifies the absolute spacing in pixels between the
* children. Default is 0.
*/
mxPartitionLayout.prototype.spacing = null;
/**
* Variable: border
*
* Integer that specifies the absolute inset in pixels for the parent that
* contains the children. Default is 0.
*/
mxPartitionLayout.prototype.border = null;
/**
* Variable: resizeVertices
*
* Boolean that specifies if vertices should be resized. Default is true.
*/
mxPartitionLayout.prototype.resizeVertices = true;
/**
* Function: isHorizontal
*
* Returns <horizontal>.
*/
mxPartitionLayout.prototype.isHorizontal = function()
{
return this.horizontal;
};
/**
* Function: moveCell
*
* Implements <mxGraphLayout.moveCell>.
*/
mxPartitionLayout.prototype.moveCell = function(cell, x, y)
{
var model = this.graph.getModel();
var parent = model.getParent(cell);
if (cell != null &&
parent != null)
{
var i = 0;
var last = 0;
var childCount = model.getChildCount(parent);
// Finds index of the closest swimlane
// TODO: Take into account the orientation
for (i = 0; i < childCount; i++)
{
var child = model.getChildAt(parent, i);
var bounds = this.getVertexBounds(child);
if (bounds != null)
{
var tmp = bounds.x + bounds.width / 2;
if (last < x && tmp > x)
{
break;
}
last = tmp;
}
}
// Changes child order in parent
var idx = parent.getIndex(cell);
idx = Math.max(0, i - ((i > idx) ? 1 : 0));
model.add(parent, cell, idx);
}
};
/**
* Function: execute
*
* Implements <mxGraphLayout.execute>. All children where <isVertexIgnored>
* returns false and <isVertexMovable> returns true are modified.
*/
mxPartitionLayout.prototype.execute = function(parent)
{
var horizontal = this.isHorizontal();
var model = this.graph.getModel();
var pgeo = model.getGeometry(parent);
// Handles special case where the parent is either a layer with no
// geometry or the current root of the view in which case the size
// of the graph's container will be used.
if (this.graph.container != null &&
((pgeo == null &&
model.isLayer(parent)) ||
parent == this.graph.getView().currentRoot))
{
var width = this.graph.container.offsetWidth - 1;
var height = this.graph.container.offsetHeight - 1;
pgeo = new mxRectangle(0, 0, width, height);
}
if (pgeo != null)
{
var children = [];
var childCount = model.getChildCount(parent);
for (var i = 0; i < childCount; i++)
{
var child = model.getChildAt(parent, i);
if (!this.isVertexIgnored(child) &&
this.isVertexMovable(child))
{
children.push(child);
}
}
var n = children.length;
if (n > 0)
{
var x0 = this.border;
var y0 = this.border;
var other = (horizontal) ? pgeo.height : pgeo.width;
other -= 2 * this.border;
var size = (this.graph.isSwimlane(parent)) ?
this.graph.getStartSize(parent) :
new mxRectangle();
other -= (horizontal) ? size.height : size.width;
x0 = x0 + size.width;
y0 = y0 + size.height;
var tmp = this.border + (n - 1) * this.spacing;
var value = (horizontal) ?
((pgeo.width - x0 - tmp) / n) :
((pgeo.height - y0 - tmp) / n);
// Avoids negative values, that is values where the sum of the
// spacing plus the border is larger then the available space
if (value > 0)
{
model.beginUpdate();
try
{
for (var i = 0; i < n; i++)
{
var child = children[i];
var geo = model.getGeometry(child);
if (geo != null)
{
geo = geo.clone();
geo.x = x0;
geo.y = y0;
if (horizontal)
{
if (this.resizeVertices)
{
geo.width = value;
geo.height = other;
}
x0 += value + this.spacing;
}
else
{
if (this.resizeVertices)
{
geo.height = value;
geo.width = other;
}
y0 += value + this.spacing;
}
model.setGeometry(child, geo);
}
}
}
finally
{
model.endUpdate();
}
}
}
}
};

View File

@ -0,0 +1,318 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxRadialTreeLayout
*
* Extends <mxGraphLayout> to implement a radial tree algorithm. This
* layout is suitable for graphs that have no cycles (trees). Vertices that are
* not connected to the tree will be ignored by this layout.
*
* Example:
*
* (code)
* var layout = new mxRadialTreeLayout(graph);
* layout.execute(graph.getDefaultParent());
* (end)
*
* Constructor: mxRadialTreeLayout
*
* Constructs a new radial tree layout for the specified graph
*/
function mxRadialTreeLayout(graph)
{
mxCompactTreeLayout.call(this, graph , false);
};
/**
* Extends mxGraphLayout.
*/
mxUtils.extend(mxRadialTreeLayout, mxCompactTreeLayout);
/**
* Variable: angleOffset
*
* The initial offset to compute the angle position.
*/
mxRadialTreeLayout.prototype.angleOffset = 0.5;
/**
* Variable: rootx
*
* The X co-ordinate of the root cell
*/
mxRadialTreeLayout.prototype.rootx = 0;
/**
* Variable: rooty
*
* The Y co-ordinate of the root cell
*/
mxRadialTreeLayout.prototype.rooty = 0;
/**
* Variable: levelDistance
*
* Holds the levelDistance. Default is 120.
*/
mxRadialTreeLayout.prototype.levelDistance = 120;
/**
* Variable: nodeDistance
*
* Holds the nodeDistance. Default is 10.
*/
mxRadialTreeLayout.prototype.nodeDistance = 10;
/**
* Variable: autoRadius
*
* Specifies if the radios should be computed automatically
*/
mxRadialTreeLayout.prototype.autoRadius = false;
/**
* Variable: sortEdges
*
* Specifies if edges should be sorted according to the order of their
* opposite terminal cell in the model.
*/
mxRadialTreeLayout.prototype.sortEdges = false;
/**
* Variable: rowMinX
*
* Array of leftmost x coordinate of each row
*/
mxRadialTreeLayout.prototype.rowMinX = [];
/**
* Variable: rowMaxX
*
* Array of rightmost x coordinate of each row
*/
mxRadialTreeLayout.prototype.rowMaxX = [];
/**
* Variable: rowMinCenX
*
* Array of x coordinate of leftmost vertex of each row
*/
mxRadialTreeLayout.prototype.rowMinCenX = [];
/**
* Variable: rowMaxCenX
*
* Array of x coordinate of rightmost vertex of each row
*/
mxRadialTreeLayout.prototype.rowMaxCenX = [];
/**
* Variable: rowRadi
*
* Array of y deltas of each row behind root vertex, also the radius in the tree
*/
mxRadialTreeLayout.prototype.rowRadi = [];
/**
* Variable: row
*
* Array of vertices on each row
*/
mxRadialTreeLayout.prototype.row = [];
/**
* Function: isVertexIgnored
*
* Returns a boolean indicating if the given <mxCell> should be ignored as a
* vertex. This returns true if the cell has no connections.
*
* Parameters:
*
* vertex - <mxCell> whose ignored state should be returned.
*/
mxRadialTreeLayout.prototype.isVertexIgnored = function(vertex)
{
return mxGraphLayout.prototype.isVertexIgnored.apply(this, arguments) ||
this.graph.getConnections(vertex).length == 0;
};
/**
* Function: execute
*
* Implements <mxGraphLayout.execute>.
*
* If the parent has any connected edges, then it is used as the root of
* the tree. Else, <mxGraph.findTreeRoots> will be used to find a suitable
* root node within the set of children of the given parent.
*
* Parameters:
*
* parent - <mxCell> whose children should be laid out.
* root - Optional <mxCell> that will be used as the root of the tree.
*/
mxRadialTreeLayout.prototype.execute = function(parent, root)
{
this.parent = parent;
this.useBoundingBox = false;
this.edgeRouting = false;
//this.horizontal = false;
mxCompactTreeLayout.prototype.execute.apply(this, arguments);
var bounds = null;
var rootBounds = this.getVertexBounds(this.root);
this.centerX = rootBounds.x + rootBounds.width / 2;
this.centerY = rootBounds.y + rootBounds.height / 2;
// Calculate the bounds of the involved vertices directly from the values set in the compact tree
for (var vertex in this.visited)
{
var vertexBounds = this.getVertexBounds(this.visited[vertex]);
bounds = (bounds != null) ? bounds : vertexBounds.clone();
bounds.add(vertexBounds);
}
this.calcRowDims([this.node], 0);
var maxLeftGrad = 0;
var maxRightGrad = 0;
// Find the steepest left and right gradients
for (var i = 0; i < this.row.length; i++)
{
var leftGrad = (this.centerX - this.rowMinX[i] - this.nodeDistance) / this.rowRadi[i];
var rightGrad = (this.rowMaxX[i] - this.centerX - this.nodeDistance) / this.rowRadi[i];
maxLeftGrad = Math.max (maxLeftGrad, leftGrad);
maxRightGrad = Math.max (maxRightGrad, rightGrad);
}
// Extend out row so they meet the maximum gradient and convert to polar co-ords
for (var i = 0; i < this.row.length; i++)
{
var xLeftLimit = this.centerX - this.nodeDistance - maxLeftGrad * this.rowRadi[i];
var xRightLimit = this.centerX + this.nodeDistance + maxRightGrad * this.rowRadi[i];
var fullWidth = xRightLimit - xLeftLimit;
for (var j = 0; j < this.row[i].length; j ++)
{
var row = this.row[i];
var node = row[j];
var vertexBounds = this.getVertexBounds(node.cell);
var xProportion = (vertexBounds.x + vertexBounds.width / 2 - xLeftLimit) / (fullWidth);
var theta = 2 * Math.PI * xProportion;
node.theta = theta;
}
}
// Post-process from outside inwards to try to align parents with children
for (var i = this.row.length - 2; i >= 0; i--)
{
var row = this.row[i];
for (var j = 0; j < row.length; j++)
{
var node = row[j];
var child = node.child;
var counter = 0;
var totalTheta = 0;
while (child != null)
{
totalTheta += child.theta;
counter++;
child = child.next;
}
if (counter > 0)
{
var averTheta = totalTheta / counter;
if (averTheta > node.theta && j < row.length - 1)
{
var nextTheta = row[j+1].theta;
node.theta = Math.min (averTheta, nextTheta - Math.PI/10);
}
else if (averTheta < node.theta && j > 0 )
{
var lastTheta = row[j-1].theta;
node.theta = Math.max (averTheta, lastTheta + Math.PI/10);
}
}
}
}
// Set locations
for (var i = 0; i < this.row.length; i++)
{
for (var j = 0; j < this.row[i].length; j ++)
{
var row = this.row[i];
var node = row[j];
var vertexBounds = this.getVertexBounds(node.cell);
this.setVertexLocation(node.cell,
this.centerX - vertexBounds.width / 2 + this.rowRadi[i] * Math.cos(node.theta),
this.centerY - vertexBounds.height / 2 + this.rowRadi[i] * Math.sin(node.theta));
}
}
};
/**
* Function: calcRowDims
*
* Recursive function to calculate the dimensions of each row
*
* Parameters:
*
* row - Array of internal nodes, the children of which are to be processed.
* rowNum - Integer indicating which row is being processed.
*/
mxRadialTreeLayout.prototype.calcRowDims = function(row, rowNum)
{
if (row == null || row.length == 0)
{
return;
}
// Place root's children proportionally around the first level
this.rowMinX[rowNum] = this.centerX;
this.rowMaxX[rowNum] = this.centerX;
this.rowMinCenX[rowNum] = this.centerX;
this.rowMaxCenX[rowNum] = this.centerX;
this.row[rowNum] = [];
var rowHasChildren = false;
for (var i = 0; i < row.length; i++)
{
var child = row[i] != null ? row[i].child : null;
while (child != null)
{
var cell = child.cell;
var vertexBounds = this.getVertexBounds(cell);
this.rowMinX[rowNum] = Math.min(vertexBounds.x, this.rowMinX[rowNum]);
this.rowMaxX[rowNum] = Math.max(vertexBounds.x + vertexBounds.width, this.rowMaxX[rowNum]);
this.rowMinCenX[rowNum] = Math.min(vertexBounds.x + vertexBounds.width / 2, this.rowMinCenX[rowNum]);
this.rowMaxCenX[rowNum] = Math.max(vertexBounds.x + vertexBounds.width / 2, this.rowMaxCenX[rowNum]);
this.rowRadi[rowNum] = vertexBounds.y - this.getVertexBounds(this.root).y;
if (child.child != null)
{
rowHasChildren = true;
}
this.row[rowNum].push(child);
child = child.next;
}
}
if (rowHasChildren)
{
this.calcRowDims(this.row[rowNum], rowNum + 1);
}
};

View File

@ -0,0 +1,603 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxStackLayout
*
* Extends <mxGraphLayout> to create a horizontal or vertical stack of the
* child vertices. The children do not need to be connected for this layout
* to work.
*
* Example:
*
* (code)
* var layout = new mxStackLayout(graph, true);
* layout.execute(graph.getDefaultParent());
* (end)
*
* Constructor: mxStackLayout
*
* Constructs a new stack layout layout for the specified graph,
* spacing, orientation and offset.
*/
function mxStackLayout(graph, horizontal, spacing, x0, y0, border)
{
mxGraphLayout.call(this, graph);
this.horizontal = (horizontal != null) ? horizontal : true;
this.spacing = (spacing != null) ? spacing : 0;
this.x0 = (x0 != null) ? x0 : 0;
this.y0 = (y0 != null) ? y0 : 0;
this.border = (border != null) ? border : 0;
};
/**
* Extends mxGraphLayout.
*/
mxStackLayout.prototype = new mxGraphLayout();
mxStackLayout.prototype.constructor = mxStackLayout;
/**
* Variable: horizontal
*
* Specifies the orientation of the layout. Default is true.
*/
mxStackLayout.prototype.horizontal = null;
/**
* Variable: spacing
*
* Specifies the spacing between the cells. Default is 0.
*/
mxStackLayout.prototype.spacing = null;
/**
* Variable: x0
*
* Specifies the horizontal origin of the layout. Default is 0.
*/
mxStackLayout.prototype.x0 = null;
/**
* Variable: y0
*
* Specifies the vertical origin of the layout. Default is 0.
*/
mxStackLayout.prototype.y0 = null;
/**
* Variable: border
*
* Border to be added if fill is true. Default is 0.
*/
mxStackLayout.prototype.border = 0;
/**
* Variable: marginTop
*
* Top margin for the child area. Default is 0.
*/
mxStackLayout.prototype.marginTop = 0;
/**
* Variable: marginLeft
*
* Top margin for the child area. Default is 0.
*/
mxStackLayout.prototype.marginLeft = 0;
/**
* Variable: marginRight
*
* Top margin for the child area. Default is 0.
*/
mxStackLayout.prototype.marginRight = 0;
/**
* Variable: marginBottom
*
* Top margin for the child area. Default is 0.
*/
mxStackLayout.prototype.marginBottom = 0;
/**
* Variable: keepFirstLocation
*
* Boolean indicating if the location of the first cell should be
* kept, that is, it will not be moved to x0 or y0. Default is false.
*/
mxStackLayout.prototype.keepFirstLocation = false;
/**
* Variable: fill
*
* Boolean indicating if dimension should be changed to fill out the parent
* cell. Default is false.
*/
mxStackLayout.prototype.fill = false;
/**
* Variable: resizeParent
*
* If the parent should be resized to match the width/height of the
* stack. Default is false.
*/
mxStackLayout.prototype.resizeParent = false;
/**
* Variable: resizeParentMax
*
* Use maximum of existing value and new value for resize of parent.
* Default is false.
*/
mxStackLayout.prototype.resizeParentMax = false;
/**
* Variable: resizeLast
*
* If the last element should be resized to fill out the parent. Default is
* false. If <resizeParent> is true then this is ignored.
*/
mxStackLayout.prototype.resizeLast = false;
/**
* Variable: wrap
*
* Value at which a new column or row should be created. Default is null.
*/
mxStackLayout.prototype.wrap = null;
/**
* Variable: borderCollapse
*
* If the strokeWidth should be ignored. Default is true.
*/
mxStackLayout.prototype.borderCollapse = true;
/**
* Variable: allowGaps
*
* If gaps should be allowed in the stack. Default is false.
*/
mxStackLayout.prototype.allowGaps = false;
/**
* Variable: gridSize
*
* Grid size for alignment of position and size. Default is 0.
*/
mxStackLayout.prototype.gridSize = 0;
/**
* Function: isHorizontal
*
* Returns <horizontal>.
*/
mxStackLayout.prototype.isHorizontal = function()
{
return this.horizontal;
};
/**
* Function: moveCell
*
* Implements <mxGraphLayout.moveCell>.
*/
mxStackLayout.prototype.moveCell = function(cell, x, y)
{
var model = this.graph.getModel();
var parent = model.getParent(cell);
var horizontal = this.isHorizontal();
if (cell != null && parent != null)
{
var i = 0;
var last = 0;
var childCount = model.getChildCount(parent);
var value = (horizontal) ? x : y;
var pstate = this.graph.getView().getState(parent);
if (pstate != null)
{
value -= (horizontal) ? pstate.x : pstate.y;
}
value /= this.graph.view.scale;
for (i = 0; i < childCount; i++)
{
var child = model.getChildAt(parent, i);
if (child != cell)
{
var bounds = model.getGeometry(child);
if (bounds != null)
{
var tmp = (horizontal) ?
bounds.x + bounds.width / 2 :
bounds.y + bounds.height / 2;
if (last <= value && tmp > value)
{
break;
}
last = tmp;
}
}
}
// Changes child order in parent
var idx = parent.getIndex(cell);
idx = Math.max(0, i - ((i > idx) ? 1 : 0));
model.add(parent, cell, idx);
}
};
/**
* Function: getParentSize
*
* Returns the size for the parent container or the size of the graph
* container if the parent is a layer or the root of the model.
*/
mxStackLayout.prototype.getParentSize = function(parent)
{
var model = this.graph.getModel();
var pgeo = model.getGeometry(parent);
// Handles special case where the parent is either a layer with no
// geometry or the current root of the view in which case the size
// of the graph's container will be used.
if (this.graph.container != null && ((pgeo == null &&
model.isLayer(parent)) || parent == this.graph.getView().currentRoot))
{
var width = this.graph.container.offsetWidth - 1;
var height = this.graph.container.offsetHeight - 1;
pgeo = new mxRectangle(0, 0, width, height);
}
return pgeo;
};
/**
* Function: getLayoutCells
*
* Returns the cells to be layouted.
*/
mxStackLayout.prototype.getLayoutCells = function(parent)
{
var model = this.graph.getModel();
var childCount = model.getChildCount(parent);
var cells = [];
for (var i = 0; i < childCount; i++)
{
var child = model.getChildAt(parent, i);
if (!this.isVertexIgnored(child) && this.isVertexMovable(child))
{
cells.push(child);
}
}
if (this.allowGaps)
{
cells.sort(mxUtils.bind(this, function(c1, c2)
{
var geo1 = this.graph.getCellGeometry(c1);
var geo2 = this.graph.getCellGeometry(c2);
return (this.horizontal) ?
((geo1.x == geo2.x) ? 0 : ((geo1.x > geo2.x > 0) ? 1 : -1)) :
((geo1.y == geo2.y) ? 0 : ((geo1.y > geo2.y > 0) ? 1 : -1));
}));
}
return cells;
};
/**
* Function: snap
*
* Snaps the given value to the grid size.
*/
mxStackLayout.prototype.snap = function(value)
{
if (this.gridSize != null && this.gridSize > 0)
{
value = Math.max(value, this.gridSize);
if (value / this.gridSize > 1)
{
var mod = value % this.gridSize;
value += mod > this.gridSize / 2 ? (this.gridSize - mod) : -mod;
}
}
return value;
};
/**
* Function: execute
*
* Implements <mxGraphLayout.execute>.
*
* Only children where <isVertexIgnored> returns false are taken into
* account.
*/
mxStackLayout.prototype.execute = function(parent)
{
if (parent != null)
{
var pgeo = this.getParentSize(parent);
var horizontal = this.isHorizontal();
var model = this.graph.getModel();
var fillValue = null;
if (pgeo != null)
{
fillValue = (horizontal) ? pgeo.height - this.marginTop - this.marginBottom :
pgeo.width - this.marginLeft - this.marginRight;
}
fillValue -= 2 * this.border;
var x0 = this.x0 + this.border + this.marginLeft;
var y0 = this.y0 + this.border + this.marginTop;
// Handles swimlane start size
if (this.graph.isSwimlane(parent))
{
// Uses computed style to get latest
var style = this.graph.getCellStyle(parent);
var start = mxUtils.getNumber(style, mxConstants.STYLE_STARTSIZE, mxConstants.DEFAULT_STARTSIZE);
var horz = mxUtils.getValue(style, mxConstants.STYLE_HORIZONTAL, true) == 1;
if (pgeo != null)
{
if (horz)
{
start = Math.min(start, pgeo.height);
}
else
{
start = Math.min(start, pgeo.width);
}
}
if (horizontal == horz)
{
fillValue -= start;
}
if (horz)
{
y0 += start;
}
else
{
x0 += start;
}
}
model.beginUpdate();
try
{
var tmp = 0;
var last = null;
var lastValue = 0;
var lastChild = null;
var cells = this.getLayoutCells(parent);
for (var i = 0; i < cells.length; i++)
{
var child = cells[i];
var geo = model.getGeometry(child);
if (geo != null)
{
geo = geo.clone();
if (this.wrap != null && last != null)
{
if ((horizontal && last.x + last.width +
geo.width + 2 * this.spacing > this.wrap) ||
(!horizontal && last.y + last.height +
geo.height + 2 * this.spacing > this.wrap))
{
last = null;
if (horizontal)
{
y0 += tmp + this.spacing;
}
else
{
x0 += tmp + this.spacing;
}
tmp = 0;
}
}
tmp = Math.max(tmp, (horizontal) ? geo.height : geo.width);
var sw = 0;
if (!this.borderCollapse)
{
var childStyle = this.graph.getCellStyle(child);
sw = mxUtils.getNumber(childStyle, mxConstants.STYLE_STROKEWIDTH, 1);
}
if (last != null)
{
var temp = lastValue + this.spacing + Math.floor(sw / 2);
if (horizontal)
{
geo.x = this.snap(((this.allowGaps) ? Math.max(temp, geo.x) :
temp) - this.marginLeft) + this.marginLeft;
}
else
{
geo.y = this.snap(((this.allowGaps) ? Math.max(temp, geo.y) :
temp) - this.marginTop) + this.marginTop;
}
}
else if (!this.keepFirstLocation)
{
if (horizontal)
{
geo.x = (this.allowGaps && geo.x > x0) ? Math.max(this.snap(geo.x -
this.marginLeft) + this.marginLeft, x0) : x0;
}
else
{
geo.y = (this.allowGaps && geo.y > y0) ? Math.max(this.snap(geo.y -
this.marginTop) + this.marginTop, y0) : y0;
}
}
if (horizontal)
{
geo.y = y0;
}
else
{
geo.x = x0;
}
if (this.fill && fillValue != null)
{
if (horizontal)
{
geo.height = fillValue;
}
else
{
geo.width = fillValue;
}
}
if (horizontal)
{
geo.width = this.snap(geo.width);
}
else
{
geo.height = this.snap(geo.height);
}
this.setChildGeometry(child, geo);
lastChild = child;
last = geo;
if (horizontal)
{
lastValue = last.x + last.width + Math.floor(sw / 2);
}
else
{
lastValue = last.y + last.height + Math.floor(sw / 2);
}
}
}
if (this.resizeParent && pgeo != null && last != null && !this.graph.isCellCollapsed(parent))
{
this.updateParentGeometry(parent, pgeo, last);
}
else if (this.resizeLast && pgeo != null && last != null && lastChild != null)
{
if (horizontal)
{
last.width = pgeo.width - last.x - this.spacing - this.marginRight - this.marginLeft;
}
else
{
last.height = pgeo.height - last.y - this.spacing - this.marginBottom;
}
this.setChildGeometry(lastChild, last);
}
}
finally
{
model.endUpdate();
}
}
};
/**
* Function: setChildGeometry
*
* Sets the specific geometry to the given child cell.
*
* Parameters:
*
* child - The given child of <mxCell>.
* geo - The specific geometry of <mxGeometry>.
*/
mxStackLayout.prototype.setChildGeometry = function(child, geo)
{
var geo2 = this.graph.getCellGeometry(child);
if (geo2 == null || geo.x != geo2.x || geo.y != geo2.y ||
geo.width != geo2.width || geo.height != geo2.height)
{
this.graph.getModel().setGeometry(child, geo);
}
};
/**
* Function: updateParentGeometry
*
* Updates the geometry of the given parent cell.
*
* Parameters:
*
* parent - The given parent of <mxCell>.
* pgeo - The new <mxGeometry> for parent.
* last - The last <mxGeometry>.
*/
mxStackLayout.prototype.updateParentGeometry = function(parent, pgeo, last)
{
var horizontal = this.isHorizontal();
var model = this.graph.getModel();
var pgeo2 = pgeo.clone();
if (horizontal)
{
var tmp = last.x + last.width + this.marginRight + this.border;
if (this.resizeParentMax)
{
pgeo2.width = Math.max(pgeo2.width, tmp);
}
else
{
pgeo2.width = tmp;
}
}
else
{
var tmp = last.y + last.height + this.marginBottom + this.border;
if (this.resizeParentMax)
{
pgeo2.height = Math.max(pgeo2.height, tmp);
}
else
{
pgeo2.height = tmp;
}
}
if (pgeo.x != pgeo2.x || pgeo.y != pgeo2.y ||
pgeo.width != pgeo2.width || pgeo.height != pgeo2.height)
{
model.setGeometry(parent, pgeo2);
}
};

View File

@ -0,0 +1,825 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxCell
*
* Cells are the elements of the graph model. They represent the state
* of the groups, vertices and edges in a graph.
*
* Custom attributes:
*
* For custom attributes we recommend using an XML node as the value of a cell.
* The following code can be used to create a cell with an XML node as the
* value:
*
* (code)
* var doc = mxUtils.createXmlDocument();
* var node = doc.createElement('MyNode')
* node.setAttribute('label', 'MyLabel');
* node.setAttribute('attribute1', 'value1');
* graph.insertVertex(graph.getDefaultParent(), null, node, 40, 40, 80, 30);
* (end)
*
* For the label to work, <mxGraph.convertValueToString> and
* <mxGraph.cellLabelChanged> should be overridden as follows:
*
* (code)
* graph.convertValueToString = function(cell)
* {
* if (mxUtils.isNode(cell.value))
* {
* return cell.getAttribute('label', '')
* }
* };
*
* var cellLabelChanged = graph.cellLabelChanged;
* graph.cellLabelChanged = function(cell, newValue, autoSize)
* {
* if (mxUtils.isNode(cell.value))
* {
* // Clones the value for correct undo/redo
* var elt = cell.value.cloneNode(true);
* elt.setAttribute('label', newValue);
* newValue = elt;
* }
*
* cellLabelChanged.apply(this, arguments);
* };
* (end)
*
* Callback: onInit
*
* Called from within the constructor.
*
* Constructor: mxCell
*
* Constructs a new cell to be used in a graph model.
* This method invokes <onInit> upon completion.
*
* Parameters:
*
* value - Optional object that represents the cell value.
* geometry - Optional <mxGeometry> that specifies the geometry.
* style - Optional formatted string that defines the style.
*/
function mxCell(value, geometry, style)
{
this.value = value;
this.setGeometry(geometry);
this.setStyle(style);
if (this.onInit != null)
{
this.onInit();
}
};
/**
* Variable: id
*
* Holds the Id. Default is null.
*/
mxCell.prototype.id = null;
/**
* Variable: value
*
* Holds the user object. Default is null.
*/
mxCell.prototype.value = null;
/**
* Variable: geometry
*
* Holds the <mxGeometry>. Default is null.
*/
mxCell.prototype.geometry = null;
/**
* Variable: style
*
* Holds the style as a string of the form [(stylename|key=value);]. Default is
* null.
*/
mxCell.prototype.style = null;
/**
* Variable: vertex
*
* Specifies whether the cell is a vertex. Default is false.
*/
mxCell.prototype.vertex = false;
/**
* Variable: edge
*
* Specifies whether the cell is an edge. Default is false.
*/
mxCell.prototype.edge = false;
/**
* Variable: connectable
*
* Specifies whether the cell is connectable. Default is true.
*/
mxCell.prototype.connectable = true;
/**
* Variable: visible
*
* Specifies whether the cell is visible. Default is true.
*/
mxCell.prototype.visible = true;
/**
* Variable: collapsed
*
* Specifies whether the cell is collapsed. Default is false.
*/
mxCell.prototype.collapsed = false;
/**
* Variable: parent
*
* Reference to the parent cell.
*/
mxCell.prototype.parent = null;
/**
* Variable: source
*
* Reference to the source terminal.
*/
mxCell.prototype.source = null;
/**
* Variable: target
*
* Reference to the target terminal.
*/
mxCell.prototype.target = null;
/**
* Variable: children
*
* Holds the child cells.
*/
mxCell.prototype.children = null;
/**
* Variable: edges
*
* Holds the edges.
*/
mxCell.prototype.edges = null;
/**
* Variable: mxTransient
*
* List of members that should not be cloned inside <clone>. This field is
* passed to <mxUtils.clone> and is not made persistent in <mxCellCodec>.
* This is not a convention for all classes, it is only used in this class
* to mark transient fields since transient modifiers are not supported by
* the language.
*/
mxCell.prototype.mxTransient = ['id', 'value', 'parent', 'source',
'target', 'children', 'edges'];
/**
* Function: getId
*
* Returns the Id of the cell as a string.
*/
mxCell.prototype.getId = function()
{
return this.id;
};
/**
* Function: setId
*
* Sets the Id of the cell to the given string.
*/
mxCell.prototype.setId = function(id)
{
this.id = id;
};
/**
* Function: getValue
*
* Returns the user object of the cell. The user
* object is stored in <value>.
*/
mxCell.prototype.getValue = function()
{
return this.value;
};
/**
* Function: setValue
*
* Sets the user object of the cell. The user object
* is stored in <value>.
*/
mxCell.prototype.setValue = function(value)
{
this.value = value;
};
/**
* Function: valueChanged
*
* Changes the user object after an in-place edit
* and returns the previous value. This implementation
* replaces the user object with the given value and
* returns the old user object.
*/
mxCell.prototype.valueChanged = function(newValue)
{
var previous = this.getValue();
this.setValue(newValue);
return previous;
};
/**
* Function: getGeometry
*
* Returns the <mxGeometry> that describes the <geometry>.
*/
mxCell.prototype.getGeometry = function()
{
return this.geometry;
};
/**
* Function: setGeometry
*
* Sets the <mxGeometry> to be used as the <geometry>.
*/
mxCell.prototype.setGeometry = function(geometry)
{
this.geometry = geometry;
};
/**
* Function: getStyle
*
* Returns a string that describes the <style>.
*/
mxCell.prototype.getStyle = function()
{
return this.style;
};
/**
* Function: setStyle
*
* Sets the string to be used as the <style>.
*/
mxCell.prototype.setStyle = function(style)
{
this.style = style;
};
/**
* Function: isVertex
*
* Returns true if the cell is a vertex.
*/
mxCell.prototype.isVertex = function()
{
return this.vertex != 0;
};
/**
* Function: setVertex
*
* Specifies if the cell is a vertex. This should only be assigned at
* construction of the cell and not be changed during its lifecycle.
*
* Parameters:
*
* vertex - Boolean that specifies if the cell is a vertex.
*/
mxCell.prototype.setVertex = function(vertex)
{
this.vertex = vertex;
};
/**
* Function: isEdge
*
* Returns true if the cell is an edge.
*/
mxCell.prototype.isEdge = function()
{
return this.edge != 0;
};
/**
* Function: setEdge
*
* Specifies if the cell is an edge. This should only be assigned at
* construction of the cell and not be changed during its lifecycle.
*
* Parameters:
*
* edge - Boolean that specifies if the cell is an edge.
*/
mxCell.prototype.setEdge = function(edge)
{
this.edge = edge;
};
/**
* Function: isConnectable
*
* Returns true if the cell is connectable.
*/
mxCell.prototype.isConnectable = function()
{
return this.connectable != 0;
};
/**
* Function: setConnectable
*
* Sets the connectable state.
*
* Parameters:
*
* connectable - Boolean that specifies the new connectable state.
*/
mxCell.prototype.setConnectable = function(connectable)
{
this.connectable = connectable;
};
/**
* Function: isVisible
*
* Returns true if the cell is visibile.
*/
mxCell.prototype.isVisible = function()
{
return this.visible != 0;
};
/**
* Function: setVisible
*
* Specifies if the cell is visible.
*
* Parameters:
*
* visible - Boolean that specifies the new visible state.
*/
mxCell.prototype.setVisible = function(visible)
{
this.visible = visible;
};
/**
* Function: isCollapsed
*
* Returns true if the cell is collapsed.
*/
mxCell.prototype.isCollapsed = function()
{
return this.collapsed != 0;
};
/**
* Function: setCollapsed
*
* Sets the collapsed state.
*
* Parameters:
*
* collapsed - Boolean that specifies the new collapsed state.
*/
mxCell.prototype.setCollapsed = function(collapsed)
{
this.collapsed = collapsed;
};
/**
* Function: getParent
*
* Returns the cell's parent.
*/
mxCell.prototype.getParent = function()
{
return this.parent;
};
/**
* Function: setParent
*
* Sets the parent cell.
*
* Parameters:
*
* parent - <mxCell> that represents the new parent.
*/
mxCell.prototype.setParent = function(parent)
{
this.parent = parent;
};
/**
* Function: getTerminal
*
* Returns the source or target terminal.
*
* Parameters:
*
* source - Boolean that specifies if the source terminal should be
* returned.
*/
mxCell.prototype.getTerminal = function(source)
{
return (source) ? this.source : this.target;
};
/**
* Function: setTerminal
*
* Sets the source or target terminal and returns the new terminal.
*
* Parameters:
*
* terminal - <mxCell> that represents the new source or target terminal.
* isSource - Boolean that specifies if the source or target terminal
* should be set.
*/
mxCell.prototype.setTerminal = function(terminal, isSource)
{
if (isSource)
{
this.source = terminal;
}
else
{
this.target = terminal;
}
return terminal;
};
/**
* Function: getChildCount
*
* Returns the number of child cells.
*/
mxCell.prototype.getChildCount = function()
{
return (this.children == null) ? 0 : this.children.length;
};
/**
* Function: getIndex
*
* Returns the index of the specified child in the child array.
*
* Parameters:
*
* child - Child whose index should be returned.
*/
mxCell.prototype.getIndex = function(child)
{
return mxUtils.indexOf(this.children, child);
};
/**
* Function: getChildAt
*
* Returns the child at the specified index.
*
* Parameters:
*
* index - Integer that specifies the child to be returned.
*/
mxCell.prototype.getChildAt = function(index)
{
return (this.children == null) ? null : this.children[index];
};
/**
* Function: insert
*
* Inserts the specified child into the child array at the specified index
* and updates the parent reference of the child. If not childIndex is
* specified then the child is appended to the child array. Returns the
* inserted child.
*
* Parameters:
*
* child - <mxCell> to be inserted or appended to the child array.
* index - Optional integer that specifies the index at which the child
* should be inserted into the child array.
*/
mxCell.prototype.insert = function(child, index)
{
if (child != null)
{
if (index == null)
{
index = this.getChildCount();
if (child.getParent() == this)
{
index--;
}
}
child.removeFromParent();
child.setParent(this);
if (this.children == null)
{
this.children = [];
this.children.push(child);
}
else
{
this.children.splice(index, 0, child);
}
}
return child;
};
/**
* Function: remove
*
* Removes the child at the specified index from the child array and
* returns the child that was removed. Will remove the parent reference of
* the child.
*
* Parameters:
*
* index - Integer that specifies the index of the child to be
* removed.
*/
mxCell.prototype.remove = function(index)
{
var child = null;
if (this.children != null && index >= 0)
{
child = this.getChildAt(index);
if (child != null)
{
this.children.splice(index, 1);
child.setParent(null);
}
}
return child;
};
/**
* Function: removeFromParent
*
* Removes the cell from its parent.
*/
mxCell.prototype.removeFromParent = function()
{
if (this.parent != null)
{
var index = this.parent.getIndex(this);
this.parent.remove(index);
}
};
/**
* Function: getEdgeCount
*
* Returns the number of edges in the edge array.
*/
mxCell.prototype.getEdgeCount = function()
{
return (this.edges == null) ? 0 : this.edges.length;
};
/**
* Function: getEdgeIndex
*
* Returns the index of the specified edge in <edges>.
*
* Parameters:
*
* edge - <mxCell> whose index in <edges> should be returned.
*/
mxCell.prototype.getEdgeIndex = function(edge)
{
return mxUtils.indexOf(this.edges, edge);
};
/**
* Function: getEdgeAt
*
* Returns the edge at the specified index in <edges>.
*
* Parameters:
*
* index - Integer that specifies the index of the edge to be returned.
*/
mxCell.prototype.getEdgeAt = function(index)
{
return (this.edges == null) ? null : this.edges[index];
};
/**
* Function: insertEdge
*
* Inserts the specified edge into the edge array and returns the edge.
* Will update the respective terminal reference of the edge.
*
* Parameters:
*
* edge - <mxCell> to be inserted into the edge array.
* isOutgoing - Boolean that specifies if the edge is outgoing.
*/
mxCell.prototype.insertEdge = function(edge, isOutgoing)
{
if (edge != null)
{
edge.removeFromTerminal(isOutgoing);
edge.setTerminal(this, isOutgoing);
if (this.edges == null ||
edge.getTerminal(!isOutgoing) != this ||
mxUtils.indexOf(this.edges, edge) < 0)
{
if (this.edges == null)
{
this.edges = [];
}
this.edges.push(edge);
}
}
return edge;
};
/**
* Function: removeEdge
*
* Removes the specified edge from the edge array and returns the edge.
* Will remove the respective terminal reference from the edge.
*
* Parameters:
*
* edge - <mxCell> to be removed from the edge array.
* isOutgoing - Boolean that specifies if the edge is outgoing.
*/
mxCell.prototype.removeEdge = function(edge, isOutgoing)
{
if (edge != null)
{
if (edge.getTerminal(!isOutgoing) != this &&
this.edges != null)
{
var index = this.getEdgeIndex(edge);
if (index >= 0)
{
this.edges.splice(index, 1);
}
}
edge.setTerminal(null, isOutgoing);
}
return edge;
};
/**
* Function: removeFromTerminal
*
* Removes the edge from its source or target terminal.
*
* Parameters:
*
* isSource - Boolean that specifies if the edge should be removed from its
* source or target terminal.
*/
mxCell.prototype.removeFromTerminal = function(isSource)
{
var terminal = this.getTerminal(isSource);
if (terminal != null)
{
terminal.removeEdge(this, isSource);
}
};
/**
* Function: hasAttribute
*
* Returns true if the user object is an XML node that contains the given
* attribute.
*
* Parameters:
*
* name - Name of the attribute.
*/
mxCell.prototype.hasAttribute = function(name)
{
var userObject = this.getValue();
return (userObject != null &&
userObject.nodeType == mxConstants.NODETYPE_ELEMENT && userObject.hasAttribute) ?
userObject.hasAttribute(name) : userObject.getAttribute(name) != null;
};
/**
* Function: getAttribute
*
* Returns the specified attribute from the user object if it is an XML
* node.
*
* Parameters:
*
* name - Name of the attribute whose value should be returned.
* defaultValue - Optional default value to use if the attribute has no
* value.
*/
mxCell.prototype.getAttribute = function(name, defaultValue)
{
var userObject = this.getValue();
var val = (userObject != null &&
userObject.nodeType == mxConstants.NODETYPE_ELEMENT) ?
userObject.getAttribute(name) : null;
return (val != null) ? val : defaultValue;
};
/**
* Function: setAttribute
*
* Sets the specified attribute on the user object if it is an XML node.
*
* Parameters:
*
* name - Name of the attribute whose value should be set.
* value - New value of the attribute.
*/
mxCell.prototype.setAttribute = function(name, value)
{
var userObject = this.getValue();
if (userObject != null &&
userObject.nodeType == mxConstants.NODETYPE_ELEMENT)
{
userObject.setAttribute(name, value);
}
};
/**
* Function: clone
*
* Returns a clone of the cell. Uses <cloneValue> to clone
* the user object. All fields in <mxTransient> are ignored
* during the cloning.
*/
mxCell.prototype.clone = function()
{
var clone = mxUtils.clone(this, this.mxTransient);
clone.setValue(this.cloneValue());
return clone;
};
/**
* Function: cloneValue
*
* Returns a clone of the cell's user object.
*/
mxCell.prototype.cloneValue = function()
{
var value = this.getValue();
if (value != null)
{
if (typeof(value.clone) == 'function')
{
value = value.clone();
}
else if (!isNaN(value.nodeType))
{
value = value.cloneNode(true);
}
}
return value;
};

View File

@ -0,0 +1,163 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
var mxCellPath =
{
/**
* Class: mxCellPath
*
* Implements a mechanism for temporary cell Ids.
*
* Variable: PATH_SEPARATOR
*
* Defines the separator between the path components. Default is ".".
*/
PATH_SEPARATOR: '.',
/**
* Function: create
*
* Creates the cell path for the given cell. The cell path is a
* concatenation of the indices of all ancestors on the (finite) path to
* the root, eg. "0.0.0.1".
*
* Parameters:
*
* cell - Cell whose path should be returned.
*/
create: function(cell)
{
var result = '';
if (cell != null)
{
var parent = cell.getParent();
while (parent != null)
{
var index = parent.getIndex(cell);
result = index + mxCellPath.PATH_SEPARATOR + result;
cell = parent;
parent = cell.getParent();
}
}
// Removes trailing separator
var n = result.length;
if (n > 1)
{
result = result.substring(0, n - 1);
}
return result;
},
/**
* Function: getParentPath
*
* Returns the path for the parent of the cell represented by the given
* path. Returns null if the given path has no parent.
*
* Parameters:
*
* path - Path whose parent path should be returned.
*/
getParentPath: function(path)
{
if (path != null)
{
var index = path.lastIndexOf(mxCellPath.PATH_SEPARATOR);
if (index >= 0)
{
return path.substring(0, index);
}
else if (path.length > 0)
{
return '';
}
}
return null;
},
/**
* Function: resolve
*
* Returns the cell for the specified cell path using the given root as the
* root of the path.
*
* Parameters:
*
* root - Root cell of the path to be resolved.
* path - String that defines the path.
*/
resolve: function(root, path)
{
var parent = root;
if (path != null)
{
var tokens = path.split(mxCellPath.PATH_SEPARATOR);
for (var i=0; i<tokens.length; i++)
{
parent = parent.getChildAt(parseInt(tokens[i]));
}
}
return parent;
},
/**
* Function: compare
*
* Compares the given cell paths and returns -1 if p1 is smaller, 0 if
* p1 is equal and 1 if p1 is greater than p2.
*/
compare: function(p1, p2)
{
var min = Math.min(p1.length, p2.length);
var comp = 0;
for (var i = 0; i < min; i++)
{
if (p1[i] != p2[i])
{
if (p1[i].length == 0 ||
p2[i].length == 0)
{
comp = (p1[i] == p2[i]) ? 0 : ((p1[i] > p2[i]) ? 1 : -1);
}
else
{
var t1 = parseInt(p1[i]);
var t2 = parseInt(p2[i]);
comp = (t1 == t2) ? 0 : ((t1 > t2) ? 1 : -1);
}
break;
}
}
// Compares path length if both paths are equal to this point
if (comp == 0)
{
var t1 = p1.length;
var t2 = p2.length;
if (t1 != t2)
{
comp = (t1 > t2) ? 1 : -1;
}
}
return comp;
}
};

View File

@ -0,0 +1,415 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxGeometry
*
* Extends <mxRectangle> to represent the geometry of a cell.
*
* For vertices, the geometry consists of the x- and y-location, and the width
* and height. For edges, the geometry consists of the optional terminal- and
* control points. The terminal points are only required if an edge is
* unconnected, and are stored in the <sourcePoint> and <targetPoint>
* variables, respectively.
*
* Example:
*
* If an edge is unconnected, that is, it has no source or target terminal,
* then a geometry with terminal points for a new edge can be defined as
* follows.
*
* (code)
* geometry.setTerminalPoint(new mxPoint(x1, y1), true);
* geometry.points = [new mxPoint(x2, y2)];
* geometry.setTerminalPoint(new mxPoint(x3, y3), false);
* (end)
*
* Control points are used regardless of the connected state of an edge and may
* be ignored or interpreted differently depending on the edge's <mxEdgeStyle>.
*
* To disable automatic reset of control points after a cell has been moved or
* resized, the the <mxGraph.resizeEdgesOnMove> and
* <mxGraph.resetEdgesOnResize> may be used.
*
* Edge Labels:
*
* Using the x- and y-coordinates of a cell's geometry, it is possible to
* position the label on edges on a specific location on the actual edge shape
* as it appears on the screen. The x-coordinate of an edge's geometry is used
* to describe the distance from the center of the edge from -1 to 1 with 0
* being the center of the edge and the default value. The y-coordinate of an
* edge's geometry is used to describe the absolute, orthogonal distance in
* pixels from that point. In addition, the <mxGeometry.offset> is used as an
* absolute offset vector from the resulting point.
*
* This coordinate system is applied if <relative> is true, otherwise the
* offset defines the absolute vector from the edge's center point to the
* label and the values for <x> and <y> are ignored.
*
* The width and height parameter for edge geometries can be used to set the
* label width and height (eg. for word wrapping).
*
* Ports:
*
* The term "port" refers to a relatively positioned, connectable child cell,
* which is used to specify the connection between the parent and another cell
* in the graph. Ports are typically modeled as vertices with relative
* geometries.
*
* Offsets:
*
* The <offset> field is interpreted in 3 different ways, depending on the cell
* and the geometry. For edges, the offset defines the absolute offset for the
* edge label. For relative geometries, the offset defines the absolute offset
* for the origin (top, left corner) of the vertex, otherwise the offset
* defines the absolute offset for the label inside the vertex or group.
*
* Constructor: mxGeometry
*
* Constructs a new object to describe the size and location of a vertex or
* the control points of an edge.
*/
function mxGeometry(x, y, width, height)
{
mxRectangle.call(this, x, y, width, height);
};
/**
* Extends mxRectangle.
*/
mxGeometry.prototype = new mxRectangle();
mxGeometry.prototype.constructor = mxGeometry;
/**
* Variable: TRANSLATE_CONTROL_POINTS
*
* Global switch to translate the points in translate. Default is true.
*/
mxGeometry.prototype.TRANSLATE_CONTROL_POINTS = true;
/**
* Variable: alternateBounds
*
* Stores alternate values for x, y, width and height in a rectangle. See
* <swap> to exchange the values. Default is null.
*/
mxGeometry.prototype.alternateBounds = null;
/**
* Variable: sourcePoint
*
* Defines the source <mxPoint> of the edge. This is used if the
* corresponding edge does not have a source vertex. Otherwise it is
* ignored. Default is null.
*/
mxGeometry.prototype.sourcePoint = null;
/**
* Variable: targetPoint
*
* Defines the target <mxPoint> of the edge. This is used if the
* corresponding edge does not have a target vertex. Otherwise it is
* ignored. Default is null.
*/
mxGeometry.prototype.targetPoint = null;
/**
* Variable: points
*
* Array of <mxPoints> which specifies the control points along the edge.
* These points are the intermediate points on the edge, for the endpoints
* use <targetPoint> and <sourcePoint> or set the terminals of the edge to
* a non-null value. Default is null.
*/
mxGeometry.prototype.points = null;
/**
* Variable: offset
*
* For edges, this holds the offset (in pixels) from the position defined
* by <x> and <y> on the edge. For relative geometries (for vertices), this
* defines the absolute offset from the point defined by the relative
* coordinates. For absolute geometries (for vertices), this defines the
* offset for the label. Default is null.
*/
mxGeometry.prototype.offset = null;
/**
* Variable: relative
*
* Specifies if the coordinates in the geometry are to be interpreted as
* relative coordinates. For edges, this is used to define the location of
* the edge label relative to the edge as rendered on the display. For
* vertices, this specifies the relative location inside the bounds of the
* parent cell.
*
* If this is false, then the coordinates are relative to the origin of the
* parent cell or, for edges, the edge label position is relative to the
* center of the edge as rendered on screen.
*
* Default is false.
*/
mxGeometry.prototype.relative = false;
/**
* Function: swap
*
* Swaps the x, y, width and height with the values stored in
* <alternateBounds> and puts the previous values into <alternateBounds> as
* a rectangle. This operation is carried-out in-place, that is, using the
* existing geometry instance. If this operation is called during a graph
* model transactional change, then the geometry should be cloned before
* calling this method and setting the geometry of the cell using
* <mxGraphModel.setGeometry>.
*/
mxGeometry.prototype.swap = function()
{
if (this.alternateBounds != null)
{
var old = new mxRectangle(
this.x, this.y, this.width, this.height);
this.x = this.alternateBounds.x;
this.y = this.alternateBounds.y;
this.width = this.alternateBounds.width;
this.height = this.alternateBounds.height;
this.alternateBounds = old;
}
};
/**
* Function: getTerminalPoint
*
* Returns the <mxPoint> representing the source or target point of this
* edge. This is only used if the edge has no source or target vertex.
*
* Parameters:
*
* isSource - Boolean that specifies if the source or target point
* should be returned.
*/
mxGeometry.prototype.getTerminalPoint = function(isSource)
{
return (isSource) ? this.sourcePoint : this.targetPoint;
};
/**
* Function: setTerminalPoint
*
* Sets the <sourcePoint> or <targetPoint> to the given <mxPoint> and
* returns the new point.
*
* Parameters:
*
* point - Point to be used as the new source or target point.
* isSource - Boolean that specifies if the source or target point
* should be set.
*/
mxGeometry.prototype.setTerminalPoint = function(point, isSource)
{
if (isSource)
{
this.sourcePoint = point;
}
else
{
this.targetPoint = point;
}
return point;
};
/**
* Function: rotate
*
* Rotates the geometry by the given angle around the given center. That is,
* <x> and <y> of the geometry, the <sourcePoint>, <targetPoint> and all
* <points> are translated by the given amount. <x> and <y> are only
* translated if <relative> is false.
*
* Parameters:
*
* angle - Number that specifies the rotation angle in degrees.
* cx - <mxPoint> that specifies the center of the rotation.
*/
mxGeometry.prototype.rotate = function(angle, cx)
{
var rad = mxUtils.toRadians(angle);
var cos = Math.cos(rad);
var sin = Math.sin(rad);
// Rotates the geometry
if (!this.relative)
{
var ct = new mxPoint(this.getCenterX(), this.getCenterY());
var pt = mxUtils.getRotatedPoint(ct, cos, sin, cx);
this.x = Math.round(pt.x - this.width / 2);
this.y = Math.round(pt.y - this.height / 2);
}
// Rotates the source point
if (this.sourcePoint != null)
{
var pt = mxUtils.getRotatedPoint(this.sourcePoint, cos, sin, cx);
this.sourcePoint.x = Math.round(pt.x);
this.sourcePoint.y = Math.round(pt.y);
}
// Translates the target point
if (this.targetPoint != null)
{
var pt = mxUtils.getRotatedPoint(this.targetPoint, cos, sin, cx);
this.targetPoint.x = Math.round(pt.x);
this.targetPoint.y = Math.round(pt.y);
}
// Translate the control points
if (this.points != null)
{
for (var i = 0; i < this.points.length; i++)
{
if (this.points[i] != null)
{
var pt = mxUtils.getRotatedPoint(this.points[i], cos, sin, cx);
this.points[i].x = Math.round(pt.x);
this.points[i].y = Math.round(pt.y);
}
}
}
};
/**
* Function: translate
*
* Translates the geometry by the specified amount. That is, <x> and <y> of the
* geometry, the <sourcePoint>, <targetPoint> and all <points> are translated
* by the given amount. <x> and <y> are only translated if <relative> is false.
* If <TRANSLATE_CONTROL_POINTS> is false, then <points> are not modified by
* this function.
*
* Parameters:
*
* dx - Number that specifies the x-coordinate of the translation.
* dy - Number that specifies the y-coordinate of the translation.
*/
mxGeometry.prototype.translate = function(dx, dy)
{
dx = parseFloat(dx);
dy = parseFloat(dy);
// Translates the geometry
if (!this.relative)
{
this.x = parseFloat(this.x) + dx;
this.y = parseFloat(this.y) + dy;
}
// Translates the source point
if (this.sourcePoint != null)
{
this.sourcePoint.x = parseFloat(this.sourcePoint.x) + dx;
this.sourcePoint.y = parseFloat(this.sourcePoint.y) + dy;
}
// Translates the target point
if (this.targetPoint != null)
{
this.targetPoint.x = parseFloat(this.targetPoint.x) + dx;
this.targetPoint.y = parseFloat(this.targetPoint.y) + dy;
}
// Translate the control points
if (this.TRANSLATE_CONTROL_POINTS && this.points != null)
{
for (var i = 0; i < this.points.length; i++)
{
if (this.points[i] != null)
{
this.points[i].x = parseFloat(this.points[i].x) + dx;
this.points[i].y = parseFloat(this.points[i].y) + dy;
}
}
}
};
/**
* Function: scale
*
* Scales the geometry by the given amount. That is, <x> and <y> of the
* geometry, the <sourcePoint>, <targetPoint> and all <points> are scaled
* by the given amount. <x>, <y>, <width> and <height> are only scaled if
* <relative> is false. If <fixedAspect> is true, then the smaller value
* is used to scale the width and the height.
*
* Parameters:
*
* sx - Number that specifies the horizontal scale factor.
* sy - Number that specifies the vertical scale factor.
* fixedAspect - Optional boolean to keep the aspect ratio fixed.
*/
mxGeometry.prototype.scale = function(sx, sy, fixedAspect)
{
sx = parseFloat(sx);
sy = parseFloat(sy);
// Translates the source point
if (this.sourcePoint != null)
{
this.sourcePoint.x = parseFloat(this.sourcePoint.x) * sx;
this.sourcePoint.y = parseFloat(this.sourcePoint.y) * sy;
}
// Translates the target point
if (this.targetPoint != null)
{
this.targetPoint.x = parseFloat(this.targetPoint.x) * sx;
this.targetPoint.y = parseFloat(this.targetPoint.y) * sy;
}
// Translate the control points
if (this.points != null)
{
for (var i = 0; i < this.points.length; i++)
{
if (this.points[i] != null)
{
this.points[i].x = parseFloat(this.points[i].x) * sx;
this.points[i].y = parseFloat(this.points[i].y) * sy;
}
}
}
// Translates the geometry
if (!this.relative)
{
this.x = parseFloat(this.x) * sx;
this.y = parseFloat(this.y) * sy;
if (fixedAspect)
{
sy = sx = Math.min(sx, sy);
}
this.width = parseFloat(this.width) * sx;
this.height = parseFloat(this.height) * sy;
}
};
/**
* Function: equals
*
* Returns true if the given object equals this geometry.
*/
mxGeometry.prototype.equals = function(obj)
{
return mxRectangle.prototype.equals.apply(this, arguments) &&
this.relative == obj.relative &&
((this.sourcePoint == null && obj.sourcePoint == null) || (this.sourcePoint != null && this.sourcePoint.equals(obj.sourcePoint))) &&
((this.targetPoint == null && obj.targetPoint == null) || (this.targetPoint != null && this.targetPoint.equals(obj.targetPoint))) &&
((this.points == null && obj.points == null) || (this.points != null && mxUtils.equalPoints(this.points, obj.points))) &&
((this.alternateBounds == null && obj.alternateBounds == null) || (this.alternateBounds != null && this.alternateBounds.equals(obj.alternateBounds))) &&
((this.offset == null && obj.offset == null) || (this.offset != null && this.offset.equals(obj.offset)));
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,797 @@
/**
* Copyright (c) 2006-2017, JGraph Ltd
* Copyright (c) 2006-2017, Gaudenz Alder
*/
var mxClient =
{
/**
* Class: mxClient
*
* Bootstrapping mechanism for the mxGraph thin client. The production version
* of this file contains all code required to run the mxGraph thin client, as
* well as global constants to identify the browser and operating system in
* use. You may have to load chrome://global/content/contentAreaUtils.js in
* your page to disable certain security restrictions in Mozilla.
*
* Variable: VERSION
*
* Contains the current version of the mxGraph library. The strings that
* communicate versions of mxGraph use the following format.
*
* versionMajor.versionMinor.buildNumber.revisionNumber
*
* Current version is 4.2.2.
*/
VERSION: '4.2.2',
/**
* Variable: IS_IE
*
* True if the current browser is Internet Explorer 10 or below. Use <mxClient.IS_IE11>
* to detect IE 11.
*/
IS_IE: navigator.userAgent != null && navigator.userAgent.indexOf('MSIE') >= 0,
/**
* Variable: IS_IE6
*
* True if the current browser is Internet Explorer 6.x.
*/
IS_IE6: navigator.userAgent != null && navigator.userAgent.indexOf('MSIE 6') >= 0,
/**
* Variable: IS_IE11
*
* True if the current browser is Internet Explorer 11.x.
*/
IS_IE11: navigator.userAgent != null && !!navigator.userAgent.match(/Trident\/7\./),
/**
* Variable: IS_EDGE
*
* True if the current browser is Microsoft Edge.
*/
IS_EDGE: navigator.userAgent != null && !!navigator.userAgent.match(/Edge\//),
/**
* Variable: IS_QUIRKS
*
* True if the current browser is Internet Explorer and it is in quirks mode.
*/
IS_QUIRKS: navigator.userAgent != null && navigator.userAgent.indexOf('MSIE') >= 0 &&
(document.documentMode == null || document.documentMode == 5),
/**
* Variable: IS_EM
*
* True if the browser is IE11 in enterprise mode (IE8 standards mode).
*/
IS_EM: 'spellcheck' in document.createElement('textarea') && document.documentMode == 8,
/**
* Variable: VML_PREFIX
*
* Prefix for VML namespace in node names. Default is 'v'.
*/
VML_PREFIX: 'v',
/**
* Variable: OFFICE_PREFIX
*
* Prefix for VML office namespace in node names. Default is 'o'.
*/
OFFICE_PREFIX: 'o',
/**
* Variable: IS_NS
*
* True if the current browser is Netscape (including Firefox).
*/
IS_NS: navigator.userAgent != null &&
navigator.userAgent.indexOf('Mozilla/') >= 0 &&
navigator.userAgent.indexOf('MSIE') < 0 &&
navigator.userAgent.indexOf('Edge/') < 0,
/**
* Variable: IS_OP
*
* True if the current browser is Opera.
*/
IS_OP: navigator.userAgent != null &&
(navigator.userAgent.indexOf('Opera/') >= 0 ||
navigator.userAgent.indexOf('OPR/') >= 0),
/**
* Variable: IS_OT
*
* True if -o-transform is available as a CSS style, ie for Opera browsers
* based on a Presto engine with version 2.5 or later.
*/
IS_OT: navigator.userAgent != null &&
navigator.userAgent.indexOf('Presto/') >= 0 &&
navigator.userAgent.indexOf('Presto/2.4.') < 0 &&
navigator.userAgent.indexOf('Presto/2.3.') < 0 &&
navigator.userAgent.indexOf('Presto/2.2.') < 0 &&
navigator.userAgent.indexOf('Presto/2.1.') < 0 &&
navigator.userAgent.indexOf('Presto/2.0.') < 0 &&
navigator.userAgent.indexOf('Presto/1.') < 0,
/**
* Variable: IS_SF
*
* True if the current browser is Safari.
*/
IS_SF: /Apple Computer, Inc/.test(navigator.vendor),
/**
* Variable: IS_ANDROID
*
* Returns true if the user agent contains Android.
*/
IS_ANDROID: navigator.appVersion.indexOf('Android') >= 0,
/**
* Variable: IS_IOS
*
* Returns true if the user agent is an iPad, iPhone or iPod.
*/
IS_IOS: (/iP(hone|od|ad)/.test(navigator.platform)),
/**
* Variable: IS_GC
*
* True if the current browser is Google Chrome.
*/
IS_GC: /Google Inc/.test(navigator.vendor),
/**
* Variable: IS_CHROMEAPP
*
* True if the this is running inside a Chrome App.
*/
IS_CHROMEAPP: window.chrome != null && chrome.app != null && chrome.app.runtime != null,
/**
* Variable: IS_FF
*
* True if the current browser is Firefox.
*/
IS_FF: typeof InstallTrigger !== 'undefined',
/**
* Variable: IS_MT
*
* True if -moz-transform is available as a CSS style. This is the case
* for all Firefox-based browsers newer than or equal 3, such as Camino,
* Iceweasel, Seamonkey and Iceape.
*/
IS_MT: (navigator.userAgent.indexOf('Firefox/') >= 0 &&
navigator.userAgent.indexOf('Firefox/1.') < 0 &&
navigator.userAgent.indexOf('Firefox/2.') < 0) ||
(navigator.userAgent.indexOf('Iceweasel/') >= 0 &&
navigator.userAgent.indexOf('Iceweasel/1.') < 0 &&
navigator.userAgent.indexOf('Iceweasel/2.') < 0) ||
(navigator.userAgent.indexOf('SeaMonkey/') >= 0 &&
navigator.userAgent.indexOf('SeaMonkey/1.') < 0) ||
(navigator.userAgent.indexOf('Iceape/') >= 0 &&
navigator.userAgent.indexOf('Iceape/1.') < 0),
/**
* Variable: IS_VML
*
* True if the browser supports VML.
*/
IS_VML: navigator.appName.toUpperCase() == 'MICROSOFT INTERNET EXPLORER',
/**
* Variable: IS_SVG
*
* True if the browser supports SVG.
*/
IS_SVG: navigator.appName.toUpperCase() != 'MICROSOFT INTERNET EXPLORER',
/**
* Variable: NO_FO
*
* True if foreignObject support is not available. This is the case for
* Opera, older SVG-based browsers and all versions of IE.
*/
NO_FO: !document.createElementNS || document.createElementNS('http://www.w3.org/2000/svg',
'foreignObject') != '[object SVGForeignObjectElement]' || navigator.userAgent.indexOf('Opera/') >= 0,
/**
* Variable: IS_WIN
*
* True if the client is a Windows.
*/
IS_WIN: navigator.appVersion.indexOf('Win') > 0,
/**
* Variable: IS_MAC
*
* True if the client is a Mac.
*/
IS_MAC: navigator.appVersion.indexOf('Mac') > 0,
/**
* Variable: IS_CHROMEOS
*
* True if the client is a Chrome OS.
*/
IS_CHROMEOS: /\bCrOS\b/.test(navigator.appVersion),
/**
* Variable: IS_TOUCH
*
* True if this device supports touchstart/-move/-end events (Apple iOS,
* Android, Chromebook and Chrome Browser on touch-enabled devices).
*/
IS_TOUCH: 'ontouchstart' in document.documentElement,
/**
* Variable: IS_POINTER
*
* True if this device supports Microsoft pointer events (always false on Macs).
*/
IS_POINTER: window.PointerEvent != null && !(navigator.appVersion.indexOf('Mac') > 0),
/**
* Variable: IS_LOCAL
*
* True if the documents location does not start with http:// or https://.
*/
IS_LOCAL: document.location.href.indexOf('http://') < 0 &&
document.location.href.indexOf('https://') < 0,
/**
* Variable: defaultBundles
*
* Contains the base names of the default bundles if mxLoadResources is false.
*/
defaultBundles: [],
/**
* Function: isBrowserSupported
*
* Returns true if the current browser is supported, that is, if
* <mxClient.IS_VML> or <mxClient.IS_SVG> is true.
*
* Example:
*
* (code)
* if (!mxClient.isBrowserSupported())
* {
* mxUtils.error('Browser is not supported!', 200, false);
* }
* (end)
*/
isBrowserSupported: function()
{
return mxClient.IS_VML || mxClient.IS_SVG;
},
/**
* Function: link
*
* Adds a link node to the head of the document. Use this
* to add a stylesheet to the page as follows:
*
* (code)
* mxClient.link('stylesheet', filename);
* (end)
*
* where filename is the (relative) URL of the stylesheet. The charset
* is hardcoded to ISO-8859-1 and the type is text/css.
*
* Parameters:
*
* rel - String that represents the rel attribute of the link node.
* href - String that represents the href attribute of the link node.
* doc - Optional parent document of the link node.
* id - unique id for the link element to check if it already exists
*/
link: function(rel, href, doc, id)
{
doc = doc || document;
// Workaround for Operation Aborted in IE6 if base tag is used in head
if (mxClient.IS_IE6)
{
doc.write('<link rel="' + rel + '" href="' + href + '" charset="UTF-8" type="text/css"/>');
}
else
{
var link = doc.createElement('link');
link.setAttribute('rel', rel);
link.setAttribute('href', href);
link.setAttribute('charset', 'UTF-8');
link.setAttribute('type', 'text/css');
if (id)
{
link.setAttribute('id', id);
}
var head = doc.getElementsByTagName('head')[0];
head.appendChild(link);
}
},
/**
* Function: loadResources
*
* Helper method to load the default bundles if mxLoadResources is false.
*
* Parameters:
*
* fn - Function to call after all resources have been loaded.
* lan - Optional string to pass to <mxResources.add>.
*/
loadResources: function(fn, lan)
{
var pending = mxClient.defaultBundles.length;
function callback()
{
if (--pending == 0)
{
fn();
}
}
for (var i = 0; i < mxClient.defaultBundles.length; i++)
{
mxResources.add(mxClient.defaultBundles[i], lan, callback);
}
},
/**
* Function: include
*
* Dynamically adds a script node to the document header.
*
* In production environments, the includes are resolved in the mxClient.js
* file to reduce the number of requests required for client startup. This
* function should only be used in development environments, but not in
* production systems.
*/
include: function(src)
{
document.write('<script src="'+src+'"></script>');
}
};
/**
* Variable: mxLoadResources
*
* Optional global config variable to toggle loading of the two resource files
* in <mxGraph> and <mxEditor>. Default is true. NOTE: This is a global variable,
* not a variable of mxClient. If this is false, you can use <mxClient.loadResources>
* with its callback to load the default bundles asynchronously.
*
* (code)
* <script type="text/javascript">
* var mxLoadResources = false;
* </script>
* <script type="text/javascript" src="/path/to/core/directory/js/mxClient.js"></script>
* (end)
*/
if (typeof(mxLoadResources) == 'undefined')
{
mxLoadResources = true;
}
/**
* Variable: mxForceIncludes
*
* Optional global config variable to force loading the JavaScript files in
* development mode. Default is undefined. NOTE: This is a global variable,
* not a variable of mxClient.
*
* (code)
* <script type="text/javascript">
* var mxLoadResources = true;
* </script>
* <script type="text/javascript" src="/path/to/core/directory/js/mxClient.js"></script>
* (end)
*/
if (typeof(mxForceIncludes) == 'undefined')
{
mxForceIncludes = false;
}
/**
* Variable: mxResourceExtension
*
* Optional global config variable to specify the extension of resource files.
* Default is true. NOTE: This is a global variable, not a variable of mxClient.
*
* (code)
* <script type="text/javascript">
* var mxResourceExtension = '.txt';
* </script>
* <script type="text/javascript" src="/path/to/core/directory/js/mxClient.js"></script>
* (end)
*/
if (typeof(mxResourceExtension) == 'undefined')
{
mxResourceExtension = '.txt';
}
/**
* Variable: mxLoadStylesheets
*
* Optional global config variable to toggle loading of the CSS files when
* the library is initialized. Default is true. NOTE: This is a global variable,
* not a variable of mxClient.
*
* (code)
* <script type="text/javascript">
* var mxLoadStylesheets = false;
* </script>
* <script type="text/javascript" src="/path/to/core/directory/js/mxClient.js"></script>
* (end)
*/
if (typeof(mxLoadStylesheets) == 'undefined')
{
mxLoadStylesheets = true;
}
/**
* Variable: basePath
*
* Basepath for all URLs in the core without trailing slash. Default is '.'.
* Set mxBasePath prior to loading the mxClient library as follows to override
* this setting:
*
* (code)
* <script type="text/javascript">
* mxBasePath = '/path/to/core/directory';
* </script>
* <script type="text/javascript" src="/path/to/core/directory/js/mxClient.js"></script>
* (end)
*
* When using a relative path, the path is relative to the URL of the page that
* contains the assignment. Trailing slashes are automatically removed.
*/
if (typeof(mxBasePath) != 'undefined' && mxBasePath.length > 0)
{
// Adds a trailing slash if required
if (mxBasePath.substring(mxBasePath.length - 1) == '/')
{
mxBasePath = mxBasePath.substring(0, mxBasePath.length - 1);
}
mxClient.basePath = mxBasePath;
}
else
{
mxClient.basePath = '.';
}
/**
* Variable: imageBasePath
*
* Basepath for all images URLs in the core without trailing slash. Default is
* <mxClient.basePath> + '/images'. Set mxImageBasePath prior to loading the
* mxClient library as follows to override this setting:
*
* (code)
* <script type="text/javascript">
* mxImageBasePath = '/path/to/image/directory';
* </script>
* <script type="text/javascript" src="/path/to/core/directory/js/mxClient.js"></script>
* (end)
*
* When using a relative path, the path is relative to the URL of the page that
* contains the assignment. Trailing slashes are automatically removed.
*/
if (typeof(mxImageBasePath) != 'undefined' && mxImageBasePath.length > 0)
{
// Adds a trailing slash if required
if (mxImageBasePath.substring(mxImageBasePath.length - 1) == '/')
{
mxImageBasePath = mxImageBasePath.substring(0, mxImageBasePath.length - 1);
}
mxClient.imageBasePath = mxImageBasePath;
}
else
{
mxClient.imageBasePath = mxClient.basePath + '/images';
}
/**
* Variable: language
*
* Defines the language of the client, eg. en for english, de for german etc.
* The special value 'none' will disable all built-in internationalization and
* resource loading. See <mxResources.getSpecialBundle> for handling identifiers
* with and without a dash.
*
* Set mxLanguage prior to loading the mxClient library as follows to override
* this setting:
*
* (code)
* <script type="text/javascript">
* mxLanguage = 'en';
* </script>
* <script type="text/javascript" src="js/mxClient.js"></script>
* (end)
*
* If internationalization is disabled, then the following variables should be
* overridden to reflect the current language of the system. These variables are
* cleared when i18n is disabled.
* <mxEditor.askZoomResource>, <mxEditor.lastSavedResource>,
* <mxEditor.currentFileResource>, <mxEditor.propertiesResource>,
* <mxEditor.tasksResource>, <mxEditor.helpResource>, <mxEditor.outlineResource>,
* <mxElbowEdgeHandler.doubleClickOrientationResource>, <mxUtils.errorResource>,
* <mxUtils.closeResource>, <mxGraphSelectionModel.doneResource>,
* <mxGraphSelectionModel.updatingSelectionResource>, <mxGraphView.doneResource>,
* <mxGraphView.updatingDocumentResource>, <mxCellRenderer.collapseExpandResource>,
* <mxGraph.containsValidationErrorsResource> and
* <mxGraph.alreadyConnectedResource>.
*/
if (typeof(mxLanguage) != 'undefined' && mxLanguage != null)
{
mxClient.language = mxLanguage;
}
else
{
mxClient.language = (mxClient.IS_IE) ? navigator.userLanguage : navigator.language;
}
/**
* Variable: defaultLanguage
*
* Defines the default language which is used in the common resource files. Any
* resources for this language will only load the common resource file, but not
* the language-specific resource file. Default is 'en'.
*
* Set mxDefaultLanguage prior to loading the mxClient library as follows to override
* this setting:
*
* (code)
* <script type="text/javascript">
* mxDefaultLanguage = 'de';
* </script>
* <script type="text/javascript" src="js/mxClient.js"></script>
* (end)
*/
if (typeof(mxDefaultLanguage) != 'undefined' && mxDefaultLanguage != null)
{
mxClient.defaultLanguage = mxDefaultLanguage;
}
else
{
mxClient.defaultLanguage = 'en';
}
// Adds all required stylesheets and namespaces
if (mxLoadStylesheets)
{
mxClient.link('stylesheet', mxClient.basePath + '/css/common.css');
}
/**
* Variable: languages
*
* Defines the optional array of all supported language extensions. The default
* language does not have to be part of this list. See
* <mxResources.isLanguageSupported>.
*
* (code)
* <script type="text/javascript">
* mxLanguages = ['de', 'it', 'fr'];
* </script>
* <script type="text/javascript" src="js/mxClient.js"></script>
* (end)
*
* This is used to avoid unnecessary requests to language files, ie. if a 404
* will be returned.
*/
if (typeof(mxLanguages) != 'undefined' && mxLanguages != null)
{
mxClient.languages = mxLanguages;
}
// Adds required namespaces, stylesheets and memory handling for older IE browsers
if (mxClient.IS_VML)
{
if (mxClient.IS_SVG)
{
mxClient.IS_VML = false;
}
else
{
// Enables support for IE8 standards mode. Note that this requires all attributes for VML
// elements to be set using direct notation, ie. node.attr = value, not setAttribute.
if (document.namespaces != null)
{
if (document.documentMode == 8)
{
document.namespaces.add(mxClient.VML_PREFIX, 'urn:schemas-microsoft-com:vml', '#default#VML');
document.namespaces.add(mxClient.OFFICE_PREFIX, 'urn:schemas-microsoft-com:office:office', '#default#VML');
}
else
{
document.namespaces.add(mxClient.VML_PREFIX, 'urn:schemas-microsoft-com:vml');
document.namespaces.add(mxClient.OFFICE_PREFIX, 'urn:schemas-microsoft-com:office:office');
}
}
// Workaround for limited number of stylesheets in IE (does not work in standards mode)
if (mxClient.IS_QUIRKS && document.styleSheets.length >= 30)
{
(function()
{
var node = document.createElement('style');
node.type = 'text/css';
node.styleSheet.cssText = mxClient.VML_PREFIX + '\\:*{behavior:url(#default#VML)}' +
mxClient.OFFICE_PREFIX + '\\:*{behavior:url(#default#VML)}';
document.getElementsByTagName('head')[0].appendChild(node);
})();
}
else
{
document.createStyleSheet().cssText = mxClient.VML_PREFIX + '\\:*{behavior:url(#default#VML)}' +
mxClient.OFFICE_PREFIX + '\\:*{behavior:url(#default#VML)}';
}
if (mxLoadStylesheets)
{
mxClient.link('stylesheet', mxClient.basePath + '/css/explorer.css');
}
}
}
// PREPROCESSOR-REMOVE-START
// If script is loaded via CommonJS, do not write <script> tags to the page
// for dependencies. These are already included in the build.
if (mxForceIncludes || !(typeof module === 'object' && module.exports != null))
{
// PREPROCESSOR-REMOVE-END
mxClient.include(mxClient.basePath+'/js/util/mxLog.js');
mxClient.include(mxClient.basePath+'/js/util/mxObjectIdentity.js');
mxClient.include(mxClient.basePath+'/js/util/mxDictionary.js');
mxClient.include(mxClient.basePath+'/js/util/mxResources.js');
mxClient.include(mxClient.basePath+'/js/util/mxPoint.js');
mxClient.include(mxClient.basePath+'/js/util/mxRectangle.js');
mxClient.include(mxClient.basePath+'/js/util/mxEffects.js');
mxClient.include(mxClient.basePath+'/js/util/mxUtils.js');
mxClient.include(mxClient.basePath+'/js/util/mxConstants.js');
mxClient.include(mxClient.basePath+'/js/util/mxEventObject.js');
mxClient.include(mxClient.basePath+'/js/util/mxMouseEvent.js');
mxClient.include(mxClient.basePath+'/js/util/mxEventSource.js');
mxClient.include(mxClient.basePath+'/js/util/mxEvent.js');
mxClient.include(mxClient.basePath+'/js/util/mxXmlRequest.js');
mxClient.include(mxClient.basePath+'/js/util/mxClipboard.js');
mxClient.include(mxClient.basePath+'/js/util/mxWindow.js');
mxClient.include(mxClient.basePath+'/js/util/mxForm.js');
mxClient.include(mxClient.basePath+'/js/util/mxImage.js');
mxClient.include(mxClient.basePath+'/js/util/mxDivResizer.js');
mxClient.include(mxClient.basePath+'/js/util/mxDragSource.js');
mxClient.include(mxClient.basePath+'/js/util/mxToolbar.js');
mxClient.include(mxClient.basePath+'/js/util/mxUndoableEdit.js');
mxClient.include(mxClient.basePath+'/js/util/mxUndoManager.js');
mxClient.include(mxClient.basePath+'/js/util/mxUrlConverter.js');
mxClient.include(mxClient.basePath+'/js/util/mxPanningManager.js');
mxClient.include(mxClient.basePath+'/js/util/mxPopupMenu.js');
mxClient.include(mxClient.basePath+'/js/util/mxAutoSaveManager.js');
mxClient.include(mxClient.basePath+'/js/util/mxAnimation.js');
mxClient.include(mxClient.basePath+'/js/util/mxMorphing.js');
mxClient.include(mxClient.basePath+'/js/util/mxImageBundle.js');
mxClient.include(mxClient.basePath+'/js/util/mxImageExport.js');
mxClient.include(mxClient.basePath+'/js/util/mxAbstractCanvas2D.js');
mxClient.include(mxClient.basePath+'/js/util/mxXmlCanvas2D.js');
mxClient.include(mxClient.basePath+'/js/util/mxSvgCanvas2D.js');
mxClient.include(mxClient.basePath+'/js/util/mxVmlCanvas2D.js');
mxClient.include(mxClient.basePath+'/js/util/mxGuide.js');
mxClient.include(mxClient.basePath+'/js/shape/mxShape.js');
mxClient.include(mxClient.basePath+'/js/shape/mxStencil.js');
mxClient.include(mxClient.basePath+'/js/shape/mxStencilRegistry.js');
mxClient.include(mxClient.basePath+'/js/shape/mxMarker.js');
mxClient.include(mxClient.basePath+'/js/shape/mxActor.js');
mxClient.include(mxClient.basePath+'/js/shape/mxCloud.js');
mxClient.include(mxClient.basePath+'/js/shape/mxRectangleShape.js');
mxClient.include(mxClient.basePath+'/js/shape/mxEllipse.js');
mxClient.include(mxClient.basePath+'/js/shape/mxDoubleEllipse.js');
mxClient.include(mxClient.basePath+'/js/shape/mxRhombus.js');
mxClient.include(mxClient.basePath+'/js/shape/mxPolyline.js');
mxClient.include(mxClient.basePath+'/js/shape/mxArrow.js');
mxClient.include(mxClient.basePath+'/js/shape/mxArrowConnector.js');
mxClient.include(mxClient.basePath+'/js/shape/mxText.js');
mxClient.include(mxClient.basePath+'/js/shape/mxTriangle.js');
mxClient.include(mxClient.basePath+'/js/shape/mxHexagon.js');
mxClient.include(mxClient.basePath+'/js/shape/mxLine.js');
mxClient.include(mxClient.basePath+'/js/shape/mxImageShape.js');
mxClient.include(mxClient.basePath+'/js/shape/mxLabel.js');
mxClient.include(mxClient.basePath+'/js/shape/mxCylinder.js');
mxClient.include(mxClient.basePath+'/js/shape/mxConnector.js');
mxClient.include(mxClient.basePath+'/js/shape/mxSwimlane.js');
mxClient.include(mxClient.basePath+'/js/layout/mxGraphLayout.js');
mxClient.include(mxClient.basePath+'/js/layout/mxStackLayout.js');
mxClient.include(mxClient.basePath+'/js/layout/mxPartitionLayout.js');
mxClient.include(mxClient.basePath+'/js/layout/mxCompactTreeLayout.js');
mxClient.include(mxClient.basePath+'/js/layout/mxRadialTreeLayout.js');
mxClient.include(mxClient.basePath+'/js/layout/mxFastOrganicLayout.js');
mxClient.include(mxClient.basePath+'/js/layout/mxCircleLayout.js');
mxClient.include(mxClient.basePath+'/js/layout/mxParallelEdgeLayout.js');
mxClient.include(mxClient.basePath+'/js/layout/mxCompositeLayout.js');
mxClient.include(mxClient.basePath+'/js/layout/mxEdgeLabelLayout.js');
mxClient.include(mxClient.basePath+'/js/layout/hierarchical/model/mxGraphAbstractHierarchyCell.js');
mxClient.include(mxClient.basePath+'/js/layout/hierarchical/model/mxGraphHierarchyNode.js');
mxClient.include(mxClient.basePath+'/js/layout/hierarchical/model/mxGraphHierarchyEdge.js');
mxClient.include(mxClient.basePath+'/js/layout/hierarchical/model/mxGraphHierarchyModel.js');
mxClient.include(mxClient.basePath+'/js/layout/hierarchical/model/mxSwimlaneModel.js');
mxClient.include(mxClient.basePath+'/js/layout/hierarchical/stage/mxHierarchicalLayoutStage.js');
mxClient.include(mxClient.basePath+'/js/layout/hierarchical/stage/mxMedianHybridCrossingReduction.js');
mxClient.include(mxClient.basePath+'/js/layout/hierarchical/stage/mxMinimumCycleRemover.js');
mxClient.include(mxClient.basePath+'/js/layout/hierarchical/stage/mxCoordinateAssignment.js');
mxClient.include(mxClient.basePath+'/js/layout/hierarchical/stage/mxSwimlaneOrdering.js');
mxClient.include(mxClient.basePath+'/js/layout/hierarchical/mxHierarchicalLayout.js');
mxClient.include(mxClient.basePath+'/js/layout/hierarchical/mxSwimlaneLayout.js');
mxClient.include(mxClient.basePath+'/js/model/mxGraphModel.js');
mxClient.include(mxClient.basePath+'/js/model/mxCell.js');
mxClient.include(mxClient.basePath+'/js/model/mxGeometry.js');
mxClient.include(mxClient.basePath+'/js/model/mxCellPath.js');
mxClient.include(mxClient.basePath+'/js/view/mxPerimeter.js');
mxClient.include(mxClient.basePath+'/js/view/mxPrintPreview.js');
mxClient.include(mxClient.basePath+'/js/view/mxStylesheet.js');
mxClient.include(mxClient.basePath+'/js/view/mxCellState.js');
mxClient.include(mxClient.basePath+'/js/view/mxGraphSelectionModel.js');
mxClient.include(mxClient.basePath+'/js/view/mxCellEditor.js');
mxClient.include(mxClient.basePath+'/js/view/mxCellRenderer.js');
mxClient.include(mxClient.basePath+'/js/view/mxEdgeStyle.js');
mxClient.include(mxClient.basePath+'/js/view/mxStyleRegistry.js');
mxClient.include(mxClient.basePath+'/js/view/mxGraphView.js');
mxClient.include(mxClient.basePath+'/js/view/mxGraph.js');
mxClient.include(mxClient.basePath+'/js/view/mxCellOverlay.js');
mxClient.include(mxClient.basePath+'/js/view/mxOutline.js');
mxClient.include(mxClient.basePath+'/js/view/mxMultiplicity.js');
mxClient.include(mxClient.basePath+'/js/view/mxLayoutManager.js');
mxClient.include(mxClient.basePath+'/js/view/mxSwimlaneManager.js');
mxClient.include(mxClient.basePath+'/js/view/mxTemporaryCellStates.js');
mxClient.include(mxClient.basePath+'/js/view/mxCellStatePreview.js');
mxClient.include(mxClient.basePath+'/js/view/mxConnectionConstraint.js');
mxClient.include(mxClient.basePath+'/js/handler/mxGraphHandler.js');
mxClient.include(mxClient.basePath+'/js/handler/mxPanningHandler.js');
mxClient.include(mxClient.basePath+'/js/handler/mxPopupMenuHandler.js');
mxClient.include(mxClient.basePath+'/js/handler/mxCellMarker.js');
mxClient.include(mxClient.basePath+'/js/handler/mxSelectionCellsHandler.js');
mxClient.include(mxClient.basePath+'/js/handler/mxConnectionHandler.js');
mxClient.include(mxClient.basePath+'/js/handler/mxConstraintHandler.js');
mxClient.include(mxClient.basePath+'/js/handler/mxRubberband.js');
mxClient.include(mxClient.basePath+'/js/handler/mxHandle.js');
mxClient.include(mxClient.basePath+'/js/handler/mxVertexHandler.js');
mxClient.include(mxClient.basePath+'/js/handler/mxEdgeHandler.js');
mxClient.include(mxClient.basePath+'/js/handler/mxElbowEdgeHandler.js');
mxClient.include(mxClient.basePath+'/js/handler/mxEdgeSegmentHandler.js');
mxClient.include(mxClient.basePath+'/js/handler/mxKeyHandler.js');
mxClient.include(mxClient.basePath+'/js/handler/mxTooltipHandler.js');
mxClient.include(mxClient.basePath+'/js/handler/mxCellTracker.js');
mxClient.include(mxClient.basePath+'/js/handler/mxCellHighlight.js');
mxClient.include(mxClient.basePath+'/js/editor/mxDefaultKeyHandler.js');
mxClient.include(mxClient.basePath+'/js/editor/mxDefaultPopupMenu.js');
mxClient.include(mxClient.basePath+'/js/editor/mxDefaultToolbar.js');
mxClient.include(mxClient.basePath+'/js/editor/mxEditor.js');
mxClient.include(mxClient.basePath+'/js/io/mxCodecRegistry.js');
mxClient.include(mxClient.basePath+'/js/io/mxCodec.js');
mxClient.include(mxClient.basePath+'/js/io/mxObjectCodec.js');
mxClient.include(mxClient.basePath+'/js/io/mxCellCodec.js');
mxClient.include(mxClient.basePath+'/js/io/mxModelCodec.js');
mxClient.include(mxClient.basePath+'/js/io/mxRootChangeCodec.js');
mxClient.include(mxClient.basePath+'/js/io/mxChildChangeCodec.js');
mxClient.include(mxClient.basePath+'/js/io/mxTerminalChangeCodec.js');
mxClient.include(mxClient.basePath+'/js/io/mxGenericChangeCodec.js');
mxClient.include(mxClient.basePath+'/js/io/mxGraphCodec.js');
mxClient.include(mxClient.basePath+'/js/io/mxGraphViewCodec.js');
mxClient.include(mxClient.basePath+'/js/io/mxStylesheetCodec.js');
mxClient.include(mxClient.basePath+'/js/io/mxDefaultKeyHandlerCodec.js');
mxClient.include(mxClient.basePath+'/js/io/mxDefaultToolbarCodec.js');
mxClient.include(mxClient.basePath+'/js/io/mxDefaultPopupMenuCodec.js');
mxClient.include(mxClient.basePath+'/js/io/mxEditorCodec.js');
// PREPROCESSOR-REMOVE-START
}
// PREPROCESSOR-REMOVE-END

View File

@ -0,0 +1,86 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxActor
*
* Extends <mxShape> to implement an actor shape. If a custom shape with one
* filled area is needed, then this shape's <redrawPath> should be overridden.
*
* Example:
*
* (code)
* function SampleShape() { }
*
* SampleShape.prototype = new mxActor();
* SampleShape.prototype.constructor = vsAseShape;
*
* mxCellRenderer.registerShape('sample', SampleShape);
* SampleShape.prototype.redrawPath = function(path, x, y, w, h)
* {
* path.moveTo(0, 0);
* path.lineTo(w, h);
* // ...
* path.close();
* }
* (end)
*
* This shape is registered under <mxConstants.SHAPE_ACTOR> in
* <mxCellRenderer>.
*
* Constructor: mxActor
*
* Constructs a new actor shape.
*
* Parameters:
*
* bounds - <mxRectangle> that defines the bounds. This is stored in
* <mxShape.bounds>.
* fill - String that defines the fill color. This is stored in <fill>.
* stroke - String that defines the stroke color. This is stored in <stroke>.
* strokewidth - Optional integer that defines the stroke width. Default is
* 1. This is stored in <strokewidth>.
*/
function mxActor(bounds, fill, stroke, strokewidth)
{
mxShape.call(this);
this.bounds = bounds;
this.fill = fill;
this.stroke = stroke;
this.strokewidth = (strokewidth != null) ? strokewidth : 1;
};
/**
* Extends mxShape.
*/
mxUtils.extend(mxActor, mxShape);
/**
* Function: paintVertexShape
*
* Redirects to redrawPath for subclasses to work.
*/
mxActor.prototype.paintVertexShape = function(c, x, y, w, h)
{
c.translate(x, y);
c.begin();
this.redrawPath(c, x, y, w, h);
c.fillAndStroke();
};
/**
* Function: redrawPath
*
* Draws the path for this shape.
*/
mxActor.prototype.redrawPath = function(c, x, y, w, h)
{
var width = w/3;
c.moveTo(0, h);
c.curveTo(0, 3 * h / 5, 0, 2 * h / 5, w / 2, 2 * h / 5);
c.curveTo(w / 2 - width, 2 * h / 5, w / 2 - width, 0, w / 2, 0);
c.curveTo(w / 2 + width, 0, w / 2 + width, 2 * h / 5, w / 2, 2 * h / 5);
c.curveTo(w, 2 * h / 5, w, 3 * h / 5, w, h);
c.close();
};

View File

@ -0,0 +1,115 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxArrow
*
* Extends <mxShape> to implement an arrow shape. (The shape
* is used to represent edges, not vertices.)
* This shape is registered under <mxConstants.SHAPE_ARROW>
* in <mxCellRenderer>.
*
* Constructor: mxArrow
*
* Constructs a new arrow shape.
*
* Parameters:
*
* points - Array of <mxPoints> that define the points. This is stored in
* <mxShape.points>.
* fill - String that defines the fill color. This is stored in <fill>.
* stroke - String that defines the stroke color. This is stored in <stroke>.
* strokewidth - Optional integer that defines the stroke width. Default is
* 1. This is stored in <strokewidth>.
* arrowWidth - Optional integer that defines the arrow width. Default is
* <mxConstants.ARROW_WIDTH>. This is stored in <arrowWidth>.
* spacing - Optional integer that defines the spacing between the arrow shape
* and its endpoints. Default is <mxConstants.ARROW_SPACING>. This is stored in
* <spacing>.
* endSize - Optional integer that defines the size of the arrowhead. Default
* is <mxConstants.ARROW_SIZE>. This is stored in <endSize>.
*/
function mxArrow(points, fill, stroke, strokewidth, arrowWidth, spacing, endSize)
{
mxShape.call(this);
this.points = points;
this.fill = fill;
this.stroke = stroke;
this.strokewidth = (strokewidth != null) ? strokewidth : 1;
this.arrowWidth = (arrowWidth != null) ? arrowWidth : mxConstants.ARROW_WIDTH;
this.spacing = (spacing != null) ? spacing : mxConstants.ARROW_SPACING;
this.endSize = (endSize != null) ? endSize : mxConstants.ARROW_SIZE;
};
/**
* Extends mxShape.
*/
mxUtils.extend(mxArrow, mxShape);
/**
* Function: augmentBoundingBox
*
* Augments the bounding box with the edge width and markers.
*/
mxArrow.prototype.augmentBoundingBox = function(bbox)
{
mxShape.prototype.augmentBoundingBox.apply(this, arguments);
var w = Math.max(this.arrowWidth, this.endSize);
bbox.grow((w / 2 + this.strokewidth) * this.scale);
};
/**
* Function: paintEdgeShape
*
* Paints the line shape.
*/
mxArrow.prototype.paintEdgeShape = function(c, pts)
{
// Geometry of arrow
var spacing = mxConstants.ARROW_SPACING;
var width = mxConstants.ARROW_WIDTH;
var arrow = mxConstants.ARROW_SIZE;
// Base vector (between end points)
var p0 = pts[0];
var pe = pts[pts.length - 1];
var dx = pe.x - p0.x;
var dy = pe.y - p0.y;
var dist = Math.sqrt(dx * dx + dy * dy);
var length = dist - 2 * spacing - arrow;
// Computes the norm and the inverse norm
var nx = dx / dist;
var ny = dy / dist;
var basex = length * nx;
var basey = length * ny;
var floorx = width * ny/3;
var floory = -width * nx/3;
// Computes points
var p0x = p0.x - floorx / 2 + spacing * nx;
var p0y = p0.y - floory / 2 + spacing * ny;
var p1x = p0x + floorx;
var p1y = p0y + floory;
var p2x = p1x + basex;
var p2y = p1y + basey;
var p3x = p2x + floorx;
var p3y = p2y + floory;
// p4 not necessary
var p5x = p3x - 3 * floorx;
var p5y = p3y - 3 * floory;
c.begin();
c.moveTo(p0x, p0y);
c.lineTo(p1x, p1y);
c.lineTo(p2x, p2y);
c.lineTo(p3x, p3y);
c.lineTo(pe.x - spacing * nx, pe.y - spacing * ny);
c.lineTo(p5x, p5y);
c.lineTo(p5x + floorx, p5y + floory);
c.close();
c.fillAndStroke();
};

View File

@ -0,0 +1,495 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxArrowConnector
*
* Extends <mxShape> to implement an new rounded arrow shape with support for
* waypoints and double arrows. (The shape is used to represent edges, not
* vertices.) This shape is registered under <mxConstants.SHAPE_ARROW_CONNECTOR>
* in <mxCellRenderer>.
*
* Constructor: mxArrowConnector
*
* Constructs a new arrow shape.
*
* Parameters:
*
* points - Array of <mxPoints> that define the points. This is stored in
* <mxShape.points>.
* fill - String that defines the fill color. This is stored in <fill>.
* stroke - String that defines the stroke color. This is stored in <stroke>.
* strokewidth - Optional integer that defines the stroke width. Default is
* 1. This is stored in <strokewidth>.
* arrowWidth - Optional integer that defines the arrow width. Default is
* <mxConstants.ARROW_WIDTH>. This is stored in <arrowWidth>.
* spacing - Optional integer that defines the spacing between the arrow shape
* and its endpoints. Default is <mxConstants.ARROW_SPACING>. This is stored in
* <spacing>.
* endSize - Optional integer that defines the size of the arrowhead. Default
* is <mxConstants.ARROW_SIZE>. This is stored in <endSize>.
*/
function mxArrowConnector(points, fill, stroke, strokewidth, arrowWidth, spacing, endSize)
{
mxShape.call(this);
this.points = points;
this.fill = fill;
this.stroke = stroke;
this.strokewidth = (strokewidth != null) ? strokewidth : 1;
this.arrowWidth = (arrowWidth != null) ? arrowWidth : mxConstants.ARROW_WIDTH;
this.arrowSpacing = (spacing != null) ? spacing : mxConstants.ARROW_SPACING;
this.startSize = mxConstants.ARROW_SIZE / 5;
this.endSize = mxConstants.ARROW_SIZE / 5;
};
/**
* Extends mxShape.
*/
mxUtils.extend(mxArrowConnector, mxShape);
/**
* Variable: useSvgBoundingBox
*
* Allows to use the SVG bounding box in SVG. Default is false for performance
* reasons.
*/
mxArrowConnector.prototype.useSvgBoundingBox = true;
/**
* Function: isRoundable
*
* Hook for subclassers.
*/
mxArrowConnector.prototype.isRoundable = function()
{
return true;
};
/**
* Variable: resetStyles
*
* Overrides mxShape to reset spacing.
*/
mxArrowConnector.prototype.resetStyles = function()
{
mxShape.prototype.resetStyles.apply(this, arguments);
this.arrowSpacing = mxConstants.ARROW_SPACING;
};
/**
* Overrides apply to get smooth transition from default start- and endsize.
*/
mxArrowConnector.prototype.apply = function(state)
{
mxShape.prototype.apply.apply(this, arguments);
if (this.style != null)
{
this.startSize = mxUtils.getNumber(this.style, mxConstants.STYLE_STARTSIZE, mxConstants.ARROW_SIZE / 5) * 3;
this.endSize = mxUtils.getNumber(this.style, mxConstants.STYLE_ENDSIZE, mxConstants.ARROW_SIZE / 5) * 3;
}
};
/**
* Function: augmentBoundingBox
*
* Augments the bounding box with the edge width and markers.
*/
mxArrowConnector.prototype.augmentBoundingBox = function(bbox)
{
mxShape.prototype.augmentBoundingBox.apply(this, arguments);
var w = this.getEdgeWidth();
if (this.isMarkerStart())
{
w = Math.max(w, this.getStartArrowWidth());
}
if (this.isMarkerEnd())
{
w = Math.max(w, this.getEndArrowWidth());
}
bbox.grow((w / 2 + this.strokewidth) * this.scale);
};
/**
* Function: paintEdgeShape
*
* Paints the line shape.
*/
mxArrowConnector.prototype.paintEdgeShape = function(c, pts)
{
// Geometry of arrow
var strokeWidth = this.strokewidth;
if (this.outline)
{
strokeWidth = Math.max(1, mxUtils.getNumber(this.style, mxConstants.STYLE_STROKEWIDTH, this.strokewidth));
}
var startWidth = this.getStartArrowWidth() + strokeWidth;
var endWidth = this.getEndArrowWidth() + strokeWidth;
var edgeWidth = this.outline ? this.getEdgeWidth() + strokeWidth : this.getEdgeWidth();
var openEnded = this.isOpenEnded();
var markerStart = this.isMarkerStart();
var markerEnd = this.isMarkerEnd();
var spacing = (openEnded) ? 0 : this.arrowSpacing + strokeWidth / 2;
var startSize = this.startSize + strokeWidth;
var endSize = this.endSize + strokeWidth;
var isRounded = this.isArrowRounded();
// Base vector (between first points)
var pe = pts[pts.length - 1];
// Finds first non-overlapping point
var i0 = 1;
while (i0 < pts.length - 1 && pts[i0].x == pts[0].x && pts[i0].y == pts[0].y)
{
i0++;
}
var dx = pts[i0].x - pts[0].x;
var dy = pts[i0].y - pts[0].y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist == 0)
{
return;
}
// Computes the norm and the inverse norm
var nx = dx / dist;
var nx2, nx1 = nx;
var ny = dy / dist;
var ny2, ny1 = ny;
var orthx = edgeWidth * ny;
var orthy = -edgeWidth * nx;
// Stores the inbound function calls in reverse order in fns
var fns = [];
if (isRounded)
{
c.setLineJoin('round');
}
else if (pts.length > 2)
{
// Only mitre if there are waypoints
c.setMiterLimit(1.42);
}
c.begin();
var startNx = nx;
var startNy = ny;
if (markerStart && !openEnded)
{
this.paintMarker(c, pts[0].x, pts[0].y, nx, ny, startSize, startWidth, edgeWidth, spacing, true);
}
else
{
var outStartX = pts[0].x + orthx / 2 + spacing * nx;
var outStartY = pts[0].y + orthy / 2 + spacing * ny;
var inEndX = pts[0].x - orthx / 2 + spacing * nx;
var inEndY = pts[0].y - orthy / 2 + spacing * ny;
if (openEnded)
{
c.moveTo(outStartX, outStartY);
fns.push(function()
{
c.lineTo(inEndX, inEndY);
});
}
else
{
c.moveTo(inEndX, inEndY);
c.lineTo(outStartX, outStartY);
}
}
var dx1 = 0;
var dy1 = 0;
var dist1 = 0;
for (var i = 0; i < pts.length - 2; i++)
{
// Work out in which direction the line is bending
var pos = mxUtils.relativeCcw(pts[i].x, pts[i].y, pts[i+1].x, pts[i+1].y, pts[i+2].x, pts[i+2].y);
dx1 = pts[i+2].x - pts[i+1].x;
dy1 = pts[i+2].y - pts[i+1].y;
dist1 = Math.sqrt(dx1 * dx1 + dy1 * dy1);
if (dist1 != 0)
{
nx1 = dx1 / dist1;
ny1 = dy1 / dist1;
var tmp1 = nx * nx1 + ny * ny1;
var tmp = Math.max(Math.sqrt((tmp1 + 1) / 2), 0.04);
// Work out the normal orthogonal to the line through the control point and the edge sides intersection
nx2 = (nx + nx1);
ny2 = (ny + ny1);
var dist2 = Math.sqrt(nx2 * nx2 + ny2 * ny2);
if (dist2 != 0)
{
nx2 = nx2 / dist2;
ny2 = ny2 / dist2;
// Higher strokewidths require a larger minimum bend, 0.35 covers all but the most extreme cases
var strokeWidthFactor = Math.max(tmp, Math.min(this.strokewidth / 200 + 0.04, 0.35));
var angleFactor = (pos != 0 && isRounded) ? Math.max(0.1, strokeWidthFactor) : Math.max(tmp, 0.06);
var outX = pts[i+1].x + ny2 * edgeWidth / 2 / angleFactor;
var outY = pts[i+1].y - nx2 * edgeWidth / 2 / angleFactor;
var inX = pts[i+1].x - ny2 * edgeWidth / 2 / angleFactor;
var inY = pts[i+1].y + nx2 * edgeWidth / 2 / angleFactor;
if (pos == 0 || !isRounded)
{
// If the two segments are aligned, or if we're not drawing curved sections between segments
// just draw straight to the intersection point
c.lineTo(outX, outY);
(function(x, y)
{
fns.push(function()
{
c.lineTo(x, y);
});
})(inX, inY);
}
else if (pos == -1)
{
var c1x = inX + ny * edgeWidth;
var c1y = inY - nx * edgeWidth;
var c2x = inX + ny1 * edgeWidth;
var c2y = inY - nx1 * edgeWidth;
c.lineTo(c1x, c1y);
c.quadTo(outX, outY, c2x, c2y);
(function(x, y)
{
fns.push(function()
{
c.lineTo(x, y);
});
})(inX, inY);
}
else
{
c.lineTo(outX, outY);
(function(x, y)
{
var c1x = outX - ny * edgeWidth;
var c1y = outY + nx * edgeWidth;
var c2x = outX - ny1 * edgeWidth;
var c2y = outY + nx1 * edgeWidth;
fns.push(function()
{
c.quadTo(x, y, c1x, c1y);
});
fns.push(function()
{
c.lineTo(c2x, c2y);
});
})(inX, inY);
}
nx = nx1;
ny = ny1;
}
}
}
orthx = edgeWidth * ny1;
orthy = - edgeWidth * nx1;
if (markerEnd && !openEnded)
{
this.paintMarker(c, pe.x, pe.y, -nx, -ny, endSize, endWidth, edgeWidth, spacing, false);
}
else
{
c.lineTo(pe.x - spacing * nx1 + orthx / 2, pe.y - spacing * ny1 + orthy / 2);
var inStartX = pe.x - spacing * nx1 - orthx / 2;
var inStartY = pe.y - spacing * ny1 - orthy / 2;
if (!openEnded)
{
c.lineTo(inStartX, inStartY);
}
else
{
c.moveTo(inStartX, inStartY);
fns.splice(0, 0, function()
{
c.moveTo(inStartX, inStartY);
});
}
}
for (var i = fns.length - 1; i >= 0; i--)
{
fns[i]();
}
if (openEnded)
{
c.end();
c.stroke();
}
else
{
c.close();
c.fillAndStroke();
}
// Workaround for shadow on top of base arrow
c.setShadow(false);
// Need to redraw the markers without the low miter limit
c.setMiterLimit(4);
if (isRounded)
{
c.setLineJoin('flat');
}
if (pts.length > 2)
{
// Only to repaint markers if no waypoints
// Need to redraw the markers without the low miter limit
c.setMiterLimit(4);
if (markerStart && !openEnded)
{
c.begin();
this.paintMarker(c, pts[0].x, pts[0].y, startNx, startNy, startSize, startWidth, edgeWidth, spacing, true);
c.stroke();
c.end();
}
if (markerEnd && !openEnded)
{
c.begin();
this.paintMarker(c, pe.x, pe.y, -nx, -ny, endSize, endWidth, edgeWidth, spacing, true);
c.stroke();
c.end();
}
}
};
/**
* Function: paintMarker
*
* Paints the marker.
*/
mxArrowConnector.prototype.paintMarker = function(c, ptX, ptY, nx, ny, size, arrowWidth, edgeWidth, spacing, initialMove)
{
var widthArrowRatio = edgeWidth / arrowWidth;
var orthx = edgeWidth * ny / 2;
var orthy = -edgeWidth * nx / 2;
var spaceX = (spacing + size) * nx;
var spaceY = (spacing + size) * ny;
if (initialMove)
{
c.moveTo(ptX - orthx + spaceX, ptY - orthy + spaceY);
}
else
{
c.lineTo(ptX - orthx + spaceX, ptY - orthy + spaceY);
}
c.lineTo(ptX - orthx / widthArrowRatio + spaceX, ptY - orthy / widthArrowRatio + spaceY);
c.lineTo(ptX + spacing * nx, ptY + spacing * ny);
c.lineTo(ptX + orthx / widthArrowRatio + spaceX, ptY + orthy / widthArrowRatio + spaceY);
c.lineTo(ptX + orthx + spaceX, ptY + orthy + spaceY);
}
/**
* Function: isArrowRounded
*
* Returns wether the arrow is rounded
*/
mxArrowConnector.prototype.isArrowRounded = function()
{
return this.isRounded;
};
/**
* Function: getStartArrowWidth
*
* Returns the width of the start arrow
*/
mxArrowConnector.prototype.getStartArrowWidth = function()
{
return mxConstants.ARROW_WIDTH;
};
/**
* Function: getEndArrowWidth
*
* Returns the width of the end arrow
*/
mxArrowConnector.prototype.getEndArrowWidth = function()
{
return mxConstants.ARROW_WIDTH;
};
/**
* Function: getEdgeWidth
*
* Returns the width of the body of the edge
*/
mxArrowConnector.prototype.getEdgeWidth = function()
{
return mxConstants.ARROW_WIDTH / 3;
};
/**
* Function: isOpenEnded
*
* Returns whether the ends of the shape are drawn
*/
mxArrowConnector.prototype.isOpenEnded = function()
{
return false;
};
/**
* Function: isMarkerStart
*
* Returns whether the start marker is drawn
*/
mxArrowConnector.prototype.isMarkerStart = function()
{
return (mxUtils.getValue(this.style, mxConstants.STYLE_STARTARROW, mxConstants.NONE) != mxConstants.NONE);
};
/**
* Function: isMarkerEnd
*
* Returns whether the end marker is drawn
*/
mxArrowConnector.prototype.isMarkerEnd = function()
{
return (mxUtils.getValue(this.style, mxConstants.STYLE_ENDARROW, mxConstants.NONE) != mxConstants.NONE);
};

View File

@ -0,0 +1,55 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxCloud
*
* Extends <mxActor> to implement a cloud shape.
*
* This shape is registered under <mxConstants.SHAPE_CLOUD> in
* <mxCellRenderer>.
*
* Constructor: mxCloud
*
* Constructs a new cloud shape.
*
* Parameters:
*
* bounds - <mxRectangle> that defines the bounds. This is stored in
* <mxShape.bounds>.
* fill - String that defines the fill color. This is stored in <fill>.
* stroke - String that defines the stroke color. This is stored in <stroke>.
* strokewidth - Optional integer that defines the stroke width. Default is
* 1. This is stored in <strokewidth>.
*/
function mxCloud(bounds, fill, stroke, strokewidth)
{
mxActor.call(this);
this.bounds = bounds;
this.fill = fill;
this.stroke = stroke;
this.strokewidth = (strokewidth != null) ? strokewidth : 1;
};
/**
* Extends mxActor.
*/
mxUtils.extend(mxCloud, mxActor);
/**
* Function: redrawPath
*
* Draws the path for this shape.
*/
mxCloud.prototype.redrawPath = function(c, x, y, w, h)
{
c.moveTo(0.25 * w, 0.25 * h);
c.curveTo(0.05 * w, 0.25 * h, 0, 0.5 * h, 0.16 * w, 0.55 * h);
c.curveTo(0, 0.66 * h, 0.18 * w, 0.9 * h, 0.31 * w, 0.8 * h);
c.curveTo(0.4 * w, h, 0.7 * w, h, 0.8 * w, 0.8 * h);
c.curveTo(w, 0.8 * h, w, 0.6 * h, 0.875 * w, 0.5 * h);
c.curveTo(w, 0.3 * h, 0.8 * w, 0.1 * h, 0.625 * w, 0.2 * h);
c.curveTo(0.5 * w, 0.05 * h, 0.3 * w, 0.05 * h, 0.25 * w, 0.25 * h);
c.close();
};

View File

@ -0,0 +1,149 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxConnector
*
* Extends <mxShape> to implement a connector shape. The connector
* shape allows for arrow heads on either side.
*
* This shape is registered under <mxConstants.SHAPE_CONNECTOR> in
* <mxCellRenderer>.
*
* Constructor: mxConnector
*
* Constructs a new connector shape.
*
* Parameters:
*
* points - Array of <mxPoints> that define the points. This is stored in
* <mxShape.points>.
* stroke - String that defines the stroke color. This is stored in <stroke>.
* Default is 'black'.
* strokewidth - Optional integer that defines the stroke width. Default is
* 1. This is stored in <strokewidth>.
*/
function mxConnector(points, stroke, strokewidth)
{
mxPolyline.call(this, points, stroke, strokewidth);
};
/**
* Extends mxPolyline.
*/
mxUtils.extend(mxConnector, mxPolyline);
/**
* Function: updateBoundingBox
*
* Updates the <boundingBox> for this shape using <createBoundingBox> and
* <augmentBoundingBox> and stores the result in <boundingBox>.
*/
mxConnector.prototype.updateBoundingBox = function()
{
this.useSvgBoundingBox = this.style != null && this.style[mxConstants.STYLE_CURVED] == 1;
mxShape.prototype.updateBoundingBox.apply(this, arguments);
};
/**
* Function: paintEdgeShape
*
* Paints the line shape.
*/
mxConnector.prototype.paintEdgeShape = function(c, pts)
{
// The indirection via functions for markers is needed in
// order to apply the offsets before painting the line and
// paint the markers after painting the line.
var sourceMarker = this.createMarker(c, pts, true);
var targetMarker = this.createMarker(c, pts, false);
mxPolyline.prototype.paintEdgeShape.apply(this, arguments);
// Disables shadows, dashed styles and fixes fill color for markers
c.setFillColor(this.stroke);
c.setShadow(false);
c.setDashed(false);
if (sourceMarker != null)
{
sourceMarker();
}
if (targetMarker != null)
{
targetMarker();
}
};
/**
* Function: createMarker
*
* Prepares the marker by adding offsets in pts and returning a function to
* paint the marker.
*/
mxConnector.prototype.createMarker = function(c, pts, source)
{
var result = null;
var n = pts.length;
var type = mxUtils.getValue(this.style, (source) ? mxConstants.STYLE_STARTARROW : mxConstants.STYLE_ENDARROW);
var p0 = (source) ? pts[1] : pts[n - 2];
var pe = (source) ? pts[0] : pts[n - 1];
if (type != null && p0 != null && pe != null)
{
var count = 1;
// Uses next non-overlapping point
while (count < n - 1 && Math.round(p0.x - pe.x) == 0 && Math.round(p0.y - pe.y) == 0)
{
p0 = (source) ? pts[1 + count] : pts[n - 2 - count];
count++;
}
// Computes the norm and the inverse norm
var dx = pe.x - p0.x;
var dy = pe.y - p0.y;
var dist = Math.max(1, Math.sqrt(dx * dx + dy * dy));
var unitX = dx / dist;
var unitY = dy / dist;
var size = mxUtils.getNumber(this.style, (source) ? mxConstants.STYLE_STARTSIZE : mxConstants.STYLE_ENDSIZE, mxConstants.DEFAULT_MARKERSIZE);
// Allow for stroke width in the end point used and the
// orthogonal vectors describing the direction of the marker
var filled = this.style[(source) ? mxConstants.STYLE_STARTFILL : mxConstants.STYLE_ENDFILL] != 0;
result = mxMarker.createMarker(c, this, type, pe, unitX, unitY, size, source, this.strokewidth, filled);
}
return result;
};
/**
* Function: augmentBoundingBox
*
* Augments the bounding box with the strokewidth and shadow offsets.
*/
mxConnector.prototype.augmentBoundingBox = function(bbox)
{
mxShape.prototype.augmentBoundingBox.apply(this, arguments);
// Adds marker sizes
var size = 0;
if (mxUtils.getValue(this.style, mxConstants.STYLE_STARTARROW, mxConstants.NONE) != mxConstants.NONE)
{
size = mxUtils.getNumber(this.style, mxConstants.STYLE_STARTSIZE, mxConstants.DEFAULT_MARKERSIZE) + 1;
}
if (mxUtils.getValue(this.style, mxConstants.STYLE_ENDARROW, mxConstants.NONE) != mxConstants.NONE)
{
size = Math.max(size, mxUtils.getNumber(this.style, mxConstants.STYLE_ENDSIZE, mxConstants.DEFAULT_MARKERSIZE)) + 1;
}
bbox.grow(size * this.scale);
};

View File

@ -0,0 +1,118 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxCylinder
*
* Extends <mxShape> to implement an cylinder shape. If a
* custom shape with one filled area and an overlay path is
* needed, then this shape's <redrawPath> should be overridden.
* This shape is registered under <mxConstants.SHAPE_CYLINDER>
* in <mxCellRenderer>.
*
* Constructor: mxCylinder
*
* Constructs a new cylinder shape.
*
* Parameters:
*
* bounds - <mxRectangle> that defines the bounds. This is stored in
* <mxShape.bounds>.
* fill - String that defines the fill color. This is stored in <fill>.
* stroke - String that defines the stroke color. This is stored in <stroke>.
* strokewidth - Optional integer that defines the stroke width. Default is
* 1. This is stored in <strokewidth>.
*/
function mxCylinder(bounds, fill, stroke, strokewidth)
{
mxShape.call(this);
this.bounds = bounds;
this.fill = fill;
this.stroke = stroke;
this.strokewidth = (strokewidth != null) ? strokewidth : 1;
};
/**
* Extends mxShape.
*/
mxUtils.extend(mxCylinder, mxShape);
/**
* Variable: maxHeight
*
* Defines the maximum height of the top and bottom part
* of the cylinder shape.
*/
mxCylinder.prototype.maxHeight = 40;
/**
* Variable: svgStrokeTolerance
*
* Sets stroke tolerance to 0 for SVG.
*/
mxCylinder.prototype.svgStrokeTolerance = 0;
/**
* Function: paintVertexShape
*
* Redirects to redrawPath for subclasses to work.
*/
mxCylinder.prototype.paintVertexShape = function(c, x, y, w, h)
{
c.translate(x, y);
c.begin();
this.redrawPath(c, x, y, w, h, false);
c.fillAndStroke();
if (!this.outline || this.style == null || mxUtils.getValue(
this.style, mxConstants.STYLE_BACKGROUND_OUTLINE, 0) == 0)
{
c.setShadow(false);
c.begin();
this.redrawPath(c, x, y, w, h, true);
c.stroke();
}
};
/**
* Function: getCylinderSize
*
* Returns the cylinder size.
*/
mxCylinder.prototype.getCylinderSize = function(x, y, w, h)
{
return Math.min(this.maxHeight, Math.round(h / 5));
};
/**
* Function: redrawPath
*
* Draws the path for this shape.
*/
mxCylinder.prototype.redrawPath = function(c, x, y, w, h, isForeground)
{
var dy = this.getCylinderSize(x, y, w, h);
if ((isForeground && this.fill != null) || (!isForeground && this.fill == null))
{
c.moveTo(0, dy);
c.curveTo(0, 2 * dy, w, 2 * dy, w, dy);
// Needs separate shapes for correct hit-detection
if (!isForeground)
{
c.stroke();
c.begin();
}
}
if (!isForeground)
{
c.moveTo(0, dy);
c.curveTo(0, -dy / 3, w, -dy / 3, w, dy);
c.lineTo(w, h - dy);
c.curveTo(w, h + dy / 3, 0, h + dy / 3, 0, h - dy);
c.close();
}
};

View File

@ -0,0 +1,114 @@
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxDoubleEllipse
*
* Extends <mxShape> to implement a double ellipse shape. This shape is
* registered under <mxConstants.SHAPE_DOUBLE_ELLIPSE> in <mxCellRenderer>.
* Use the following override to only fill the inner ellipse in this shape:
*
* (code)
* mxDoubleEllipse.prototype.paintVertexShape = function(c, x, y, w, h)
* {
* c.ellipse(x, y, w, h);
* c.stroke();
*
* var inset = mxUtils.getValue(this.style, mxConstants.STYLE_MARGIN, Math.min(3 + this.strokewidth, Math.min(w / 5, h / 5)));
* x += inset;
* y += inset;
* w -= 2 * inset;
* h -= 2 * inset;
*
* if (w > 0 && h > 0)
* {
* c.ellipse(x, y, w, h);
* }
*
* c.fillAndStroke();
* };
* (end)
*
* Constructor: mxDoubleEllipse
*
* Constructs a new ellipse shape.
*
* Parameters:
*
* bounds - <mxRectangle> that defines the bounds. This is stored in
* <mxShape.bounds>.
* fill - String that defines the fill color. This is stored in <fill>.
* stroke - String that defines the stroke color. This is stored in <stroke>.
* strokewidth - Optional integer that defines the stroke width. Default is
* 1. This is stored in <strokewidth>.
*/
function mxDoubleEllipse(bounds, fill, stroke, strokewidth)
{
mxShape.call(this);
this.bounds = bounds;
this.fill = fill;
this.stroke = stroke;
this.strokewidth = (strokewidth != null) ? strokewidth : 1;
};
/**
* Extends mxShape.
*/
mxUtils.extend(mxDoubleEllipse, mxShape);
/**
* Variable: vmlScale
*
* Scale for improving the precision of VML rendering. Default is 10.
*/
mxDoubleEllipse.prototype.vmlScale = 10;
/**
* Function: paintBackground
*
* Paints the background.
*/
mxDoubleEllipse.prototype.paintBackground = function(c, x, y, w, h)
{
c.ellipse(x, y, w, h);
c.fillAndStroke();
};
/**
* Function: paintForeground
*
* Paints the foreground.
*/
mxDoubleEllipse.prototype.paintForeground = function(c, x, y, w, h)
{
if (!this.outline)
{
var margin = mxUtils.getValue(this.style, mxConstants.STYLE_MARGIN, Math.min(3 + this.strokewidth, Math.min(w / 5, h / 5)));
x += margin;
y += margin;
w -= 2 * margin;
h -= 2 * margin;
// FIXME: Rounding issues in IE8 standards mode (not in 1.x)
if (w > 0 && h > 0)
{
c.ellipse(x, y, w, h);
}
c.stroke();
}
};
/**
* Function: getLabelBounds
*
* Returns the bounds for the label.
*/
mxDoubleEllipse.prototype.getLabelBounds = function(rect)
{
var margin = (mxUtils.getValue(this.style, mxConstants.STYLE_MARGIN, Math.min(3 + this.strokewidth,
Math.min(rect.width / 5 / this.scale, rect.height / 5 / this.scale)))) * this.scale;
return new mxRectangle(rect.x + margin, rect.y + margin, rect.width - 2 * margin, rect.height - 2 * margin);
};

Some files were not shown because too many files have changed in this diff Show More