RealtimeMapping.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965
  1. /**
  2. * Copyright (c) 2006-2016, JGraph Ltd
  3. * Copyright (c) 2006-2016, Gaudenz Alder
  4. */
  5. /**
  6. * Creates an object that maps all changes from the given diagramMap to the
  7. * given graph model.
  8. */
  9. function RealtimeMapping(driveRealtime, diagramMap, page)
  10. {
  11. this.driveRealtime = driveRealtime;
  12. this.diagramMap = diagramMap;
  13. this.page = page;
  14. this.graphModel = new mxGraphModel();
  15. if (page.root != null)
  16. {
  17. this.graphModel.setRoot(page.root);
  18. }
  19. this.ui = this.driveRealtime.ui;
  20. this.root = this.driveRealtime.root;
  21. this.graph = this.driveRealtime.graph;
  22. this.rtModel = this.driveRealtime.rtModel;
  23. };
  24. /**
  25. * Specifies the key of the root element in the model. Default is root.
  26. */
  27. RealtimeMapping.prototype.driveRealtime = null;
  28. /**
  29. * Specifies the key of the root element in the model. Default is root.
  30. */
  31. RealtimeMapping.prototype.diagramMap = null;
  32. /**
  33. * Specifies the key of the root element in the model. Default is root.
  34. */
  35. RealtimeMapping.prototype.page = null;
  36. /**
  37. * Specifies the key of the root element in the model. Default is root.
  38. */
  39. RealtimeMapping.prototype.graphModel = null;
  40. /**
  41. * Specifies the key of the root element in the model. Default is root.
  42. */
  43. RealtimeMapping.prototype.needsUpdate = true;
  44. /**
  45. * Specifies the key of the root element in the model. Default is root.
  46. */
  47. RealtimeMapping.prototype.selectionMap = null;
  48. /**
  49. * Synchronizes the collaboration model and the graph model and installs
  50. * the required listeners to keep them in sync.
  51. */
  52. RealtimeMapping.prototype.init = function()
  53. {
  54. this.diagramMap.addEventListener(gapi.drive.realtime.EventType.VALUE_CHANGED, mxUtils.bind(this, function(evt)
  55. {
  56. if (!this.driveRealtime.isLocalEvent(evt))
  57. {
  58. if (evt.property == this.driveRealtime.rootKey && evt.newValue != null)
  59. {
  60. this.beginUpdate();
  61. this.initGraph();
  62. this.needsUpdate = true;
  63. }
  64. else if (evt.property == 'name' && evt.newValue != null)
  65. {
  66. this.driveRealtime.ignoreChange = true;
  67. this.graph.model.execute(new RenamePage(this.ui, this.page, evt.newValue));
  68. this.driveRealtime.ignoreChange = false;
  69. }
  70. else if (this.isActive() && evt.newValue != null)
  71. {
  72. if (evt.property == 'pageFormat')
  73. {
  74. this.realtimePageFormatChanged(evt.newValue);
  75. }
  76. else if (evt.property == 'pageScale')
  77. {
  78. this.realtimePageScaleChanged(evt.newValue);
  79. }
  80. else if (evt.property == 'backgroundColor')
  81. {
  82. this.realtimeBackgroundColorChanged(evt.newValue);
  83. }
  84. else if (evt.property == 'shadowVisible')
  85. {
  86. this.realtimeShadowVisibleChanged(evt.newValue);
  87. }
  88. else if (evt.property == 'foldingEnabled')
  89. {
  90. this.realtimeFoldingEnabledChanged(evt.newValue);
  91. }
  92. else if (evt.property == 'backgroundImage')
  93. {
  94. this.realtimeBackgroundImageChanged(evt.newValue);
  95. }
  96. else if (evt.property == 'mathEnabled')
  97. {
  98. this.realtimeMathEnabledChanged(evt.newValue);
  99. }
  100. }
  101. // Marks the mapping dirty regardless of active state
  102. if (evt.newValue != null && (evt.property == 'pageFormat' ||
  103. evt.property == 'pageScale' || evt.property == 'shadowVisible' ||
  104. evt.property == 'backgroundColor' || evt.property == 'foldingEnabled' ||
  105. evt.property == 'backgroundImage' || evt.property == 'mathEnabled'))
  106. {
  107. this.needsUpdate = true;
  108. }
  109. }
  110. }));
  111. if (this.diagramMap.has(this.driveRealtime.rootKey))
  112. {
  113. this.initGraph();
  114. }
  115. else
  116. {
  117. this.initRealtime();
  118. }
  119. this.page.root = this.graphModel.getRoot();
  120. this.selectionMap = this.diagramMap.get('select');
  121. if (this.selectionMap == null)
  122. {
  123. this.initializeSelection();
  124. }
  125. // Resets selection state to ensure change event
  126. if (this.driveRealtime.file.isEditable())
  127. {
  128. this.selectionMap.set(this.driveRealtime.userId, '');
  129. }
  130. this.installRemoteSelectionListener();
  131. };
  132. /**
  133. *
  134. */
  135. RealtimeMapping.prototype.initializeSelection = function()
  136. {
  137. this.selectionMap = this.rtModel.createMap();
  138. if (this.driveRealtime.file.isEditable())
  139. {
  140. this.diagramMap.set('select', this.selectionMap);
  141. }
  142. //this.log('Selection list created');
  143. };
  144. /**
  145. * Adds a listener for changes to the RT selection map to highlight
  146. * remote selection
  147. */
  148. RealtimeMapping.prototype.installRemoteSelectionListener = function()
  149. {
  150. this.selectionMap.addEventListener(gapi.drive.realtime.EventType.VALUE_CHANGED, mxUtils.bind(this, function(evt)
  151. {
  152. if (!this.driveRealtime.isLocalEvent(evt) && evt.newValue != null && (this.ui.currentPage == null ||
  153. this.ui.currentPage == this.page))
  154. {
  155. var cellIds = evt.newValue.split(',');
  156. for (var i = 0; i < cellIds.length; i++)
  157. {
  158. this.driveRealtime.highlight(this.driveRealtime.model.getCell(cellIds[i]), evt.sessionId);
  159. }
  160. }
  161. }));
  162. };
  163. /**
  164. * Returns true if this diagram is being displayed.
  165. */
  166. RealtimeMapping.prototype.isActive = function()
  167. {
  168. return this.ui.currentPage == null || this.ui.currentPage.mapping == this;
  169. };
  170. /**
  171. * Syncs initial state from collab model to graph model.
  172. */
  173. RealtimeMapping.prototype.getGraphModel = function()
  174. {
  175. return (this.isActive()) ? this.driveRealtime.model : this.graphModel;
  176. };
  177. /**
  178. * Syncs initial state from collab model to graph model.
  179. */
  180. RealtimeMapping.prototype.initGraph = function()
  181. {
  182. if (this.isActive())
  183. {
  184. this.activate(true);
  185. mxClient.NO_FO = (this.graph.mathEnabled) ? true : Editor.prototype.originalNoForeignObject;
  186. // TODO: Fixes math offset - why?
  187. this.ui.editor.graph.sizeDidChange();
  188. }
  189. var rtRoot = this.diagramMap.get(this.driveRealtime.rootKey);
  190. this.createCell(rtRoot);
  191. this.restoreCell(rtRoot);
  192. // Stores root in current model and local model
  193. var gm = this.getGraphModel();
  194. gm.setRoot(rtRoot.cell);
  195. if (gm != this.graphModel)
  196. {
  197. this.graphModel.setRoot(gm.getRoot());
  198. }
  199. };
  200. /**
  201. * Writes the graph properties from the realtime model to the given mxGraphModel node.
  202. */
  203. RealtimeMapping.prototype.writeRealtimeToNode = function(node)
  204. {
  205. node.setAttribute('shadow', this.diagramMap.get('shadowVisible'));
  206. node.setAttribute('fold', this.diagramMap.get('foldingEnabled'));
  207. node.setAttribute('math', this.diagramMap.get('mathEnabled'));
  208. node.setAttribute('pageScale', this.diagramMap.get('pageScale'));
  209. var img = this.diagramMap.get('backgroundImage');
  210. if (img != null && img.length > 0)
  211. {
  212. node.setAttribute('backgroundImage', img);
  213. }
  214. var color = this.diagramMap.get('backgroundColor');
  215. if (color != null)
  216. {
  217. node.setAttribute('background', color);
  218. }
  219. var pf = this.diagramMap.get('pageFormat');
  220. if (pf != null)
  221. {
  222. var values = pf.split(',');
  223. if (values.length > 1)
  224. {
  225. node.setAttribute('pageWidth', parseInt(values[0]));
  226. node.setAttribute('pageHeight', parseInt(values[1]));
  227. }
  228. }
  229. };
  230. /**
  231. * Writes the graph properties from the realtime model to the given mxGraphModel node.
  232. */
  233. RealtimeMapping.prototype.writeNodeToRealtime = function(node)
  234. {
  235. this.diagramMap.set('shadowVisible', node.getAttribute('shadow'));
  236. this.diagramMap.set('foldingEnabled', node.getAttribute('fold'));
  237. this.diagramMap.set('mathEnabled', node.getAttribute('math'));
  238. this.diagramMap.set('pageScale', node.getAttribute('pageScale'));
  239. var img = node.getAttribute('backgroundImage');
  240. if (img != null && img.length > 0)
  241. {
  242. this.diagramMap.set('backgroundImage', img);
  243. }
  244. var color = node.getAttribute('background');
  245. if (color != null)
  246. {
  247. this.diagramMap.set('backgroundColor', color);
  248. }
  249. this.diagramMap.set('pageFormat', node.getAttribute('pageWidth') + ',' +
  250. node.getAttribute('pageHeight'));
  251. };
  252. /**
  253. * Syncs initial state from collab model to graph model.
  254. */
  255. RealtimeMapping.prototype.activate = function(quiet)
  256. {
  257. this.realtimePageFormatChanged(this.diagramMap.get('pageFormat'), quiet);
  258. this.realtimePageScaleChanged(this.diagramMap.get('pageScale'), quiet);
  259. this.realtimeMathEnabledChanged(this.diagramMap.get('mathEnabled'), quiet);
  260. this.realtimeBackgroundColorChanged(this.diagramMap.get('backgroundColor'), quiet);
  261. this.realtimeShadowVisibleChanged(this.diagramMap.get('shadowVisible'), quiet);
  262. this.realtimeFoldingEnabledChanged(this.diagramMap.get('foldingEnabled'), quiet);
  263. this.realtimeBackgroundImageChanged(this.diagramMap.get('backgroundImage'), quiet);
  264. };
  265. /**
  266. * Syncs initial state from graph model to collab model.
  267. */
  268. RealtimeMapping.prototype.initRealtime = function()
  269. {
  270. this.rtModel.beginCompoundOperation();
  271. try
  272. {
  273. var rtCell = this.createRealtimeCell(this.getGraphModel().getRoot());
  274. this.saveRealtimeCell(rtCell.cell);
  275. this.diagramMap.set(this.driveRealtime.rootKey, rtCell);
  276. if (this.page.graphModelNode != null)
  277. {
  278. this.writeNodeToRealtime(this.page.graphModelNode);
  279. }
  280. else
  281. {
  282. this.diagramMap.set('shadowVisible', (this.graph.shadowVisible) ? '1' : '0');
  283. this.diagramMap.set('foldingEnabled', (this.graph.foldingEnabled) ? '1' : '0');
  284. this.diagramMap.set('mathEnabled', (this.graph.mathEnabled) ? '1' : '0');
  285. this.diagramMap.set('pageScale', this.graph.pageScale);
  286. this.diagramMap.set('backgroundImage', (this.graph.backgroundImage != null) ? JSON.stringify(this.graph.backgroundImage) : '');
  287. this.diagramMap.set('backgroundColor', (this.graph.background != null) ? this.graph.background : '');
  288. this.diagramMap.set('pageFormat', this.graph.pageFormat.width + ',' + this.graph.pageFormat.height);
  289. }
  290. this.root.set('modifiedDate', new Date().getTime());
  291. this.rtModel.endCompoundOperation();
  292. }
  293. catch (e)
  294. {
  295. this.rtModel.endCompoundOperation();
  296. this.ui.handleError(e);
  297. }
  298. };
  299. /**
  300. * Syncs initial state from graph model to collab model.
  301. */
  302. RealtimeMapping.prototype.createRealtimeCell = function(cell)
  303. {
  304. var rtCell = cell.rtCell;
  305. if (rtCell == null)
  306. {
  307. rtCell = this.rtModel.create('Cell');
  308. rtCell.children = this.rtModel.createList();
  309. rtCell.cell = cell;
  310. cell.rtCell = rtCell;
  311. rtCell.cellId = cell.id;
  312. rtCell.type = (cell.vertex) ? 'vertex' : ((cell.edge) ? 'edge' : '');
  313. rtCell.connectable = (cell.connectable == null || cell.connectable) ? '1' : '0';
  314. if (mxUtils.isNode(cell.value))
  315. {
  316. rtCell.xmlValue = mxUtils.getXml(cell.value);
  317. }
  318. else if (cell.value != null)
  319. {
  320. rtCell.value = cell.value;
  321. }
  322. rtCell.style = (cell.style != null) ? cell.style : null;
  323. rtCell.geometry = (cell.geometry != null) ? mxUtils.getXml(this.driveRealtime.codec.encode(cell.geometry)) : null;
  324. rtCell.visible = (cell.visible == null || cell.visible) ? '1' : '0';
  325. rtCell.collapsed = (cell.collapsed != null && cell.collapsed) ? '1' : '0';
  326. for (var i = 0; i < this.graphModel.getChildCount(cell); i++)
  327. {
  328. var child = this.graphModel.getChildAt(cell, i);
  329. this.createRealtimeCell(child);
  330. if (child.rtCell.parent == null)
  331. {
  332. child.rtCell.parent = rtCell;
  333. rtCell.children.push(child.rtCell);
  334. }
  335. }
  336. this.installRealtimeCellListeners(rtCell);
  337. }
  338. return rtCell;
  339. };
  340. /**
  341. * Syncs initial state from graph model to collab model.
  342. */
  343. RealtimeMapping.prototype.saveRealtimeCell = function(cell)
  344. {
  345. if (cell.source != null)
  346. {
  347. if (cell.source.rtCell == null)
  348. {
  349. this.createRealtimeCell(cell.source);
  350. }
  351. cell.rtCell.source = cell.source.rtCell;
  352. }
  353. else
  354. {
  355. cell.rtCell.source = null;
  356. }
  357. if (cell.target != null)
  358. {
  359. if (cell.target.rtCell == null)
  360. {
  361. this.createRealtimeCell(cell.target);
  362. }
  363. cell.rtCell.target = cell.target.rtCell;
  364. }
  365. else
  366. {
  367. cell.rtCell.target = null;
  368. }
  369. for (var i = 0; i < this.graphModel.getChildCount(cell); i++)
  370. {
  371. this.saveRealtimeCell(this.graphModel.getChildAt(cell, i));
  372. }
  373. };
  374. /**
  375. * Syncs initial state from collab model to graph model.
  376. */
  377. RealtimeMapping.prototype.createCell = function(rtCell)
  378. {
  379. var cell = rtCell.cell;
  380. if (cell == null)
  381. {
  382. cell = new mxCell();
  383. rtCell.cell = cell;
  384. cell.rtCell = rtCell;
  385. cell.id = rtCell.cellId;
  386. cell.vertex = rtCell.type == 'vertex';
  387. cell.edge = rtCell.type == 'edge';
  388. cell.connectable = rtCell.connectable != '0';
  389. cell.value = (rtCell.xmlValue != null) ? mxUtils.parseXml(rtCell.xmlValue).documentElement : rtCell.value;
  390. cell.style = rtCell.style;
  391. cell.geometry = (rtCell.geometry != null) ? this.driveRealtime.codec.decode(mxUtils.parseXml(rtCell.geometry).documentElement) : null;
  392. cell.visible = rtCell.visible != '0';
  393. cell.collapsed = rtCell.collapsed == '1';
  394. for (var i = 0; i < rtCell.children.length; i++)
  395. {
  396. var rtChild = rtCell.children.get(i);
  397. this.createCell(rtChild);
  398. if (rtChild.cell.parent == null)
  399. {
  400. cell.insert(rtChild.cell);
  401. }
  402. }
  403. this.installRealtimeCellListeners(rtCell);
  404. }
  405. return cell;
  406. };
  407. /**
  408. * Restores connection between edges and terminals.
  409. */
  410. RealtimeMapping.prototype.restoreCell = function(rtCell)
  411. {
  412. var valid = true;
  413. if (rtCell.cell != null)
  414. {
  415. //console.log('restoreCell', rtCell.cellId);
  416. if (rtCell.source != null)
  417. {
  418. // Removes edge if source is no longer in the model
  419. if (rtCell.source.parent == null)
  420. {
  421. //console.log('invalid source', valid, rtCell.cellId, rtCell.source.cellId);
  422. rtCell.source = null;
  423. valid = false;
  424. }
  425. else
  426. {
  427. if (rtCell.source.cell == null)
  428. {
  429. this.createCell(rtCell.source);
  430. }
  431. rtCell.source.cell.insertEdge(rtCell.cell, true);
  432. }
  433. }
  434. if (valid && rtCell.target != null)
  435. {
  436. // Removes edge if source is no longer in the model
  437. if (rtCell.target.parent == null)
  438. {
  439. //console.log('invalid target', valid, rtCell.cellId, rtCell.target.cellId);
  440. rtCell.target = null;
  441. valid = false;
  442. }
  443. else
  444. {
  445. if (rtCell.target.cell == null)
  446. {
  447. this.createCell(rtCell.target);
  448. }
  449. rtCell.target.cell.insertEdge(rtCell.cell, false);
  450. }
  451. }
  452. // Checks if edge contains required terminals or terminal points
  453. if (valid && this.graphModel.isEdge(rtCell.cell))
  454. {
  455. var geo = this.graphModel.getGeometry(rtCell.cell);
  456. valid = geo != null &&
  457. (this.graphModel.getTerminal(rtCell.cell, true) != null || geo.getTerminalPoint(true) != null) &&
  458. (this.graphModel.getTerminal(rtCell.cell, false) != null || geo.getTerminalPoint(false) != null);
  459. //console.log('geometry check', valid, rtCell.cellId);
  460. }
  461. }
  462. // Removes invalid cell
  463. if (!valid)
  464. {
  465. if (rtCell.parent != null)
  466. {
  467. rtCell.parent.children.removeValue(rtCell);
  468. rtCell.parent = null;
  469. }
  470. if (rtCell.cell != null)
  471. {
  472. // TODO: Remove from source and target?
  473. //console.log('remove invalid cell', rtCell.cellId);
  474. this.getGraphModel().remove(rtCell.cell);
  475. }
  476. }
  477. else
  478. {
  479. for (var i = 0; i < rtCell.children.length; i++)
  480. {
  481. this.restoreCell(rtCell.children.get(i));
  482. }
  483. }
  484. };
  485. /**
  486. * Restores connection between edges and terminals.
  487. */
  488. RealtimeMapping.prototype.containsRealtimeCell = function(rtCell)
  489. {
  490. var tmp = rtCell;
  491. while (tmp.parent != null)
  492. {
  493. tmp = tmp.parent;
  494. }
  495. return tmp == this.diagramMap.get(this.driveRealtime.rootKey);
  496. };
  497. /**
  498. *
  499. */
  500. RealtimeMapping.prototype.beginUpdate = function()
  501. {
  502. var graphModel = this.getGraphModel();
  503. if (!this.driveRealtime.ignoreChange)
  504. {
  505. this.driveRealtime.ignoreChange = true;
  506. graphModel.beginUpdate();
  507. window.setTimeout(mxUtils.bind(this, function()
  508. {
  509. graphModel.endUpdate();
  510. this.driveRealtime.ignoreChange = false;
  511. }), 0);
  512. }
  513. return graphModel;
  514. };
  515. /**
  516. * Adds the listener for added and removed cells in the collab model and maps
  517. * them to the graph model.
  518. */
  519. RealtimeMapping.prototype.installRealtimeCellListeners = function(rtCell)
  520. {
  521. rtCell.addEventListener(gapi.drive.realtime.EventType.VALUE_CHANGED, mxUtils.bind(this, function(evt)
  522. {
  523. this.handleValueChanged(rtCell, evt);
  524. this.needsUpdate = true;
  525. }));
  526. rtCell.children.addEventListener(gapi.drive.realtime.EventType.VALUES_ADDED, mxUtils.bind(this, function(evt)
  527. {
  528. this.handleValuesAdded(rtCell, evt);
  529. this.needsUpdate = true;
  530. }));
  531. rtCell.children.addEventListener(gapi.drive.realtime.EventType.VALUES_REMOVED, mxUtils.bind(this, function(evt)
  532. {
  533. this.handleValuesRemoved(rtCell, evt);
  534. this.needsUpdate = true;
  535. }));
  536. };
  537. /**
  538. * Adds the listener for added and removed cells in the collab model and maps
  539. * them to the graph model.
  540. */
  541. RealtimeMapping.prototype.handleValueChanged = function(rtCell, evt)
  542. {
  543. var cell = rtCell.cell;
  544. if (!this.driveRealtime.isLocalEvent(evt) && cell != null)
  545. {
  546. var value = evt.newValue;
  547. var key = evt.property;
  548. var graphModel = this.beginUpdate();
  549. //console.log('valueChanged: cell=' + rtCell.cellId + ' key=' + key + ' value=' + ((value != null) ? (value.cellId || value) : '[null]'));
  550. if (key == 'type')
  551. {
  552. cell.vertex = value == 'vertex';
  553. cell.edge = value == 'edge';
  554. }
  555. else if (key == 'connectable')
  556. {
  557. cell.connectable = (value == '1');
  558. }
  559. else if (key == 'source' || key == 'target')
  560. {
  561. if (value == null)
  562. {
  563. if (evt.oldValue != null)
  564. {
  565. graphModel.setTerminal(cell, null, key == 'source');
  566. }
  567. }
  568. else
  569. {
  570. // Handles the case where an edge is connected to a vertex which is not in the model
  571. if (value.cell == null || !this.containsRealtimeCell(value) || graphModel.getCell(value.cellId) == null)
  572. {
  573. if (rtCell.parent != null)
  574. {
  575. rtCell.parent.children.removeValue(rtCell);
  576. rtCell.parent = null;
  577. }
  578. graphModel.setTerminal(cell, null, key == 'source');
  579. graphModel.remove(rtCell.cell);
  580. rtCell[key] = null;
  581. }
  582. else
  583. {
  584. graphModel.setTerminal(cell, value.cell, key == 'source');
  585. }
  586. }
  587. }
  588. else if (key == 'value')
  589. {
  590. graphModel.setValue(cell, value);
  591. }
  592. else if (key == 'xmlValue')
  593. {
  594. graphModel.setValue(cell, mxUtils.parseXml(value).documentElement);
  595. }
  596. else if (key == 'style')
  597. {
  598. graphModel.setStyle(cell, value);
  599. }
  600. else if (key == 'geometry')
  601. {
  602. var geometry = (value != null) ? this.driveRealtime.codec.decode(mxUtils.parseXml(value).documentElement) : null;
  603. graphModel.setGeometry(cell, geometry);
  604. }
  605. else if (key == 'collapsed')
  606. {
  607. graphModel.setCollapsed(cell, value == '1');
  608. }
  609. else if (key == 'visible')
  610. {
  611. graphModel.setVisible(cell, value == '1');
  612. }
  613. else if (key == 'parent')
  614. {
  615. // Removes the child from its previous parent in the realtime model.
  616. if (evt.oldValue != null)
  617. {
  618. //console.log('remove clone', 'parent', evt.oldValue.cellId, 'child', rtCell.cellId);
  619. evt.oldValue.children.removeValue(rtCell);
  620. }
  621. else
  622. {
  623. this.createCell(rtCell);
  624. this.restoreCell(rtCell);
  625. }
  626. if (value == null)
  627. {
  628. graphModel.remove(cell);
  629. }
  630. else
  631. {
  632. var index = value.children.indexOf(rtCell);
  633. if (index >= 0)
  634. {
  635. //console.log('move child', 'parent', value.cellId, 'child', rtCell.cellId, index);
  636. graphModel.add(value.cell, rtCell.cell, index);
  637. }
  638. }
  639. }
  640. }
  641. };
  642. /**
  643. * Adds the listener for added and removed cells in the collab model and maps
  644. * them to the graph model.
  645. */
  646. RealtimeMapping.prototype.handleValuesAdded = function(rtCell, evt)
  647. {
  648. if (!this.driveRealtime.isLocalEvent(evt))
  649. {
  650. var graphModel = this.beginUpdate();
  651. for (var i = 0; i < evt.values.length; i++)
  652. {
  653. var rtChild = evt.values[i];
  654. //console.log('valueAdded', 'parent', rtCell.cellId, 'child', rtChild.cellId, 'index', evt.index + i, rtChild);
  655. // Removes child if the parent of the child and the parent of the children array are not
  656. // the same. This happens if clients move a cell into different parents concurrently.
  657. if (rtChild.parent != null)
  658. {
  659. if (rtChild.parent != rtCell)
  660. {
  661. //console.log('remove clone', 'parent', rtCell.cellId, 'child', rtChild.cellId);
  662. rtCell.children.removeValue(rtChild);
  663. }
  664. else
  665. {
  666. if (rtChild.cell == null || rtChild.cell.parent == null)
  667. {
  668. this.createCell(rtChild);
  669. this.restoreCell(rtChild);
  670. }
  671. // Resolves conflict when two clients change the order of a child at the same
  672. // time which results in the same child appearing multiple times in the list.
  673. // Note that the actual child index may be different from the event information
  674. // at this point so a generic check for duplicates is performed based on the
  675. // first appearance of the cell in the list.
  676. var first = rtCell.children.indexOf(rtChild);
  677. var last = rtCell.children.lastIndexOf(rtChild);
  678. while (first != last)
  679. {
  680. //console.log('remove duplicate', rtChild.cellId, last);
  681. rtCell.children.remove(last);
  682. last = rtCell.children.lastIndexOf(rtChild);
  683. }
  684. // Inserts the child at the smallest index to maintain consistency with RT
  685. if (rtChild.parent == rtCell)
  686. {
  687. //console.log('add', 'parent', rtCell.cellId, 'child', rtChild.cellId, 'index', Math.min(first, evt.index + i));
  688. graphModel.add(rtCell.cell, rtChild.cell, Math.min(first, evt.index + i));
  689. }
  690. }
  691. }
  692. }
  693. }
  694. };
  695. /**
  696. * Adds the listener for added and removed cells in the collab model and maps
  697. * them to the graph model.
  698. */
  699. RealtimeMapping.prototype.handleValuesRemoved = function(rtCell, evt)
  700. {
  701. if (!this.driveRealtime.isLocalEvent(evt))
  702. {
  703. var graphModel = this.beginUpdate();
  704. for (var i = 0; i < evt.values.length; i++)
  705. {
  706. var rtChild = evt.values[i];
  707. if (rtChild.cell != null)
  708. {
  709. //console.log('valueRemoved', 'parent', rtCell.cellId, 'child', rtChild.cellId,
  710. // 'index', evt.index + i, rtChild, rtChild.cell);
  711. // Checks if the realtime parent and the graph parent are different and updates the parent
  712. // in the graph to match the realtime parent. This happens if the child was removed as a
  713. // clone in another client.
  714. if (rtChild.parent != null && rtChild.parent != rtCell && rtChild.cell.parent != rtChild.parent.cell)
  715. {
  716. //console.log('move clone', rtChild.cellId, evt.index + i, rtChild.parent.cellId);
  717. var index = rtChild.parent.children.indexOf(rtChild);
  718. graphModel.add(rtChild.parent.cell, rtChild.cell, index);
  719. }
  720. else
  721. {
  722. // Checks if the realtime parent contains a duplicate entry of this child
  723. // and updates the index of the child in the graph. This happens if the
  724. // child was removed as a duplicate entry in another client.
  725. var index = rtCell.children.indexOf(rtChild);
  726. if (index >= 0)
  727. {
  728. //console.log('adjust duplicate', rtCell.cellId, evt.index + i, rtChild.cellId, 'index', index);
  729. graphModel.add(rtCell.cell, rtChild.cell, index);
  730. }
  731. }
  732. }
  733. }
  734. }
  735. };
  736. /**
  737. * Syncs initial state from collab model to graph model.
  738. */
  739. RealtimeMapping.prototype.realtimePageFormatChanged = function(value, quiet)
  740. {
  741. if (value != null)
  742. {
  743. var values = value.split(',');
  744. if (values.length > 1)
  745. {
  746. if (quiet)
  747. {
  748. this.graph.pageFormat = new mxRectangle(0, 0, parseInt(values[0]), parseInt(values[1]));
  749. }
  750. else
  751. {
  752. this.driveRealtime.ignorePageFormatChanged = true;
  753. this.ui.setPageFormat(new mxRectangle(0, 0, parseInt(values[0]), parseInt(values[1])));
  754. this.driveRealtime.ignorePageFormatChanged = false;
  755. }
  756. }
  757. }
  758. };
  759. /**
  760. * Syncs initial state from collab model to graph model.
  761. */
  762. RealtimeMapping.prototype.realtimePageScaleChanged = function(value, quiet)
  763. {
  764. if (value != null)
  765. {
  766. if (quiet)
  767. {
  768. this.graph.pageScale = parseFloat(value);
  769. }
  770. else
  771. {
  772. this.driveRealtime.ignorePageScaleChanged = true;
  773. this.ui.setPageScale(parseFloat(value));
  774. this.driveRealtime.ignorePageScaleChanged = false;
  775. }
  776. }
  777. };
  778. /**
  779. * Syncs initial state from collab model to graph model.
  780. */
  781. RealtimeMapping.prototype.realtimeBackgroundColorChanged = function(value, quiet)
  782. {
  783. if (quiet)
  784. {
  785. this.graph.background = (value == '') ? null : value;
  786. }
  787. else
  788. {
  789. this.driveRealtime.ignoreBackgroundColorChanged = true;
  790. this.ui.setBackgroundColor((value == '') ? null : value);
  791. this.driveRealtime.ignoreBackgroundColorChanged = false;
  792. }
  793. };
  794. /**
  795. * Syncs initial state from collab model to graph model.
  796. */
  797. RealtimeMapping.prototype.realtimeFoldingEnabledChanged = function(value, quiet)
  798. {
  799. if (quiet)
  800. {
  801. this.graph.foldingEnabled = value == '1';
  802. }
  803. else
  804. {
  805. this.driveRealtime.ignoreFoldingEnabledChanged = true;
  806. this.ui.setFoldingEnabled(value == '1');
  807. this.driveRealtime.ignoreFoldingEnabledChanged = false;
  808. }
  809. };
  810. /**
  811. * Syncs initial state from collab model to graph model.
  812. */
  813. RealtimeMapping.prototype.realtimeShadowVisibleChanged = function(value, quiet)
  814. {
  815. // Does not need quiet mode as it's handled independently of refresh
  816. this.driveRealtime.ignoreShadowVisibleChanged = true;
  817. this.ui.editor.graph.setShadowVisible(value == '1');
  818. this.driveRealtime.ignoreShadowVisibleChanged = false;
  819. };
  820. /**
  821. * Syncs initial state from collab model to graph model.
  822. */
  823. RealtimeMapping.prototype.realtimeBackgroundImageChanged = function(value, quiet)
  824. {
  825. var data = (value != null && value.length > 0) ? JSON.parse(value) : null;
  826. if (quiet)
  827. {
  828. this.graph.setBackgroundImage((data != null) ? new mxImage(data.src, data.width, data.height) : null);
  829. }
  830. else
  831. {
  832. this.driveRealtime.ignoreBackgroundImageChanged = true;
  833. this.ui.setBackgroundImage((data != null) ? new mxImage(data.src, data.width, data.height) : null);
  834. this.driveRealtime.ignoreBackgroundImageChanged = false;
  835. }
  836. };
  837. /**
  838. * Syncs initial state from collab model to graph model.
  839. */
  840. RealtimeMapping.prototype.realtimeMathEnabledChanged = function(value, quiet)
  841. {
  842. if (quiet)
  843. {
  844. this.graph.mathEnabled = urlParams['math'] == '1' || value == '1';
  845. }
  846. else
  847. {
  848. this.driveRealtime.ignoreMathEnabledChanged = true;
  849. this.ui.setMathEnabled(urlParams['math'] == '1' || value == '1');
  850. this.driveRealtime.ignoreMathEnabledChanged = false;
  851. }
  852. };