gpad/RCanvasPainter.mjs

  1. import { settings, create, parse, toJSON, loadScript, registerMethods, isBatchMode, isFunc, isStr, nsREX } from '../core.mjs';
  2. import { select as d3_select, rgb as d3_rgb } from '../d3.mjs';
  3. import { closeCurrentWindow, showProgress, loadOpenui5, ToolbarIcons, getColorExec } from '../gui/utils.mjs';
  4. import { GridDisplay, getHPainter } from '../gui/display.mjs';
  5. import { makeTranslate } from '../base/BasePainter.mjs';
  6. import { convertColor } from '../base/colors.mjs';
  7. import { selectActivePad, cleanup, resize, EAxisBits } from '../base/ObjectPainter.mjs';
  8. import { RObjectPainter } from '../base/RObjectPainter.mjs';
  9. import { RAxisPainter } from './RAxisPainter.mjs';
  10. import { RFramePainter } from './RFramePainter.mjs';
  11. import { RPadPainter } from './RPadPainter.mjs';
  12. import { addDragHandler } from './TFramePainter.mjs';
  13. /**
  14. * @summary Painter class for RCanvas
  15. *
  16. * @private
  17. */
  18. class RCanvasPainter extends RPadPainter {
  19. #websocket; // WebWindow handle used for communication with server
  20. #changed_layout; // modified layout
  21. #submreq; // submitted requests
  22. #nextreqid; // id of next request
  23. /** @summary constructor */
  24. constructor(dom, canvas, opt) {
  25. super(dom, canvas, opt, true);
  26. this.#websocket = null;
  27. this.#submreq = {};
  28. this.tooltip_allowed = settings.Tooltip;
  29. this.v7canvas = true;
  30. }
  31. /** @summary Cleanup canvas painter */
  32. cleanup() {
  33. this.#websocket = undefined;
  34. this.#submreq = {};
  35. if (this.#changed_layout)
  36. this.setLayoutKind('simple');
  37. this.#changed_layout = undefined;
  38. super.cleanup();
  39. }
  40. /** @summary Returns readonly flag */
  41. isReadonly() { return false; }
  42. /** @summary Returns canvas name */
  43. getCanvasName() {
  44. const title = this.pad?.fTitle;
  45. return (!title || !isStr(title)) ? 'rcanvas' : title.replace(/ /g, '_');
  46. }
  47. /** @summary Returns layout kind */
  48. getLayoutKind() {
  49. const origin = this.selectDom('origin'),
  50. layout = origin.empty() ? '' : origin.property('layout');
  51. return layout || 'simple';
  52. }
  53. /** @summary Set canvas layout kind */
  54. setLayoutKind(kind, main_selector) {
  55. const origin = this.selectDom('origin');
  56. if (!origin.empty()) {
  57. if (!kind) kind = 'simple';
  58. origin.property('layout', kind);
  59. origin.property('layout_selector', (kind !== 'simple') && main_selector ? main_selector : null);
  60. this.#changed_layout = (kind !== 'simple'); // use in cleanup
  61. }
  62. }
  63. /** @summary Changes layout
  64. * @return {Promise} indicating when finished */
  65. async changeLayout(layout_kind, mainid) {
  66. const current = this.getLayoutKind();
  67. if (current === layout_kind)
  68. return true;
  69. const origin = this.selectDom('origin'),
  70. sidebar2 = origin.select('.side_panel2'),
  71. lst = [];
  72. let sidebar = origin.select('.side_panel'),
  73. main = this.selectDom(), force;
  74. while (main.node().firstChild)
  75. lst.push(main.node().removeChild(main.node().firstChild));
  76. if (!sidebar.empty())
  77. cleanup(sidebar.node());
  78. if (!sidebar2.empty())
  79. cleanup(sidebar2.node());
  80. this.setLayoutKind('simple'); // restore defaults
  81. origin.html(''); // cleanup origin
  82. if (layout_kind === 'simple') {
  83. main = origin;
  84. for (let k = 0; k < lst.length; ++k)
  85. main.node().appendChild(lst[k]);
  86. this.setLayoutKind(layout_kind);
  87. force = true;
  88. } else {
  89. const grid = new GridDisplay(origin.node(), layout_kind);
  90. if (mainid === undefined)
  91. mainid = (layout_kind.indexOf('vert') === 0) ? 0 : 1;
  92. main = d3_select(grid.getGridFrame(mainid));
  93. main.classed('central_panel', true).style('position', 'relative');
  94. if (mainid === 2) {
  95. // left panel for Y
  96. sidebar = d3_select(grid.getGridFrame(0));
  97. sidebar.classed('side_panel2', true).style('position', 'relative');
  98. // bottom panel for X
  99. sidebar = d3_select(grid.getGridFrame(3));
  100. sidebar.classed('side_panel', true).style('position', 'relative');
  101. } else {
  102. sidebar = d3_select(grid.getGridFrame(1 - mainid));
  103. sidebar.classed('side_panel', true).style('position', 'relative');
  104. }
  105. // now append all childs to the new main
  106. for (let k = 0; k < lst.length; ++k)
  107. main.node().appendChild(lst[k]);
  108. this.setLayoutKind(layout_kind, '.central_panel');
  109. // remove reference to MDIDisplay, solves resize problem
  110. origin.property('mdi', null);
  111. }
  112. // resize main drawing and let draw extras
  113. resize(main.node(), force);
  114. return true;
  115. }
  116. /** @summary Toggle projection
  117. * @return {Promise} indicating when ready
  118. * @private */
  119. async toggleProjection(kind) {
  120. delete this.proj_painter;
  121. if (kind) this.proj_painter = { X: false, Y: false }; // just indicator that drawing can be preformed
  122. if (isFunc(this.showUI5ProjectionArea))
  123. return this.showUI5ProjectionArea(kind);
  124. let layout = 'simple', mainid;
  125. switch (kind) {
  126. case 'XY': layout = 'projxy'; mainid = 2; break;
  127. case 'X':
  128. case 'bottom': layout = 'vert2_31'; mainid = 0; break;
  129. case 'Y':
  130. case 'left': layout = 'horiz2_13'; mainid = 1; break;
  131. case 'top': layout = 'vert2_13'; mainid = 1; break;
  132. case 'right': layout = 'horiz2_31'; mainid = 0; break;
  133. }
  134. return this.changeLayout(layout, mainid);
  135. }
  136. /** @summary Draw projection for specified histogram
  137. * @private */
  138. async drawProjection(/* kind, hist, hopt */) {
  139. // dummy for the moment
  140. return false;
  141. }
  142. /** @summary Draw in side panel
  143. * @private */
  144. async drawInSidePanel(canv, opt, kind) {
  145. const sel = ((this.getLayoutKind() === 'projxy') && (kind === 'Y')) ? '.side_panel2' : '.side_panel',
  146. side = this.selectDom('origin').select(sel);
  147. return side.empty() ? null : this.drawObject(side.node(), canv, opt);
  148. }
  149. /** @summary Checks if canvas shown inside ui5 widget
  150. * @desc Function should be used only from the func which supposed to be replaced by ui5
  151. * @private */
  152. testUI5() {
  153. return this.use_openui ?? false;
  154. }
  155. /** @summary Show message
  156. * @desc Used normally with web-based canvas and handled in ui5
  157. * @private */
  158. showMessage(msg) {
  159. if (!this.testUI5())
  160. showProgress(msg, 7000);
  161. }
  162. /** @summary Function called when canvas menu item Save is called */
  163. saveCanvasAsFile(fname) {
  164. const pnt = fname.indexOf('.');
  165. this.createImage(fname.slice(pnt+1))
  166. .then(res => this.sendWebsocket(`SAVE:${fname}:${res}`));
  167. }
  168. /** @summary Send command to server to save canvas with specified name
  169. * @desc Should be only used in web-based canvas
  170. * @private */
  171. sendSaveCommand(fname) { this.sendWebsocket('PRODUCE:' + fname); }
  172. /** @summary Return assigned web socket
  173. * @private */
  174. getWebsocket() { return this.#websocket; }
  175. /** @summary Return true if message can be send via web socket
  176. * @private */
  177. canSendWebsocket(noper = 1) { return this.#websocket?.canSend(noper); }
  178. /** @summary Send message via web socket
  179. * @private */
  180. sendWebsocket(msg) {
  181. if (this.#websocket?.canSend()) {
  182. this.#websocket.send(msg);
  183. return true;
  184. }
  185. return false;
  186. }
  187. /** @summary Close websocket connection to canvas
  188. * @private */
  189. closeWebsocket(force) {
  190. if (this.#websocket) {
  191. this.#websocket.close(force);
  192. this.#websocket.cleanup();
  193. this.#websocket = undefined;
  194. }
  195. }
  196. /** @summary Use provided connection for the web canvas
  197. * @private */
  198. useWebsocket(handle) {
  199. this.closeWebsocket();
  200. this.#websocket = handle;
  201. this.#websocket.setReceiver(this);
  202. this.#websocket.connect();
  203. }
  204. /** @summary set, test or reset timeout of specified name
  205. * @desc Used to prevent overloading of websocket for specific function */
  206. websocketTimeout(name, tm) {
  207. if (!this.#websocket)
  208. return;
  209. if (!this.#websocket._tmouts)
  210. this.#websocket._tmouts = {};
  211. const handle = this.#websocket._tmouts[name];
  212. if (tm === undefined)
  213. return handle !== undefined;
  214. if (tm === 'reset') {
  215. if (handle) { clearTimeout(handle); delete this.#websocket._tmouts[name]; }
  216. } else if (!handle && Number.isInteger(tm))
  217. this.#websocket._tmouts[name] = setTimeout(() => { delete this.#websocket._tmouts[name]; }, tm);
  218. }
  219. /** @summary Handler for websocket open event
  220. * @private */
  221. onWebsocketOpened(/* handle */) {
  222. }
  223. /** @summary Handler for websocket close event
  224. * @private */
  225. onWebsocketClosed(/* handle */) {
  226. if (!this.embed_canvas)
  227. closeCurrentWindow();
  228. }
  229. /** @summary Handler for websocket message
  230. * @private */
  231. onWebsocketMsg(handle, msg) {
  232. // console.log('GET_MSG ' + msg.slice(0,30));
  233. if (msg === 'CLOSE') {
  234. this.onWebsocketClosed();
  235. this.closeWebsocket(true);
  236. } else if (msg.slice(0, 5) === 'SNAP:') {
  237. msg = msg.slice(5);
  238. const p1 = msg.indexOf(':'),
  239. snapid = msg.slice(0, p1),
  240. snap = parse(msg.slice(p1+1));
  241. this.syncDraw(true)
  242. .then(() => {
  243. if (!this.snapid && snap?.fWinSize)
  244. this.resizeBrowser(snap.fWinSize[0], snap.fWinSize[1]);
  245. }).then(() => this.redrawPadSnap(snap))
  246. .then(() => {
  247. this.addPadInteractive();
  248. handle.send(`SNAPDONE:${snapid}`); // send ready message back when drawing completed
  249. this.confirmDraw();
  250. }).catch(err => {
  251. if (isFunc(this.showConsoleError))
  252. this.showConsoleError(err);
  253. else
  254. console.log(err);
  255. });
  256. } else if (msg.slice(0, 4) === 'JSON') {
  257. const obj = parse(msg.slice(4));
  258. this.redrawObject(obj);
  259. } else if (msg.slice(0, 9) === 'REPL_REQ:')
  260. this.processDrawableReply(msg.slice(9));
  261. else if (msg.slice(0, 4) === 'CMD:') {
  262. msg = msg.slice(4);
  263. const p1 = msg.indexOf(':'),
  264. cmdid = msg.slice(0, p1),
  265. cmd = msg.slice(p1+1),
  266. reply = `REPLY:${cmdid}:`;
  267. if ((cmd === 'SVG') || (cmd === 'PNG') || (cmd === 'JPEG') || (cmd === 'WEBP') || (cmd === 'PDF')) {
  268. this.createImage(cmd.toLowerCase())
  269. .then(res => handle.send(reply + res));
  270. } else if (cmd.indexOf('ADDPANEL:') === 0) {
  271. if (!isFunc(this.showUI5Panel))
  272. handle.send(reply + 'false');
  273. else {
  274. const window_path = cmd.slice(9),
  275. conn = handle.createNewInstance(window_path);
  276. // set interim receiver until first message arrives
  277. conn.setReceiver({
  278. cpainter: this,
  279. onWebsocketOpened() {
  280. },
  281. onWebsocketMsg(panel_handle, msg2) {
  282. const panel_name = (msg2.indexOf('SHOWPANEL:') === 0) ? msg2.slice(10) : '';
  283. this.cpainter.showUI5Panel(panel_name, panel_handle)
  284. .then(res => handle.send(reply + (res ? 'true' : 'false')));
  285. },
  286. onWebsocketClosed() {
  287. // if connection failed,
  288. handle.send(reply + 'false');
  289. },
  290. onWebsocketError() {
  291. // if connection failed,
  292. handle.send(reply + 'false');
  293. }
  294. });
  295. // only when connection established, panel will be activated
  296. conn.connect();
  297. }
  298. } else {
  299. console.log('Unrecognized command ' + cmd);
  300. handle.send(reply);
  301. }
  302. } else if ((msg.slice(0, 7) === 'DXPROJ:') || (msg.slice(0, 7) === 'DYPROJ:')) {
  303. const kind = msg[1],
  304. hist = parse(msg.slice(7));
  305. this.drawProjection(kind, hist);
  306. } else if (msg.slice(0, 5) === 'SHOW:') {
  307. const that = msg.slice(5),
  308. on = that.at(-1) === '1';
  309. this.showSection(that.slice(0, that.length - 2), on);
  310. } else
  311. console.log(`unrecognized msg len: ${msg.length} msg: ${msg.slice(0, 30)}`);
  312. }
  313. /** @summary Submit request to RDrawable object on server side */
  314. submitDrawableRequest(kind, req, painter, method) {
  315. if (!this.getWebsocket() || !req?._typename || !painter.snapid || !isStr(painter.snapid))
  316. return null;
  317. if (kind && method) {
  318. // if kind specified - check if such request already was submitted
  319. if (!painter._requests) painter._requests = {};
  320. const prevreq = painter._requests[kind];
  321. if (prevreq) {
  322. const tm = new Date().getTime();
  323. if (!prevreq._tm || (tm - prevreq._tm < 5000)) {
  324. prevreq._nextreq = req; // submit when got reply
  325. return false;
  326. }
  327. delete painter._requests[kind]; // let submit new request after timeout
  328. }
  329. painter._requests[kind] = req; // keep reference on the request
  330. }
  331. req.id = painter.snapid;
  332. if (method) {
  333. if (!this.#nextreqid) this.#nextreqid = 1;
  334. req.reqid = this.#nextreqid++;
  335. } else
  336. req.reqid = 0; // request will not be replied
  337. const msg = JSON.stringify(req);
  338. if (req.reqid) {
  339. req._kind = kind;
  340. req._painter = painter;
  341. req._method = method;
  342. req._tm = new Date().getTime();
  343. this.#submreq[req.reqid] = req; // fast access to submitted requests
  344. }
  345. this.sendWebsocket('REQ:' + msg);
  346. return req;
  347. }
  348. /** @summary Submit menu request
  349. * @private */
  350. async submitMenuRequest(painter, menukind, reqid) {
  351. return new Promise(resolveFunc => {
  352. this.submitDrawableRequest('', {
  353. _typename: `${nsREX}RDrawableMenuRequest`,
  354. menukind: menukind || '',
  355. menureqid: reqid // used to identify menu request
  356. }, painter, resolveFunc);
  357. });
  358. }
  359. /** @summary Submit executable command for given painter */
  360. submitExec(painter, exec, subelem) {
  361. if (subelem && isStr(subelem)) {
  362. const len = subelem.length;
  363. if ((len > 2) && (subelem.indexOf('#x') === len - 2)) subelem = 'x'; else
  364. if ((len > 2) && (subelem.indexOf('#y') === len - 2)) subelem = 'y'; else
  365. if ((len > 2) && (subelem.indexOf('#z') === len - 2)) subelem = 'z';
  366. if ((subelem === 'x') || (subelem === 'y') || (subelem === 'z'))
  367. exec = subelem + 'axis#' + exec;
  368. else
  369. return console.log(`not recoginzed subelem ${subelem} in submitExec`);
  370. }
  371. this.submitDrawableRequest('', { _typename: `${nsREX}RDrawableExecRequest`, exec }, painter);
  372. }
  373. /** @summary Process reply from request to RDrawable */
  374. processDrawableReply(msg) {
  375. const reply = parse(msg);
  376. if (!reply?.reqid) return false;
  377. const req = this.#submreq[reply.reqid];
  378. if (!req) return false;
  379. // remove reference first
  380. this.#submreq[reply.reqid] = undefined;
  381. // remove blocking reference for that kind
  382. if (req._kind && req._painter?._requests) {
  383. if (req._painter._requests[req._kind] === req)
  384. delete req._painter._requests[req._kind];
  385. }
  386. if (req._method)
  387. req._method(reply, req);
  388. // resubmit last request of that kind
  389. if (req._nextreq && !req._painter._requests[req._kind])
  390. this.submitDrawableRequest(req._kind, req._nextreq, req._painter, req._method);
  391. }
  392. /** @summary Show specified section in canvas */
  393. async showSection(that, on) {
  394. switch (that) {
  395. case 'Menu': break;
  396. case 'StatusBar': break;
  397. case 'Editor': break;
  398. case 'ToolBar': break;
  399. case 'ToolTips': this.setTooltipAllowed(on); break;
  400. }
  401. return true;
  402. }
  403. /** @summary Method informs that something was changed in the canvas
  404. * @desc used to update information on the server (when used with web6gui)
  405. * @private */
  406. processChanges(kind, painter, subelem) {
  407. // check if we could send at least one message more - for some meaningful actions
  408. if (!this.canSendWebsocket(2) || !isStr(kind))
  409. return;
  410. const msg = '';
  411. if (!painter) painter = this;
  412. switch (kind) {
  413. case 'sbits':
  414. console.log('Status bits in RCanvas are changed - that to do?');
  415. break;
  416. case 'frame': // when moving frame
  417. case 'zoom': // when changing zoom inside frame
  418. console.log('Frame moved or zoom is changed - that to do?');
  419. break;
  420. case 'pave_moved':
  421. console.log('TPave is moved inside RCanvas - that to do?');
  422. break;
  423. default:
  424. if ((kind.slice(0, 5) === 'exec:') && painter?.snapid)
  425. this.submitExec(painter, kind.slice(5), subelem);
  426. else
  427. console.log('UNPROCESSED CHANGES', kind);
  428. }
  429. if (msg)
  430. console.log('RCanvas::processChanges want to send ' + msg.length + ' ' + msg.slice(0, 40));
  431. }
  432. /** @summary Handle pad button click event
  433. * @private */
  434. clickPadButton(funcname, evnt) {
  435. if (funcname === 'ToggleGed')
  436. return this.activateGed(this, null, 'toggle');
  437. if (funcname === 'ToggleStatus')
  438. return this.activateStatusBar('toggle');
  439. return super.clickPadButton(funcname, evnt);
  440. }
  441. /** @summary returns true when event status area exist for the canvas */
  442. hasEventStatus() {
  443. if (this.testUI5()) return false;
  444. if (this.brlayout)
  445. return this.brlayout.hasStatus();
  446. const hp = getHPainter();
  447. return hp ? hp.hasStatusLine() : false;
  448. }
  449. /** @summary Check if status bar can be toggled
  450. * @private */
  451. canStatusBar() {
  452. return this.testUI5() || this.brlayout || getHPainter();
  453. }
  454. /** @summary Show/toggle event status bar
  455. * @private */
  456. activateStatusBar(state) {
  457. if (this.testUI5())
  458. return;
  459. if (this.brlayout)
  460. this.brlayout.createStatusLine(23, state);
  461. else
  462. getHPainter()?.createStatusLine(23, state);
  463. this.processChanges('sbits', this);
  464. }
  465. /** @summary Show online canvas status
  466. * @private */
  467. showCanvasStatus(...msgs) {
  468. if (this.testUI5()) return;
  469. const br = this.brlayout || getHPainter()?.brlayout;
  470. br?.showStatus(...msgs);
  471. }
  472. /** @summary Returns true if GED is present on the canvas */
  473. hasGed() {
  474. if (this.testUI5()) return false;
  475. return this.brlayout?.hasContent() ?? false;
  476. }
  477. /** @summary Function used to de-activate GED
  478. * @private */
  479. removeGed() {
  480. if (this.testUI5()) return;
  481. this.registerForPadEvents(null);
  482. if (this.ged_view) {
  483. this.ged_view.getController().cleanupGed();
  484. this.ged_view.destroy();
  485. delete this.ged_view;
  486. }
  487. this.brlayout?.deleteContent(true);
  488. this.processChanges('sbits', this);
  489. }
  490. /** @summary Get view data for ui5 panel
  491. * @private */
  492. getUi5PanelData(/* panel_name */) {
  493. return { jsroot: { settings, create, parse, toJSON, loadScript, EAxisBits, getColorExec } };
  494. }
  495. /** @summary Function used to activate GED
  496. * @return {Promise} when GED is there
  497. * @private */
  498. async activateGed(objpainter, kind, mode) {
  499. if (this.testUI5() || !this.brlayout)
  500. return false;
  501. if (this.brlayout.hasContent()) {
  502. if ((mode === 'toggle') || (mode === false))
  503. this.removeGed();
  504. else
  505. objpainter?.getPadPainter()?.selectObjectPainter(objpainter);
  506. return true;
  507. }
  508. if (mode === false)
  509. return false;
  510. const btns = this.brlayout.createBrowserBtns();
  511. ToolbarIcons.createSVG(btns, ToolbarIcons.diamand, 15, 'toggle fix-pos mode', 'browser')
  512. .style('margin', '3px').on('click', () => this.brlayout.toggleKind('fix'));
  513. ToolbarIcons.createSVG(btns, ToolbarIcons.circle, 15, 'toggle float mode', 'browser')
  514. .style('margin', '3px').on('click', () => this.brlayout.toggleKind('float'));
  515. ToolbarIcons.createSVG(btns, ToolbarIcons.cross, 15, 'delete GED', 'browser')
  516. .style('margin', '3px').on('click', () => this.removeGed());
  517. // be aware, that jsroot_browser_hierarchy required for flexible layout that element use full browser area
  518. this.brlayout.setBrowserContent('<div class=\'jsroot_browser_hierarchy\' id=\'ged_placeholder\'>Loading GED ...</div>');
  519. this.brlayout.setBrowserTitle('GED');
  520. this.brlayout.toggleBrowserKind(kind || 'float');
  521. return new Promise(resolveFunc => {
  522. loadOpenui5.then(sap => {
  523. d3_select('#ged_placeholder').text('');
  524. sap.ui.require(['sap/ui/model/json/JSONModel', 'sap/ui/core/mvc/XMLView'], (JSONModel, XMLView) => {
  525. const oModel = new JSONModel({ handle: null });
  526. XMLView.create({
  527. viewName: 'rootui5.canv.view.Ged',
  528. viewData: this.getUi5PanelData('Ged')
  529. }).then(oGed => {
  530. oGed.setModel(oModel);
  531. oGed.placeAt('ged_placeholder');
  532. this.ged_view = oGed;
  533. // TODO: should be moved into Ged controller - it must be able to detect canvas painter itself
  534. this.registerForPadEvents(oGed.getController().padEventsReceiver.bind(oGed.getController()));
  535. objpainter?.getPadPainter()?.selectObjectPainter(objpainter);
  536. this.processChanges('sbits', this);
  537. resolveFunc(true);
  538. });
  539. });
  540. });
  541. });
  542. }
  543. /** @summary produce JSON for RCanvas, which can be used to display canvas once again
  544. * @private */
  545. produceJSON() {
  546. console.error('RCanvasPainter.produceJSON not yet implemented');
  547. return '';
  548. }
  549. /** @summary resize browser window to get requested canvas sizes */
  550. resizeBrowser(fullW, fullH) {
  551. if (!fullW || !fullH || this.isBatchMode() || this.embed_canvas || this.batch_mode)
  552. return;
  553. this.getWebsocket()?.resizeWindow(fullW, fullH);
  554. }
  555. /** @summary draw RCanvas object */
  556. static async draw(dom, can, opt) {
  557. const nocanvas = !can;
  558. if (nocanvas)
  559. can = create(`${nsREX}RCanvas`);
  560. const painter = new RCanvasPainter(dom, can, opt);
  561. painter.createCanvasSvg(0);
  562. selectActivePad({ pp: painter, active: false });
  563. return painter.drawPrimitives().then(() => {
  564. painter.addPadInteractive();
  565. painter.addPadButtons();
  566. painter.showPadButtons();
  567. return painter;
  568. });
  569. }
  570. } // class RCanvasPainter
  571. /** @summary draw RPadSnapshot object
  572. * @private */
  573. function drawRPadSnapshot(dom, snap, opt) {
  574. const painter = new RCanvasPainter(dom, null, opt);
  575. painter.batch_mode = isBatchMode();
  576. return painter.syncDraw(true).then(() => painter.redrawPadSnap(snap)).then(() => {
  577. painter.confirmDraw();
  578. painter.showPadButtons();
  579. return painter;
  580. });
  581. }
  582. /** @summary Ensure RCanvas and RFrame for the painter object
  583. * @param {Object} painter - painter object to process
  584. * @param {string|boolean} frame_kind - false for no frame or '3d' for special 3D mode
  585. * @desc Assigns DOM, creates and draw RCanvas and RFrame if necessary, add painter to pad list of painters
  586. * @return {Promise} for ready
  587. * @private */
  588. async function ensureRCanvas(painter, frame_kind) {
  589. if (!painter)
  590. return Promise.reject(Error('Painter not provided in ensureRCanvas'));
  591. // simple check - if canvas there, can use painter
  592. const pr = painter.getCanvSvg().empty() ? RCanvasPainter.draw(painter.getDom(), null /* noframe */) : Promise.resolve(true);
  593. return pr.then(() => {
  594. if ((frame_kind !== false) && painter.getFrameSvg().selectChild('.main_layer').empty())
  595. return RFramePainter.draw(painter.getDom(), null, isStr(frame_kind) ? frame_kind : '');
  596. }).then(() => {
  597. painter.addToPadPrimitives();
  598. return painter;
  599. });
  600. }
  601. /** @summary Function used for direct draw of RFrameTitle
  602. * @private */
  603. function drawRFrameTitle(reason, drag) {
  604. const fp = this.getFramePainter();
  605. if (!fp)
  606. return console.log('no frame painter - no title');
  607. const rect = fp.getFrameRect(),
  608. fx = rect.x,
  609. fy = rect.y,
  610. fw = rect.width,
  611. // fh = rect.height,
  612. ph = this.getPadPainter().getPadHeight(),
  613. title = this.getObject(),
  614. title_width = fw,
  615. textFont = this.v7EvalFont('text', { size: 0.07, color: 'black', align: 22 });
  616. let title_margin = this.v7EvalLength('margin', ph, 0.02),
  617. title_height = this.v7EvalLength('height', ph, 0.05);
  618. if (reason === 'drag') {
  619. title_height = drag.height;
  620. title_margin = fy - drag.y - drag.height;
  621. const changes = {};
  622. this.v7AttrChange(changes, 'margin', title_margin / ph);
  623. this.v7AttrChange(changes, 'height', title_height / ph);
  624. this.v7SendAttrChanges(changes, false); // do not invoke canvas update on the server
  625. }
  626. this.createG();
  627. makeTranslate(this.draw_g, fx, Math.round(fy-title_margin-title_height));
  628. return this.startTextDrawingAsync(textFont, 'font').then(() => {
  629. this.drawText({ x: title_width/2, y: title_height/2, text: title.fText, latex: 1 });
  630. return this.finishTextDrawing();
  631. }).then(() => {
  632. addDragHandler(this, { x: fx, y: Math.round(fy-title_margin-title_height), width: title_width, height: title_height,
  633. minwidth: 20, minheight: 20, no_change_x: true, redraw: d => this.redraw('drag', d) });
  634. });
  635. }
  636. // ==========================================================
  637. registerMethods(`${nsREX}RPalette`, {
  638. extractRColor(rcolor) {
  639. const col = rcolor.fColor || 'black';
  640. return convertColor(col);
  641. },
  642. getColor(indx) {
  643. return this.palette[indx];
  644. },
  645. getContourIndex(zc) {
  646. const cntr = this.fContour;
  647. let l = 0, r = cntr.length - 1;
  648. if (zc < cntr[0])
  649. return -1;
  650. if (zc >= cntr[r])
  651. return r - 1;
  652. if (this.fCustomContour) {
  653. while (l < r - 1) {
  654. const mid = Math.round((l+r)/2);
  655. if (cntr[mid] > zc)
  656. r = mid;
  657. else
  658. l = mid;
  659. }
  660. return l;
  661. }
  662. // last color in palette starts from level cntr[r-1]
  663. return Math.floor((zc-cntr[0]) / (cntr[r-1] - cntr[0]) * (r-1));
  664. },
  665. getContourColor(zc) {
  666. const zindx = this.getContourIndex(zc);
  667. return (zindx < 0) ? '' : this.getColor(zindx);
  668. },
  669. getContour() {
  670. return this.fContour && (this.fContour.length > 1) ? this.fContour : null;
  671. },
  672. deleteContour() {
  673. delete this.fContour;
  674. },
  675. calcColor(value, entry1, entry2) {
  676. const dist = entry2.fOrdinal - entry1.fOrdinal,
  677. r1 = entry2.fOrdinal - value,
  678. r2 = value - entry1.fOrdinal;
  679. if (!this.fInterpolate || (dist <= 0))
  680. return convertColor((r1 < r2) ? entry2.fColor : entry1.fColor);
  681. // interpolate
  682. const col1 = d3_rgb(this.extractRColor(entry1.fColor)),
  683. col2 = d3_rgb(this.extractRColor(entry2.fColor)),
  684. color = d3_rgb(Math.round((col1.r*r1 + col2.r*r2)/dist),
  685. Math.round((col1.g*r1 + col2.g*r2)/dist),
  686. Math.round((col1.b*r1 + col2.b*r2)/dist));
  687. return color.formatRgb();
  688. },
  689. createPaletteColors(len) {
  690. const arr = [];
  691. let indx = 0;
  692. while (arr.length < len) {
  693. const value = arr.length / (len-1),
  694. entry = this.fColors[indx];
  695. if ((Math.abs(entry.fOrdinal - value) < 0.0001) || (indx === this.fColors.length - 1)) {
  696. arr.push(this.extractRColor(entry.fColor));
  697. continue;
  698. }
  699. const next = this.fColors[indx+1];
  700. if (next.fOrdinal <= value)
  701. indx++;
  702. else
  703. arr.push(this.calcColor(value, entry, next));
  704. }
  705. return arr;
  706. },
  707. getColorOrdinal(value) {
  708. // extract color with ordinal value between 0 and 1
  709. if (!this.fColors)
  710. return 'black';
  711. if ((typeof value !== 'number') || (value < 0))
  712. value = 0;
  713. else if (value > 1)
  714. value = 1;
  715. // TODO: implement better way to find index
  716. let entry, next = this.fColors[0];
  717. for (let indx = 0; indx < this.fColors.length - 1; ++indx) {
  718. entry = next;
  719. if (Math.abs(entry.fOrdinal - value) < 0.0001)
  720. return this.extractRColor(entry.fColor);
  721. next = this.fColors[indx+1];
  722. if (next.fOrdinal > value)
  723. return this.calcColor(value, entry, next);
  724. }
  725. return this.extractRColor(next.fColor);
  726. },
  727. setFullRange(min, max) {
  728. // set full z scale range, used in zooming
  729. this.full_min = min;
  730. this.full_max = max;
  731. },
  732. createContour(logz, nlevels, zmin, zmax, zminpositive) {
  733. this.fContour = [];
  734. delete this.fCustomContour;
  735. this.colzmin = zmin;
  736. this.colzmax = zmax;
  737. if (logz) {
  738. if (this.colzmax <= 0) this.colzmax = 1.0;
  739. if (this.colzmin <= 0) {
  740. if ((zminpositive === undefined) || (zminpositive <= 0))
  741. this.colzmin = 0.0001*this.colzmax;
  742. else
  743. this.colzmin = ((zminpositive < 3) || (zminpositive>100)) ? 0.3*zminpositive : 1;
  744. }
  745. if (this.colzmin >= this.colzmax)
  746. this.colzmin = 0.0001*this.colzmax;
  747. const logmin = Math.log(this.colzmin)/Math.log(10),
  748. logmax = Math.log(this.colzmax)/Math.log(10),
  749. dz = (logmax-logmin)/nlevels;
  750. this.fContour.push(this.colzmin);
  751. for (let level=1; level<nlevels; level++)
  752. this.fContour.push(Math.exp((logmin + dz*level)*Math.log(10)));
  753. this.fContour.push(this.colzmax);
  754. this.fCustomContour = true;
  755. } else {
  756. if ((this.colzmin === this.colzmax) && this.colzmin) {
  757. this.colzmax += 0.01*Math.abs(this.colzmax);
  758. this.colzmin -= 0.01*Math.abs(this.colzmin);
  759. }
  760. const dz = (this.colzmax-this.colzmin)/nlevels;
  761. for (let level=0; level<=nlevels; level++)
  762. this.fContour.push(this.colzmin + dz*level);
  763. }
  764. if (!this.palette || (this.palette.length !== nlevels))
  765. this.palette = this.createPaletteColors(nlevels);
  766. }
  767. });
  768. /** @summary draw RFont object
  769. * @private */
  770. function drawRFont() {
  771. const font = this.getObject(),
  772. svg = this.getCanvSvg(),
  773. clname = 'custom_font_' + font.fFamily+font.fWeight+font.fStyle;
  774. let defs = svg.selectChild('.canvas_defs');
  775. if (defs.empty())
  776. defs = svg.insert('svg:defs', ':first-child').attr('class', 'canvas_defs');
  777. let entry = defs.selectChild('.' + clname);
  778. if (entry.empty()) {
  779. entry = defs.append('style')
  780. .attr('type', 'text/css')
  781. .attr('class', clname)
  782. .text(`@font-face { font-family: "${font.fFamily}"; font-weight: ${font.fWeight ? font.fWeight : 'normal'}; font-style: ${font.fStyle ? font.fStyle : 'normal'}; src: ${font.fSrc}; }`);
  783. const p1 = font.fSrc.indexOf('base64,'),
  784. p2 = font.fSrc.lastIndexOf(' format(');
  785. if (p1 > 0 && p2 > p1) {
  786. const base64 = font.fSrc.slice(p1 + 7, p2 - 2),
  787. is_ttf = font.fSrc.indexOf('data:application/font-ttf') > 0;
  788. // TODO: for the moment only ttf format supported by jsPDF
  789. if (is_ttf)
  790. entry.property('$fontcfg', { n: font.fFamily, base64 });
  791. }
  792. }
  793. if (font.fDefault)
  794. this.getPadPainter()._dfltRFont = font;
  795. return true;
  796. }
  797. /** @summary draw RAxis object
  798. * @private */
  799. function drawRAxis(dom, obj, opt) {
  800. const painter = new RAxisPainter(dom, obj, opt);
  801. painter.disable_zooming = true;
  802. return ensureRCanvas(painter, false)
  803. .then(() => painter.redraw())
  804. .then(() => painter);
  805. }
  806. /** @summary draw RFrame object
  807. * @private */
  808. function drawRFrame(dom, obj, opt) {
  809. const p = new RFramePainter(dom, obj);
  810. if (opt === '3d') p.mode3d = true;
  811. return ensureRCanvas(p, false).then(() => p.redraw());
  812. }
  813. export { ensureRCanvas, drawRPadSnapshot,
  814. drawRFrameTitle, drawRFont, drawRAxis, drawRFrame,
  815. RObjectPainter, RPadPainter, RCanvasPainter };