add the Schematic function
|
@ -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>
|
|
|
@ -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>');
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
10
README.md
|
@ -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的内容
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
## 有用的mxgraph's demo
|
# 有用的mxgraph's demo
|
||||||
|
|
||||||
* dynamictoolbar.html
|
* dynamictoolbar.html
|
||||||
* permissions.html
|
* permissions.html
|
|
@ -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()
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
After Width: | Height: | Size: 137 B |
After Width: | Height: | Size: 70 B |
After Width: | Height: | Size: 877 B |
After Width: | Height: | Size: 907 B |
After Width: | Height: | Size: 878 B |
After Width: | Height: | Size: 843 B |
After Width: | Height: | Size: 64 B |
After Width: | Height: | Size: 845 B |
After Width: | Height: | Size: 55 B |
After Width: | Height: | Size: 74 B |
After Width: | Height: | Size: 146 B |
After Width: | Height: | Size: 56 B |
After Width: | Height: | Size: 90 B |
After Width: | Height: | Size: 276 B |
After Width: | Height: | Size: 425 B |
After Width: | Height: | Size: 275 B |
After Width: | Height: | Size: 75 B |
|
@ -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;
|
||||||
|
};
|
|
@ -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;
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
|
@ -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();
|
||||||
|
};
|
|
@ -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);
|
||||||
|
}
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -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);
|
||||||
|
}
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
|
@ -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;
|
||||||
|
};
|
|
@ -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);
|
||||||
|
};
|
|
@ -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);
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
|
@ -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 it’s
|
||||||
|
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)
|
|
@ -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;
|
||||||
|
|
||||||
|
}());
|
|
@ -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;
|
||||||
|
|
||||||
|
}());
|
|
@ -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);
|
||||||
|
}
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
|
@ -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;
|
||||||
|
|
||||||
|
}());
|
|
@ -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;
|
||||||
|
|
||||||
|
}());
|
|
@ -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;
|
|
@ -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;
|
||||||
|
|
||||||
|
}());
|
|
@ -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'));
|
|
@ -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']);
|
||||||
|
|
||||||
|
}());
|
|
@ -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;
|
||||||
|
|
||||||
|
}());
|
|
@ -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;
|
||||||
|
|
||||||
|
}());
|
|
@ -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;
|
||||||
|
|
||||||
|
}());
|
|
@ -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;
|
|
@ -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;
|
||||||
|
|
||||||
|
}());
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
|
@ -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;
|
||||||
|
};
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -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;
|
||||||
|
};
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -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;
|
||||||
|
};
|
|
@ -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;
|
||||||
|
};
|
|
@ -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) { };
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
|
@ -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);
|
||||||
|
};
|
|
@ -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);
|
||||||
|
};
|
|
@ -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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -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();
|
||||||
|
}
|
||||||
|
};
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -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);
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
|
@ -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)]);
|
||||||
|
}
|
||||||
|
};
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -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);
|
||||||
|
}
|
||||||
|
};
|
|
@ -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);
|
||||||
|
}
|
||||||
|
};
|
|
@ -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;
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
|
@ -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)));
|
||||||
|
};
|
|
@ -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
|
|
@ -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();
|
||||||
|
};
|
|
@ -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();
|
||||||
|
};
|
|
@ -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);
|
||||||
|
};
|
|
@ -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();
|
||||||
|
};
|
|
@ -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);
|
||||||
|
};
|
|
@ -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();
|
||||||
|
}
|
||||||
|
};
|
|
@ -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);
|
||||||
|
};
|