RealtimeMapping.js 26 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016
  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. if (rtRoot.cell == null)
  191. {
  192. this.createCell(rtRoot);
  193. this.restoreCell(rtRoot);
  194. }
  195. else
  196. {
  197. this.installAllRealtimeCellListeners(rtRoot);
  198. }
  199. // Stores root in current model and local model
  200. var gm = this.getGraphModel();
  201. gm.setRoot(rtRoot.cell);
  202. if (gm != this.graphModel)
  203. {
  204. this.graphModel.setRoot(gm.getRoot());
  205. }
  206. };
  207. /**
  208. * Writes the graph properties from the realtime model to the given mxGraphModel node.
  209. */
  210. RealtimeMapping.prototype.writeRealtimeToNode = function(node)
  211. {
  212. node.setAttribute('shadow', this.diagramMap.get('shadowVisible'));
  213. node.setAttribute('fold', this.diagramMap.get('foldingEnabled'));
  214. node.setAttribute('math', this.diagramMap.get('mathEnabled'));
  215. node.setAttribute('pageScale', this.diagramMap.get('pageScale'));
  216. var img = this.diagramMap.get('backgroundImage');
  217. if (img != null && img.length > 0)
  218. {
  219. node.setAttribute('backgroundImage', img);
  220. }
  221. var color = this.diagramMap.get('backgroundColor');
  222. if (color != null)
  223. {
  224. node.setAttribute('background', color);
  225. }
  226. var pf = this.diagramMap.get('pageFormat');
  227. if (pf != null)
  228. {
  229. var values = pf.split(',');
  230. if (values.length > 1)
  231. {
  232. node.setAttribute('pageWidth', parseInt(values[0]));
  233. node.setAttribute('pageHeight', parseInt(values[1]));
  234. }
  235. }
  236. };
  237. /**
  238. * Writes the graph properties from the realtime model to the given mxGraphModel node.
  239. */
  240. RealtimeMapping.prototype.writeNodeToRealtime = function(node)
  241. {
  242. this.diagramMap.set('shadowVisible', node.getAttribute('shadow'));
  243. this.diagramMap.set('foldingEnabled', node.getAttribute('fold'));
  244. this.diagramMap.set('mathEnabled', node.getAttribute('math'));
  245. this.diagramMap.set('pageScale', node.getAttribute('pageScale'));
  246. var img = node.getAttribute('backgroundImage');
  247. if (img != null && img.length > 0)
  248. {
  249. this.diagramMap.set('backgroundImage', img);
  250. }
  251. var color = node.getAttribute('background');
  252. if (color != null)
  253. {
  254. this.diagramMap.set('backgroundColor', color);
  255. }
  256. this.diagramMap.set('pageFormat', node.getAttribute('pageWidth') + ',' +
  257. node.getAttribute('pageHeight'));
  258. };
  259. /**
  260. * Syncs initial state from collab model to graph model.
  261. */
  262. RealtimeMapping.prototype.activate = function(quiet)
  263. {
  264. this.realtimePageFormatChanged(this.diagramMap.get('pageFormat'), quiet);
  265. this.realtimePageScaleChanged(this.diagramMap.get('pageScale'), quiet);
  266. this.realtimeMathEnabledChanged(this.diagramMap.get('mathEnabled'), quiet);
  267. this.realtimeBackgroundColorChanged(this.diagramMap.get('backgroundColor'), quiet);
  268. this.realtimeShadowVisibleChanged(this.diagramMap.get('shadowVisible'), quiet);
  269. this.realtimeFoldingEnabledChanged(this.diagramMap.get('foldingEnabled'), quiet);
  270. this.realtimeBackgroundImageChanged(this.diagramMap.get('backgroundImage'), quiet);
  271. };
  272. /**
  273. * Syncs initial state from graph model to collab model.
  274. */
  275. RealtimeMapping.prototype.initRealtime = function()
  276. {
  277. this.rtModel.beginCompoundOperation();
  278. try
  279. {
  280. var rtCell = this.createRealtimeCell(this.getGraphModel().getRoot());
  281. this.saveRealtimeCell(rtCell.cell);
  282. this.diagramMap.set(this.driveRealtime.rootKey, rtCell);
  283. if (this.page.graphModelNode != null)
  284. {
  285. this.writeNodeToRealtime(this.page.graphModelNode);
  286. }
  287. else
  288. {
  289. this.diagramMap.set('shadowVisible', (this.graph.shadowVisible) ? '1' : '0');
  290. this.diagramMap.set('foldingEnabled', (this.graph.foldingEnabled) ? '1' : '0');
  291. this.diagramMap.set('mathEnabled', (this.graph.mathEnabled) ? '1' : '0');
  292. this.diagramMap.set('pageScale', this.graph.pageScale);
  293. this.diagramMap.set('backgroundImage', (this.graph.backgroundImage != null) ? JSON.stringify(this.graph.backgroundImage) : '');
  294. this.diagramMap.set('backgroundColor', (this.graph.background != null) ? this.graph.background : '');
  295. this.diagramMap.set('pageFormat', this.graph.pageFormat.width + ',' + this.graph.pageFormat.height);
  296. }
  297. this.root.set('modifiedDate', new Date().getTime());
  298. this.rtModel.endCompoundOperation();
  299. }
  300. catch (e)
  301. {
  302. this.rtModel.endCompoundOperation();
  303. this.ui.handleError(e);
  304. }
  305. };
  306. /**
  307. * Syncs initial state from graph model to collab model.
  308. */
  309. RealtimeMapping.prototype.createRealtimeCell = function(cell)
  310. {
  311. var rtCell = cell.rtCell;
  312. if (rtCell == null)
  313. {
  314. rtCell = this.rtModel.create('Cell');
  315. rtCell.children = this.rtModel.createList();
  316. rtCell.cell = cell;
  317. cell.rtCell = rtCell;
  318. rtCell.cellId = cell.id;
  319. rtCell.type = (cell.vertex) ? 'vertex' : ((cell.edge) ? 'edge' : '');
  320. rtCell.connectable = (cell.connectable == null || cell.connectable) ? '1' : '0';
  321. if (mxUtils.isNode(cell.value))
  322. {
  323. rtCell.xmlValue = mxUtils.getXml(cell.value);
  324. }
  325. else if (cell.value != null)
  326. {
  327. rtCell.value = cell.value;
  328. }
  329. rtCell.style = (cell.style != null) ? cell.style : null;
  330. rtCell.geometry = (cell.geometry != null) ? mxUtils.getXml(this.driveRealtime.codec.encode(cell.geometry)) : null;
  331. rtCell.visible = (cell.visible == null || cell.visible) ? '1' : '0';
  332. rtCell.collapsed = (cell.collapsed != null && cell.collapsed) ? '1' : '0';
  333. for (var i = 0; i < this.graphModel.getChildCount(cell); i++)
  334. {
  335. var child = this.graphModel.getChildAt(cell, i);
  336. this.createRealtimeCell(child);
  337. if (child.rtCell.parent == null)
  338. {
  339. child.rtCell.parent = rtCell;
  340. rtCell.children.push(child.rtCell);
  341. }
  342. }
  343. this.installRealtimeCellListeners(rtCell);
  344. }
  345. return rtCell;
  346. };
  347. /**
  348. * Syncs initial state from graph model to collab model.
  349. */
  350. RealtimeMapping.prototype.saveRealtimeCell = function(cell)
  351. {
  352. if (cell.source != null)
  353. {
  354. if (cell.source.rtCell == null)
  355. {
  356. this.createRealtimeCell(cell.source);
  357. }
  358. cell.rtCell.source = cell.source.rtCell;
  359. }
  360. else
  361. {
  362. cell.rtCell.source = null;
  363. }
  364. if (cell.target != null)
  365. {
  366. if (cell.target.rtCell == null)
  367. {
  368. this.createRealtimeCell(cell.target);
  369. }
  370. cell.rtCell.target = cell.target.rtCell;
  371. }
  372. else
  373. {
  374. cell.rtCell.target = null;
  375. }
  376. for (var i = 0; i < this.graphModel.getChildCount(cell); i++)
  377. {
  378. this.saveRealtimeCell(this.graphModel.getChildAt(cell, i));
  379. }
  380. };
  381. /**
  382. * Syncs initial state from collab model to graph model.
  383. */
  384. RealtimeMapping.prototype.createCell = function(rtCell)
  385. {
  386. var cell = rtCell.cell;
  387. if (cell == null)
  388. {
  389. cell = new mxCell();
  390. rtCell.cell = cell;
  391. cell.rtCell = rtCell;
  392. cell.id = rtCell.cellId;
  393. cell.vertex = rtCell.type == 'vertex';
  394. cell.edge = rtCell.type == 'edge';
  395. cell.connectable = rtCell.connectable != '0';
  396. cell.value = (rtCell.xmlValue != null) ? mxUtils.parseXml(rtCell.xmlValue).documentElement : rtCell.value;
  397. cell.style = rtCell.style;
  398. cell.geometry = (rtCell.geometry != null) ? this.driveRealtime.codec.decode(mxUtils.parseXml(rtCell.geometry).documentElement) : null;
  399. cell.visible = rtCell.visible != '0';
  400. cell.collapsed = rtCell.collapsed == '1';
  401. for (var i = 0; i < rtCell.children.length; i++)
  402. {
  403. var rtChild = rtCell.children.get(i);
  404. this.createCell(rtChild);
  405. if (rtChild.cell.parent == null)
  406. {
  407. cell.insert(rtChild.cell);
  408. }
  409. }
  410. this.installRealtimeCellListeners(rtCell);
  411. }
  412. return cell;
  413. };
  414. /**
  415. * Restores connection between edges and terminals.
  416. */
  417. RealtimeMapping.prototype.restoreCell = function(rtCell)
  418. {
  419. var valid = true;
  420. if (rtCell.cell != null)
  421. {
  422. //console.log('restoreCell', rtCell.cellId);
  423. if (rtCell.source != null)
  424. {
  425. // Removes edge if source is no longer in the model
  426. if (rtCell.source.parent == null)
  427. {
  428. //console.log('invalid source', valid, rtCell.cellId, rtCell.source.cellId);
  429. rtCell.source = null;
  430. valid = false;
  431. }
  432. else
  433. {
  434. if (rtCell.source.cell == null)
  435. {
  436. this.createCell(rtCell.source);
  437. }
  438. rtCell.source.cell.insertEdge(rtCell.cell, true);
  439. }
  440. }
  441. if (valid && rtCell.target != null)
  442. {
  443. // Removes edge if source is no longer in the model
  444. if (rtCell.target.parent == null)
  445. {
  446. //console.log('invalid target', valid, rtCell.cellId, rtCell.target.cellId);
  447. rtCell.target = null;
  448. valid = false;
  449. }
  450. else
  451. {
  452. if (rtCell.target.cell == null)
  453. {
  454. this.createCell(rtCell.target);
  455. }
  456. rtCell.target.cell.insertEdge(rtCell.cell, false);
  457. }
  458. }
  459. // Checks if edge contains required terminals or terminal points
  460. if (valid && this.graphModel.isEdge(rtCell.cell))
  461. {
  462. var geo = this.graphModel.getGeometry(rtCell.cell);
  463. valid = geo != null &&
  464. (this.graphModel.getTerminal(rtCell.cell, true) != null || geo.getTerminalPoint(true) != null) &&
  465. (this.graphModel.getTerminal(rtCell.cell, false) != null || geo.getTerminalPoint(false) != null);
  466. //console.log('geometry check', valid, rtCell.cellId);
  467. }
  468. }
  469. // Removes invalid cell
  470. if (!valid)
  471. {
  472. if (rtCell.parent != null)
  473. {
  474. rtCell.parent.children.removeValue(rtCell);
  475. rtCell.parent = null;
  476. }
  477. if (rtCell.cell != null)
  478. {
  479. // TODO: Remove from source and target?
  480. //console.log('remove invalid cell', rtCell.cellId);
  481. this.getGraphModel().remove(rtCell.cell);
  482. }
  483. }
  484. else
  485. {
  486. for (var i = 0; i < rtCell.children.length; i++)
  487. {
  488. this.restoreCell(rtCell.children.get(i));
  489. }
  490. }
  491. };
  492. /**
  493. * Restores connection between edges and terminals.
  494. */
  495. RealtimeMapping.prototype.containsRealtimeCell = function(rtCell)
  496. {
  497. var tmp = rtCell;
  498. while (tmp.parent != null)
  499. {
  500. tmp = tmp.parent;
  501. }
  502. return tmp == this.diagramMap.get(this.driveRealtime.rootKey);
  503. };
  504. /**
  505. *
  506. */
  507. RealtimeMapping.prototype.beginUpdate = function()
  508. {
  509. var graphModel = this.getGraphModel();
  510. if (!this.driveRealtime.ignoreChange)
  511. {
  512. this.driveRealtime.ignoreChange = true;
  513. graphModel.beginUpdate();
  514. window.setTimeout(mxUtils.bind(this, function()
  515. {
  516. graphModel.endUpdate();
  517. this.driveRealtime.ignoreChange = false;
  518. }), 0);
  519. }
  520. return graphModel;
  521. };
  522. /**
  523. * Syncs initial state from collab model to graph model.
  524. */
  525. RealtimeMapping.prototype.installAllRealtimeCellListeners = function(rtCell)
  526. {
  527. if (rtCell != null)
  528. {
  529. this.installRealtimeCellListeners(rtCell);
  530. for (var i = 0; i < rtCell.children.length; i++)
  531. {
  532. this.installAllRealtimeCellListeners(rtCell.children.get(i));
  533. }
  534. }
  535. };
  536. /**
  537. * Adds the listener for added and removed cells in the collab model and maps
  538. * them to the graph model.
  539. */
  540. RealtimeMapping.prototype.installRealtimeCellListeners = function(rtCell)
  541. {
  542. rtCell.addEventListener(gapi.drive.realtime.EventType.VALUE_CHANGED, mxUtils.bind(this, function(evt)
  543. {
  544. this.handleValueChanged(rtCell, evt);
  545. this.needsUpdate = true;
  546. }));
  547. rtCell.children.addEventListener(gapi.drive.realtime.EventType.VALUES_ADDED, mxUtils.bind(this, function(evt)
  548. {
  549. this.handleValuesAdded(rtCell, evt);
  550. this.needsUpdate = true;
  551. }));
  552. rtCell.children.addEventListener(gapi.drive.realtime.EventType.VALUES_REMOVED, mxUtils.bind(this, function(evt)
  553. {
  554. this.handleValuesRemoved(rtCell, evt);
  555. this.needsUpdate = true;
  556. }));
  557. };
  558. /**
  559. * Adds the listener for added and removed cells in the collab model and maps
  560. * them to the graph model.
  561. */
  562. RealtimeMapping.prototype.handleValueChanged = function(rtCell, evt)
  563. {
  564. var cell = rtCell.cell;
  565. if (!this.driveRealtime.isLocalEvent(evt) && cell != null)
  566. {
  567. var value = evt.newValue;
  568. var key = evt.property;
  569. var graphModel = this.beginUpdate();
  570. //console.log('valueChanged: cell=' + rtCell.cellId + ' key=' + key + ' value=' + ((value != null) ? (value.cellId || value) : '[null]'));
  571. if (key == 'type')
  572. {
  573. cell.vertex = value == 'vertex';
  574. cell.edge = value == 'edge';
  575. }
  576. else if (key == 'connectable')
  577. {
  578. cell.connectable = (value == '1');
  579. }
  580. else if (key == 'source' || key == 'target')
  581. {
  582. if (value == null)
  583. {
  584. if (evt.oldValue != null)
  585. {
  586. graphModel.setTerminal(cell, null, key == 'source');
  587. }
  588. }
  589. else
  590. {
  591. // Handles the case where an edge is connected to a vertex which is not in the model
  592. if (value.cell == null || !this.containsRealtimeCell(value) || graphModel.getCell(value.cellId) == null)
  593. {
  594. if (rtCell.parent != null)
  595. {
  596. rtCell.parent.children.removeValue(rtCell);
  597. rtCell.parent = null;
  598. }
  599. graphModel.setTerminal(cell, null, key == 'source');
  600. graphModel.remove(rtCell.cell);
  601. rtCell[key] = null;
  602. }
  603. else
  604. {
  605. graphModel.setTerminal(cell, value.cell, key == 'source');
  606. }
  607. }
  608. }
  609. else if (key == 'value')
  610. {
  611. graphModel.setValue(cell, value);
  612. }
  613. else if (key == 'xmlValue')
  614. {
  615. graphModel.setValue(cell, mxUtils.parseXml(value).documentElement);
  616. }
  617. else if (key == 'style')
  618. {
  619. graphModel.setStyle(cell, value);
  620. }
  621. else if (key == 'geometry')
  622. {
  623. var geometry = (value != null) ? this.driveRealtime.codec.decode(mxUtils.parseXml(value).documentElement) : null;
  624. graphModel.setGeometry(cell, geometry);
  625. }
  626. else if (key == 'collapsed')
  627. {
  628. graphModel.setCollapsed(cell, value == '1');
  629. }
  630. else if (key == 'visible')
  631. {
  632. graphModel.setVisible(cell, value == '1');
  633. }
  634. else if (key == 'parent')
  635. {
  636. // Removes the child from its previous parent in the realtime model.
  637. if (evt.oldValue != null)
  638. {
  639. //console.log('remove clone', 'parent', evt.oldValue.cellId, 'child', rtCell.cellId);
  640. evt.oldValue.children.removeValue(rtCell);
  641. }
  642. else
  643. {
  644. this.createCell(rtCell);
  645. this.restoreCell(rtCell);
  646. }
  647. if (value == null)
  648. {
  649. graphModel.remove(cell);
  650. }
  651. else
  652. {
  653. var index = value.children.indexOf(rtCell);
  654. if (index >= 0)
  655. {
  656. //console.log('move child', 'parent', value.cellId, 'child', rtCell.cellId, index);
  657. graphModel.add(value.cell, rtCell.cell, index);
  658. }
  659. }
  660. }
  661. }
  662. };
  663. /**
  664. * Adds the listener for added and removed cells in the collab model and maps
  665. * them to the graph model.
  666. */
  667. RealtimeMapping.prototype.handleValuesAdded = function(rtCell, evt)
  668. {
  669. if (!this.driveRealtime.isLocalEvent(evt))
  670. {
  671. var graphModel = this.beginUpdate();
  672. for (var i = 0; i < evt.values.length; i++)
  673. {
  674. var rtChild = evt.values[i];
  675. //console.log('valueAdded', 'parent', rtCell.cellId, 'child', rtChild.cellId, 'index', evt.index + i, rtChild);
  676. // Removes child if the parent of the child and the parent of the children array are not
  677. // the same. This happens if clients move a cell into different parents concurrently.
  678. if (rtChild.parent != null)
  679. {
  680. if (rtChild.parent != rtCell)
  681. {
  682. //console.log('remove clone', 'parent', rtCell.cellId, 'child', rtChild.cellId);
  683. rtCell.children.removeValue(rtChild);
  684. }
  685. else
  686. {
  687. if (rtChild.cell == null || rtChild.cell.parent == null)
  688. {
  689. this.createCell(rtChild);
  690. this.restoreCell(rtChild);
  691. }
  692. // Resolves conflict when two clients change the order of a child at the same
  693. // time which results in the same child appearing multiple times in the list.
  694. // Note that the actual child index may be different from the event information
  695. // at this point so a generic check for duplicates is performed based on the
  696. // first appearance of the cell in the list.
  697. var first = rtCell.children.indexOf(rtChild);
  698. var last = rtCell.children.lastIndexOf(rtChild);
  699. while (first != last)
  700. {
  701. //console.log('remove duplicate', rtChild.cellId, last);
  702. rtCell.children.remove(last);
  703. last = rtCell.children.lastIndexOf(rtChild);
  704. }
  705. // Inserts the child at the smallest index to maintain consistency with RT
  706. if (rtChild.parent == rtCell)
  707. {
  708. //console.log('add', 'parent', rtCell.cellId, 'child', rtChild.cellId, 'index', Math.min(first, evt.index + i));
  709. graphModel.add(rtCell.cell, rtChild.cell, Math.min(first, evt.index + i));
  710. }
  711. }
  712. }
  713. }
  714. }
  715. };
  716. /**
  717. * Adds the listener for added and removed cells in the collab model and maps
  718. * them to the graph model.
  719. */
  720. RealtimeMapping.prototype.handleValuesRemoved = function(rtCell, evt)
  721. {
  722. if (!this.driveRealtime.isLocalEvent(evt))
  723. {
  724. var graphModel = this.beginUpdate();
  725. for (var i = 0; i < evt.values.length; i++)
  726. {
  727. var rtChild = evt.values[i];
  728. if (rtChild.cell != null)
  729. {
  730. //console.log('valueRemoved', 'parent', rtCell.cellId, 'child', rtChild.cellId,
  731. // 'index', evt.index + i, rtChild, rtChild.cell);
  732. // Checks if the realtime parent and the graph parent are different and updates the parent
  733. // in the graph to match the realtime parent. This happens if the child was removed as a
  734. // clone in another client.
  735. if (rtChild.parent != null && rtChild.parent != rtCell && rtChild.cell.parent != rtChild.parent.cell)
  736. {
  737. //console.log('move clone', rtChild.cellId, evt.index + i, rtChild.parent.cellId);
  738. var index = rtChild.parent.children.indexOf(rtChild);
  739. graphModel.add(rtChild.parent.cell, rtChild.cell, index);
  740. }
  741. else
  742. {
  743. // Checks if the realtime parent contains a duplicate entry of this child
  744. // and updates the index of the child in the graph. This happens if the
  745. // child was removed as a duplicate entry in another client.
  746. var index = rtCell.children.indexOf(rtChild);
  747. if (index >= 0)
  748. {
  749. //console.log('adjust duplicate', rtCell.cellId, evt.index + i, rtChild.cellId, 'index', index);
  750. graphModel.add(rtCell.cell, rtChild.cell, index);
  751. }
  752. }
  753. }
  754. }
  755. }
  756. };
  757. /**
  758. * Syncs initial state from collab model to graph model.
  759. */
  760. RealtimeMapping.prototype.realtimePageFormatChanged = function(value, quiet)
  761. {
  762. if (value != null)
  763. {
  764. var values = value.split(',');
  765. if (values.length > 1)
  766. {
  767. if (quiet)
  768. {
  769. this.graph.pageFormat = new mxRectangle(0, 0, parseInt(values[0]), parseInt(values[1]));
  770. }
  771. else
  772. {
  773. this.driveRealtime.ignorePageFormatChanged = true;
  774. this.ui.setPageFormat(new mxRectangle(0, 0, parseInt(values[0]), parseInt(values[1])));
  775. this.driveRealtime.ignorePageFormatChanged = false;
  776. }
  777. }
  778. }
  779. };
  780. /**
  781. * Syncs initial state from collab model to graph model.
  782. */
  783. RealtimeMapping.prototype.realtimePageScaleChanged = function(value, quiet)
  784. {
  785. if (value != null)
  786. {
  787. if (quiet)
  788. {
  789. this.graph.pageScale = parseFloat(value);
  790. }
  791. else
  792. {
  793. this.driveRealtime.ignorePageScaleChanged = true;
  794. this.ui.setPageScale(parseFloat(value));
  795. this.driveRealtime.ignorePageScaleChanged = false;
  796. }
  797. }
  798. };
  799. /**
  800. * Syncs initial state from collab model to graph model.
  801. */
  802. RealtimeMapping.prototype.realtimeBackgroundColorChanged = function(value, quiet)
  803. {
  804. if (quiet)
  805. {
  806. this.graph.background = (value == '') ? null : value;
  807. }
  808. else
  809. {
  810. this.driveRealtime.ignoreBackgroundColorChanged = true;
  811. this.ui.setBackgroundColor((value == '') ? null : value);
  812. this.driveRealtime.ignoreBackgroundColorChanged = false;
  813. }
  814. };
  815. /**
  816. * Syncs initial state from collab model to graph model.
  817. */
  818. RealtimeMapping.prototype.realtimeFoldingEnabledChanged = function(value, quiet)
  819. {
  820. if (quiet)
  821. {
  822. this.graph.foldingEnabled = value == '1';
  823. }
  824. else
  825. {
  826. this.driveRealtime.ignoreFoldingEnabledChanged = true;
  827. this.ui.setFoldingEnabled(value == '1');
  828. this.driveRealtime.ignoreFoldingEnabledChanged = false;
  829. }
  830. };
  831. /**
  832. * Syncs initial state from collab model to graph model.
  833. */
  834. RealtimeMapping.prototype.realtimeShadowVisibleChanged = function(value, quiet)
  835. {
  836. // Does not need quiet mode as it's handled independently of refresh
  837. this.driveRealtime.ignoreShadowVisibleChanged = true;
  838. this.ui.editor.graph.setShadowVisible(value == '1');
  839. this.driveRealtime.ignoreShadowVisibleChanged = false;
  840. };
  841. /**
  842. * Syncs initial state from collab model to graph model.
  843. */
  844. RealtimeMapping.prototype.realtimeBackgroundImageChanged = function(value, quiet)
  845. {
  846. var data = (value != null && value.length > 0) ? JSON.parse(value) : null;
  847. if (quiet)
  848. {
  849. this.graph.setBackgroundImage((data != null) ? new mxImage(data.src, data.width, data.height) : null);
  850. }
  851. else
  852. {
  853. this.driveRealtime.ignoreBackgroundImageChanged = true;
  854. this.ui.setBackgroundImage((data != null) ? new mxImage(data.src, data.width, data.height) : null);
  855. this.driveRealtime.ignoreBackgroundImageChanged = false;
  856. }
  857. };
  858. /**
  859. * Syncs initial state from collab model to graph model.
  860. */
  861. RealtimeMapping.prototype.realtimeMathEnabledChanged = function(value, quiet)
  862. {
  863. if (quiet)
  864. {
  865. this.graph.mathEnabled = urlParams['math'] == '1' || value == '1';
  866. }
  867. else
  868. {
  869. this.driveRealtime.ignoreMathEnabledChanged = true;
  870. this.ui.setMathEnabled(urlParams['math'] == '1' || value == '1');
  871. this.driveRealtime.ignoreMathEnabledChanged = false;
  872. }
  873. };
  874. /**
  875. * Syncs initial state from collab model to graph model.
  876. */
  877. RealtimeMapping.prototype.removeAllRealtimeCellListeners = function(rtCell)
  878. {
  879. if (rtCell != null)
  880. {
  881. rtCell.removeAllEventListeners();
  882. rtCell.children.removeAllEventListeners();
  883. for (var i = 0; i < rtCell.children.length; i++)
  884. {
  885. this.removeAllRealtimeCellListeners(rtCell.children.get(i));
  886. }
  887. }
  888. };
  889. /**
  890. * Syncs initial state from collab model to graph model.
  891. */
  892. RealtimeMapping.prototype.destroy = function()
  893. {
  894. this.diagramMap.removeAllEventListeners();
  895. this.selectionMap.removeAllEventListeners();
  896. this.removeAllRealtimeCellListeners(this.diagramMap.get(this.driveRealtime.rootKey));
  897. };