UNPKG

jsroot

Version:
985 lines (811 loc) 33 kB
import { settings, create, parse, toJSON, loadScript, registerMethods, isBatchMode, isFunc, isStr, nsREX } from '../core.mjs'; import { select as d3_select, rgb as d3_rgb } from '../d3.mjs'; import { closeCurrentWindow, showProgress, loadOpenui5, ToolbarIcons, getColorExec } from '../gui/utils.mjs'; import { GridDisplay, getHPainter } from '../gui/display.mjs'; import { makeTranslate } from '../base/BasePainter.mjs'; import { convertColor } from '../base/colors.mjs'; import { selectActivePad, cleanup, resize, EAxisBits } from '../base/ObjectPainter.mjs'; import { RObjectPainter } from '../base/RObjectPainter.mjs'; import { RAxisPainter } from './RAxisPainter.mjs'; import { RFramePainter } from './RFramePainter.mjs'; import { RPadPainter } from './RPadPainter.mjs'; import { addDragHandler } from './TFramePainter.mjs'; /** * @summary Painter class for RCanvas * * @private */ class RCanvasPainter extends RPadPainter { /** @summary constructor */ constructor(dom, canvas) { super(dom, canvas, true); this._websocket = null; this.tooltip_allowed = settings.Tooltip; this.v7canvas = true; } /** @summary Cleanup canvas painter */ cleanup() { delete this._websocket; delete this._submreq; if (this._changed_layout) this.setLayoutKind('simple'); delete this._changed_layout; super.cleanup(); } /** @summary Returns canvas name */ getCanvasName() { const title = this.pad?.fTitle; return (!title || !isStr(title)) ? 'rcanvas' : title.replace(/ /g, '_'); } /** @summary Returns layout kind */ getLayoutKind() { const origin = this.selectDom('origin'), layout = origin.empty() ? '' : origin.property('layout'); return layout || 'simple'; } /** @summary Set canvas layout kind */ setLayoutKind(kind, main_selector) { const origin = this.selectDom('origin'); if (!origin.empty()) { if (!kind) kind = 'simple'; origin.property('layout', kind); origin.property('layout_selector', (kind !== 'simple') && main_selector ? main_selector : null); this._changed_layout = (kind !== 'simple'); // use in cleanup } } /** @summary Changes layout * @return {Promise} indicating when finished */ async changeLayout(layout_kind, mainid) { const current = this.getLayoutKind(); if (current === layout_kind) return true; const origin = this.selectDom('origin'), sidebar2 = origin.select('.side_panel2'), lst = []; let sidebar = origin.select('.side_panel'), main = this.selectDom(), force; while (main.node().firstChild) lst.push(main.node().removeChild(main.node().firstChild)); if (!sidebar.empty()) cleanup(sidebar.node()); if (!sidebar2.empty()) cleanup(sidebar2.node()); this.setLayoutKind('simple'); // restore defaults origin.html(''); // cleanup origin if (layout_kind === 'simple') { main = origin; for (let k = 0; k < lst.length; ++k) main.node().appendChild(lst[k]); this.setLayoutKind(layout_kind); force = true; } else { const grid = new GridDisplay(origin.node(), layout_kind); if (mainid === undefined) mainid = (layout_kind.indexOf('vert') === 0) ? 0 : 1; main = d3_select(grid.getGridFrame(mainid)); main.classed('central_panel', true).style('position', 'relative'); if (mainid === 2) { // left panel for Y sidebar = d3_select(grid.getGridFrame(0)); sidebar.classed('side_panel2', true).style('position', 'relative'); // bottom panel for X sidebar = d3_select(grid.getGridFrame(3)); sidebar.classed('side_panel', true).style('position', 'relative'); } else { sidebar = d3_select(grid.getGridFrame(1 - mainid)); sidebar.classed('side_panel', true).style('position', 'relative'); } // now append all childs to the new main for (let k = 0; k < lst.length; ++k) main.node().appendChild(lst[k]); this.setLayoutKind(layout_kind, '.central_panel'); // remove reference to MDIDisplay, solves resize problem origin.property('mdi', null); } // resize main drawing and let draw extras resize(main.node(), force); return true; } /** @summary Toggle projection * @return {Promise} indicating when ready * @private */ async toggleProjection(kind) { delete this.proj_painter; if (kind) this.proj_painter = { X: false, Y: false }; // just indicator that drawing can be preformed if (isFunc(this.showUI5ProjectionArea)) return this.showUI5ProjectionArea(kind); let layout = 'simple', mainid; switch (kind) { case 'XY': layout = 'projxy'; mainid = 2; break; case 'X': case 'bottom': layout = 'vert2_31'; mainid = 0; break; case 'Y': case 'left': layout = 'horiz2_13'; mainid = 1; break; case 'top': layout = 'vert2_13'; mainid = 1; break; case 'right': layout = 'horiz2_31'; mainid = 0; break; } return this.changeLayout(layout, mainid); } /** @summary Draw projection for specified histogram * @private */ async drawProjection(/* kind, hist, hopt */) { // dummy for the moment return false; } /** @summary Draw in side panel * @private */ async drawInSidePanel(canv, opt, kind) { const sel = ((this.getLayoutKind() === 'projxy') && (kind === 'Y')) ? '.side_panel2' : '.side_panel', side = this.selectDom('origin').select(sel); return side.empty() ? null : this.drawObject(side.node(), canv, opt); } /** @summary Checks if canvas shown inside ui5 widget * @desc Function should be used only from the func which supposed to be replaced by ui5 * @private */ testUI5() { return this.use_openui ?? false; } /** @summary Show message * @desc Used normally with web-based canvas and handled in ui5 * @private */ showMessage(msg) { if (!this.testUI5()) showProgress(msg, 7000); } /** @summary Function called when canvas menu item Save is called */ saveCanvasAsFile(fname) { const pnt = fname.indexOf('.'); this.createImage(fname.slice(pnt+1)) .then(res => this.sendWebsocket(`SAVE:${fname}:${res}`)); } /** @summary Send command to server to save canvas with specified name * @desc Should be only used in web-based canvas * @private */ sendSaveCommand(fname) { this.sendWebsocket('PRODUCE:' + fname); } /** @summary Return true if message can be send via web socket * @private */ canSendWebSocket() { return this._websocket?.canSend(); } /** @summary Send message via web socket * @private */ sendWebsocket(msg) { if (this._websocket?.canSend()) { this._websocket.send(msg); return true; } return false; } /** @summary Close websocket connection to canvas * @private */ closeWebsocket(force) { if (this._websocket) { this._websocket.close(force); this._websocket.cleanup(); delete this._websocket; } } /** @summary Use provided connection for the web canvas * @private */ useWebsocket(handle) { this.closeWebsocket(); this._websocket = handle; this._websocket.setReceiver(this); this._websocket.connect(); } /** @summary set, test or reset timeout of specified name * @desc Used to prevent overloading of websocket for specific function */ websocketTimeout(name, tm) { if (!this._websocket) return; if (!this._websocket._tmouts) this._websocket._tmouts = {}; const handle = this._websocket._tmouts[name]; if (tm === undefined) return handle !== undefined; if (tm === 'reset') { if (handle) { clearTimeout(handle); delete this._websocket._tmouts[name]; } } else if (!handle && Number.isInteger(tm)) this._websocket._tmouts[name] = setTimeout(() => { delete this._websocket._tmouts[name]; }, tm); } /** @summary Handler for websocket open event * @private */ onWebsocketOpened(/* handle */) { } /** @summary Handler for websocket close event * @private */ onWebsocketClosed(/* handle */) { if (!this.embed_canvas) closeCurrentWindow(); } /** @summary Handler for websocket message * @private */ onWebsocketMsg(handle, msg) { // console.log('GET_MSG ' + msg.slice(0,30)); if (msg === 'CLOSE') { this.onWebsocketClosed(); this.closeWebsocket(true); } else if (msg.slice(0, 5) === 'SNAP:') { msg = msg.slice(5); const p1 = msg.indexOf(':'), snapid = msg.slice(0, p1), snap = parse(msg.slice(p1+1)); this.syncDraw(true) .then(() => { if (!this.snapid && snap?.fWinSize) this.resizeBrowser(snap.fWinSize[0], snap.fWinSize[1]); }).then(() => this.redrawPadSnap(snap)) .then(() => { this.addPadInteractive(); handle.send(`SNAPDONE:${snapid}`); // send ready message back when drawing completed this.confirmDraw(); }).catch(err => { if (isFunc(this.showConsoleError)) this.showConsoleError(err); else console.log(err); }); } else if (msg.slice(0, 4) === 'JSON') { const obj = parse(msg.slice(4)); this.redrawObject(obj); } else if (msg.slice(0, 9) === 'REPL_REQ:') this.processDrawableReply(msg.slice(9)); else if (msg.slice(0, 4) === 'CMD:') { msg = msg.slice(4); const p1 = msg.indexOf(':'), cmdid = msg.slice(0, p1), cmd = msg.slice(p1+1), reply = `REPLY:${cmdid}:`; if ((cmd === 'SVG') || (cmd === 'PNG') || (cmd === 'JPEG') || (cmd === 'WEBP') || (cmd === 'PDF')) { this.createImage(cmd.toLowerCase()) .then(res => handle.send(reply + res)); } else if (cmd.indexOf('ADDPANEL:') === 0) { if (!isFunc(this.showUI5Panel)) handle.send(reply + 'false'); else { const window_path = cmd.slice(9), conn = handle.createNewInstance(window_path); // set interim receiver until first message arrives conn.setReceiver({ cpainter: this, onWebsocketOpened() { }, onWebsocketMsg(panel_handle, msg2) { const panel_name = (msg2.indexOf('SHOWPANEL:') === 0) ? msg2.slice(10) : ''; this.cpainter.showUI5Panel(panel_name, panel_handle) .then(res => handle.send(reply + (res ? 'true' : 'false'))); }, onWebsocketClosed() { // if connection failed, handle.send(reply + 'false'); }, onWebsocketError() { // if connection failed, handle.send(reply + 'false'); } }); // only when connection established, panel will be activated conn.connect(); } } else { console.log('Unrecognized command ' + cmd); handle.send(reply); } } else if ((msg.slice(0, 7) === 'DXPROJ:') || (msg.slice(0, 7) === 'DYPROJ:')) { const kind = msg[1], hist = parse(msg.slice(7)); this.drawProjection(kind, hist); } else if (msg.slice(0, 5) === 'SHOW:') { const that = msg.slice(5), on = that.at(-1) === '1'; this.showSection(that.slice(0, that.length - 2), on); } else console.log(`unrecognized msg len: ${msg.length} msg: ${msg.slice(0, 30)}`); } /** @summary Submit request to RDrawable object on server side */ submitDrawableRequest(kind, req, painter, method) { if (!this._websocket || !req || !req._typename || !painter.snapid || !isStr(painter.snapid)) return null; if (kind && method) { // if kind specified - check if such request already was submitted if (!painter._requests) painter._requests = {}; const prevreq = painter._requests[kind]; if (prevreq) { const tm = new Date().getTime(); if (!prevreq._tm || (tm - prevreq._tm < 5000)) { prevreq._nextreq = req; // submit when got reply return false; } delete painter._requests[kind]; // let submit new request after timeout } painter._requests[kind] = req; // keep reference on the request } req.id = painter.snapid; if (method) { if (!this._nextreqid) this._nextreqid = 1; req.reqid = this._nextreqid++; } else req.reqid = 0; // request will not be replied const msg = JSON.stringify(req); if (req.reqid) { req._kind = kind; req._painter = painter; req._method = method; req._tm = new Date().getTime(); if (!this._submreq) this._submreq = {}; this._submreq[req.reqid] = req; // fast access to submitted requests } this.sendWebsocket('REQ:' + msg); return req; } /** @summary Submit menu request * @private */ async submitMenuRequest(painter, menukind, reqid) { return new Promise(resolveFunc => { this.submitDrawableRequest('', { _typename: `${nsREX}RDrawableMenuRequest`, menukind: menukind || '', menureqid: reqid // used to identify menu request }, painter, resolveFunc); }); } /** @summary Submit executable command for given painter */ submitExec(painter, exec, subelem) { // snapid is intentionally ignored - only painter.snapid has to be used if (!this._websocket) return; if (subelem && isStr(subelem)) { const len = subelem.length; if ((len > 2) && (subelem.indexOf('#x') === len - 2)) subelem = 'x'; else if ((len > 2) && (subelem.indexOf('#y') === len - 2)) subelem = 'y'; else if ((len > 2) && (subelem.indexOf('#z') === len - 2)) subelem = 'z'; if ((subelem === 'x') || (subelem === 'y') || (subelem === 'z')) exec = subelem + 'axis#' + exec; else return console.log(`not recoginzed subelem ${subelem} in SubmitExec`); } this.submitDrawableRequest('', { _typename: `${nsREX}RDrawableExecRequest`, exec }, painter); } /** @summary Process reply from request to RDrawable */ processDrawableReply(msg) { const reply = parse(msg); if (!reply || !reply.reqid || !this._submreq) return false; const req = this._submreq[reply.reqid]; if (!req) return false; // remove reference first delete this._submreq[reply.reqid]; // remove blocking reference for that kind if (req._kind && req._painter?._requests) { if (req._painter._requests[req._kind] === req) delete req._painter._requests[req._kind]; } if (req._method) req._method(reply, req); // resubmit last request of that kind if (req._nextreq && !req._painter._requests[req._kind]) this.submitDrawableRequest(req._kind, req._nextreq, req._painter, req._method); } /** @summary Show specified section in canvas */ async showSection(that, on) { switch (that) { case 'Menu': break; case 'StatusBar': break; case 'Editor': break; case 'ToolBar': break; case 'ToolTips': this.setTooltipAllowed(on); break; } return true; } /** @summary Method informs that something was changed in the canvas * @desc used to update information on the server (when used with web6gui) * @private */ processChanges(kind, painter, subelem) { // check if we could send at least one message more - for some meaningful actions if (!this._websocket || !this._websocket.canSend(2) || !isStr(kind)) return; const msg = ''; if (!painter) painter = this; switch (kind) { case 'sbits': console.log('Status bits in RCanvas are changed - that to do?'); break; case 'frame': // when moving frame case 'zoom': // when changing zoom inside frame console.log('Frame moved or zoom is changed - that to do?'); break; case 'pave_moved': console.log('TPave is moved inside RCanvas - that to do?'); break; default: if ((kind.slice(0, 5) === 'exec:') && painter?.snapid) this.submitExec(painter, kind.slice(5), subelem); else console.log('UNPROCESSED CHANGES', kind); } if (msg) console.log('RCanvas::processChanges want to send ' + msg.length + ' ' + msg.slice(0, 40)); } /** @summary Handle pad button click event * @private */ clickPadButton(funcname, evnt) { if (funcname === 'ToggleGed') return this.activateGed(this, null, 'toggle'); if (funcname === 'ToggleStatus') return this.activateStatusBar('toggle'); return super.clickPadButton(funcname, evnt); } /** @summary returns true when event status area exist for the canvas */ hasEventStatus() { if (this.testUI5()) return false; if (this.brlayout) return this.brlayout.hasStatus(); const hp = getHPainter(); return hp ? hp.hasStatusLine() : false; } /** @summary Check if status bar can be toggled * @private */ canStatusBar() { return this.testUI5() || this.brlayout || getHPainter(); } /** @summary Show/toggle event status bar * @private */ activateStatusBar(state) { if (this.testUI5()) return; if (this.brlayout) this.brlayout.createStatusLine(23, state); else getHPainter()?.createStatusLine(23, state); this.processChanges('sbits', this); } /** @summary Show online canvas status * @private */ showCanvasStatus(...msgs) { if (this.testUI5()) return; const br = this.brlayout || getHPainter()?.brlayout; br?.showStatus(...msgs); } /** @summary Returns true if GED is present on the canvas */ hasGed() { if (this.testUI5()) return false; return this.brlayout?.hasContent() ?? false; } /** @summary Function used to de-activate GED * @private */ removeGed() { if (this.testUI5()) return; this.registerForPadEvents(null); if (this.ged_view) { this.ged_view.getController().cleanupGed(); this.ged_view.destroy(); delete this.ged_view; } this.brlayout?.deleteContent(true); this.processChanges('sbits', this); } /** @summary Get view data for ui5 panel * @private */ getUi5PanelData(/* panel_name */) { return { jsroot: { settings, create, parse, toJSON, loadScript, EAxisBits, getColorExec } }; } /** @summary Function used to activate GED * @return {Promise} when GED is there * @private */ async activateGed(objpainter, kind, mode) { if (this.testUI5() || !this.brlayout) return false; if (this.brlayout.hasContent()) { if ((mode === 'toggle') || (mode === false)) this.removeGed(); else objpainter?.getPadPainter()?.selectObjectPainter(objpainter); return true; } if (mode === false) return false; const btns = this.brlayout.createBrowserBtns(); ToolbarIcons.createSVG(btns, ToolbarIcons.diamand, 15, 'toggle fix-pos mode', 'browser') .style('margin', '3px').on('click', () => this.brlayout.toggleKind('fix')); ToolbarIcons.createSVG(btns, ToolbarIcons.circle, 15, 'toggle float mode', 'browser') .style('margin', '3px').on('click', () => this.brlayout.toggleKind('float')); ToolbarIcons.createSVG(btns, ToolbarIcons.cross, 15, 'delete GED', 'browser') .style('margin', '3px').on('click', () => this.removeGed()); // be aware, that jsroot_browser_hierarchy required for flexible layout that element use full browser area this.brlayout.setBrowserContent('<div class=\'jsroot_browser_hierarchy\' id=\'ged_placeholder\'>Loading GED ...</div>'); this.brlayout.setBrowserTitle('GED'); this.brlayout.toggleBrowserKind(kind || 'float'); return new Promise(resolveFunc => { loadOpenui5.then(sap => { d3_select('#ged_placeholder').text(''); sap.ui.require(['sap/ui/model/json/JSONModel', 'sap/ui/core/mvc/XMLView'], (JSONModel, XMLView) => { const oModel = new JSONModel({ handle: null }); XMLView.create({ viewName: 'rootui5.canv.view.Ged', viewData: this.getUi5PanelData('Ged') }).then(oGed => { oGed.setModel(oModel); oGed.placeAt('ged_placeholder'); this.ged_view = oGed; // TODO: should be moved into Ged controller - it must be able to detect canvas painter itself this.registerForPadEvents(oGed.getController().padEventsReceiver.bind(oGed.getController())); objpainter?.getPadPainter()?.selectObjectPainter(objpainter); this.processChanges('sbits', this); resolveFunc(true); }); }); }); }); } /** @summary produce JSON for RCanvas, which can be used to display canvas once again * @private */ produceJSON() { console.error('RCanvasPainter.produceJSON not yet implemented'); return ''; } /** @summary resize browser window to get requested canvas sizes */ resizeBrowser(fullW, fullH) { if (!fullW || !fullH || this.isBatchMode() || this.embed_canvas || this.batch_mode) return; this._websocket?.resizeWindow(fullW, fullH); } /** @summary draw RCanvas object */ static async draw(dom, can /* , opt */) { const nocanvas = !can; if (nocanvas) can = create(`${nsREX}RCanvas`); const painter = new RCanvasPainter(dom, can); painter.normal_canvas = !nocanvas; painter.createCanvasSvg(0); selectActivePad({ pp: painter, active: false }); return painter.drawPrimitives().then(() => { painter.addPadInteractive(); painter.addPadButtons(); painter.showPadButtons(); return painter; }); } } // class RCanvasPainter /** @summary draw RPadSnapshot object * @private */ function drawRPadSnapshot(dom, snap /* , opt */) { const painter = new RCanvasPainter(dom, null); painter.normal_canvas = false; painter.batch_mode = isBatchMode(); return painter.syncDraw(true).then(() => painter.redrawPadSnap(snap)).then(() => { painter.confirmDraw(); painter.showPadButtons(); return painter; }); } /** @summary Ensure RCanvas and RFrame for the painter object * @param {Object} painter - painter object to process * @param {string|boolean} frame_kind - false for no frame or '3d' for special 3D mode * @desc Assigns DOM, creates and draw RCanvas and RFrame if necessary, add painter to pad list of painters * @return {Promise} for ready * @private */ async function ensureRCanvas(painter, frame_kind) { if (!painter) return Promise.reject(Error('Painter not provided in ensureRCanvas')); // simple check - if canvas there, can use painter const pr = painter.getCanvSvg().empty() ? RCanvasPainter.draw(painter.getDom(), null /* noframe */) : Promise.resolve(true); return pr.then(() => { if ((frame_kind !== false) && painter.getFrameSvg().selectChild('.main_layer').empty()) return RFramePainter.draw(painter.getDom(), null, isStr(frame_kind) ? frame_kind : ''); }).then(() => { painter.addToPadPrimitives(); return painter; }); } /** @summary Function used for direct draw of RFrameTitle * @private */ function drawRFrameTitle(reason, drag) { const fp = this.getFramePainter(); if (!fp) return console.log('no frame painter - no title'); const rect = fp.getFrameRect(), fx = rect.x, fy = rect.y, fw = rect.width, // fh = rect.height, ph = this.getPadPainter().getPadHeight(), title = this.getObject(), title_width = fw, textFont = this.v7EvalFont('text', { size: 0.07, color: 'black', align: 22 }); let title_margin = this.v7EvalLength('margin', ph, 0.02), title_height = this.v7EvalLength('height', ph, 0.05); if (reason === 'drag') { title_height = drag.height; title_margin = fy - drag.y - drag.height; const changes = {}; this.v7AttrChange(changes, 'margin', title_margin / ph); this.v7AttrChange(changes, 'height', title_height / ph); this.v7SendAttrChanges(changes, false); // do not invoke canvas update on the server } this.createG(); makeTranslate(this.draw_g, fx, Math.round(fy-title_margin-title_height)); return this.startTextDrawingAsync(textFont, 'font').then(() => { this.drawText({ x: title_width/2, y: title_height/2, text: title.fText, latex: 1 }); return this.finishTextDrawing(); }).then(() => { addDragHandler(this, { x: fx, y: Math.round(fy-title_margin-title_height), width: title_width, height: title_height, minwidth: 20, minheight: 20, no_change_x: true, redraw: d => this.redraw('drag', d) }); }); } // ========================================================== registerMethods(`${nsREX}RPalette`, { extractRColor(rcolor) { const col = rcolor.fColor || 'black'; return convertColor(col); }, getColor(indx) { return this.palette[indx]; }, getContourIndex(zc) { const cntr = this.fContour; let l = 0, r = cntr.length - 1; if (zc < cntr[0]) return -1; if (zc >= cntr[r]) return r-1; if (this.fCustomContour) { while (l < r-1) { const mid = Math.round((l+r)/2); if (cntr[mid] > zc) r = mid; else l = mid; } return l; } // last color in palette starts from level cntr[r-1] return Math.floor((zc-cntr[0]) / (cntr[r-1] - cntr[0]) * (r-1)); }, getContourColor(zc) { const zindx = this.getContourIndex(zc); return (zindx < 0) ? '' : this.getColor(zindx); }, getContour() { return this.fContour && (this.fContour.length > 1) ? this.fContour : null; }, deleteContour() { delete this.fContour; }, calcColor(value, entry1, entry2) { const dist = entry2.fOrdinal - entry1.fOrdinal, r1 = entry2.fOrdinal - value, r2 = value - entry1.fOrdinal; if (!this.fInterpolate || (dist <= 0)) return convertColor((r1 < r2) ? entry2.fColor : entry1.fColor); // interpolate const col1 = d3_rgb(this.extractRColor(entry1.fColor)), col2 = d3_rgb(this.extractRColor(entry2.fColor)), color = d3_rgb(Math.round((col1.r*r1 + col2.r*r2)/dist), Math.round((col1.g*r1 + col2.g*r2)/dist), Math.round((col1.b*r1 + col2.b*r2)/dist)); return color.formatRgb(); }, createPaletteColors(len) { const arr = []; let indx = 0; while (arr.length < len) { const value = arr.length / (len-1), entry = this.fColors[indx]; if ((Math.abs(entry.fOrdinal - value) < 0.0001) || (indx === this.fColors.length - 1)) { arr.push(this.extractRColor(entry.fColor)); continue; } const next = this.fColors[indx+1]; if (next.fOrdinal <= value) indx++; else arr.push(this.calcColor(value, entry, next)); } return arr; }, getColorOrdinal(value) { // extract color with ordinal value between 0 and 1 if (!this.fColors) return 'black'; if ((typeof value !== 'number') || (value < 0)) value = 0; else if (value > 1) value = 1; // TODO: implement better way to find index let entry, next = this.fColors[0]; for (let indx = 0; indx < this.fColors.length - 1; ++indx) { entry = next; if (Math.abs(entry.fOrdinal - value) < 0.0001) return this.extractRColor(entry.fColor); next = this.fColors[indx+1]; if (next.fOrdinal > value) return this.calcColor(value, entry, next); } return this.extractRColor(next.fColor); }, setFullRange(min, max) { // set full z scale range, used in zooming this.full_min = min; this.full_max = max; }, createContour(logz, nlevels, zmin, zmax, zminpositive) { this.fContour = []; delete this.fCustomContour; this.colzmin = zmin; this.colzmax = zmax; if (logz) { if (this.colzmax <= 0) this.colzmax = 1.0; if (this.colzmin <= 0) { if ((zminpositive === undefined) || (zminpositive <= 0)) this.colzmin = 0.0001*this.colzmax; else this.colzmin = ((zminpositive < 3) || (zminpositive>100)) ? 0.3*zminpositive : 1; } if (this.colzmin >= this.colzmax) this.colzmin = 0.0001*this.colzmax; const logmin = Math.log(this.colzmin)/Math.log(10), logmax = Math.log(this.colzmax)/Math.log(10), dz = (logmax-logmin)/nlevels; this.fContour.push(this.colzmin); for (let level=1; level<nlevels; level++) this.fContour.push(Math.exp((logmin + dz*level)*Math.log(10))); this.fContour.push(this.colzmax); this.fCustomContour = true; } else { if ((this.colzmin === this.colzmax) && (this.colzmin !== 0)) { this.colzmax += 0.01*Math.abs(this.colzmax); this.colzmin -= 0.01*Math.abs(this.colzmin); } const dz = (this.colzmax-this.colzmin)/nlevels; for (let level=0; level<=nlevels; level++) this.fContour.push(this.colzmin + dz*level); } if (!this.palette || (this.palette.length !== nlevels)) this.palette = this.createPaletteColors(nlevels); } }); /** @summary draw RFont object * @private */ function drawRFont() { const font = this.getObject(), svg = this.getCanvSvg(), clname = 'custom_font_' + font.fFamily+font.fWeight+font.fStyle; let defs = svg.selectChild('.canvas_defs'); if (defs.empty()) defs = svg.insert('svg:defs', ':first-child').attr('class', 'canvas_defs'); let entry = defs.selectChild('.' + clname); if (entry.empty()) { entry = defs.append('style') .attr('type', 'text/css') .attr('class', clname) .text(`@font-face { font-family: "${font.fFamily}"; font-weight: ${font.fWeight ? font.fWeight : 'normal'}; font-style: ${font.fStyle ? font.fStyle : 'normal'}; src: ${font.fSrc}; }`); const p1 = font.fSrc.indexOf('base64,'), p2 = font.fSrc.lastIndexOf(' format('); if (p1 > 0 && p2 > p1) { const base64 = font.fSrc.slice(p1 + 7, p2 - 2), is_ttf = font.fSrc.indexOf('data:application/font-ttf') > 0; // TODO: for the moment only ttf format supported by jsPDF if (is_ttf) entry.property('$fontcfg', { n: font.fFamily, base64 }); } } if (font.fDefault) this.getPadPainter()._dfltRFont = font; return true; } /** @summary draw RAxis object * @private */ function drawRAxis(dom, obj, opt) { const painter = new RAxisPainter(dom, obj, opt); painter.disable_zooming = true; return ensureRCanvas(painter, false) .then(() => painter.redraw()) .then(() => painter); } /** @summary draw RFrame object * @private */ function drawRFrame(dom, obj, opt) { const p = new RFramePainter(dom, obj); if (opt === '3d') p.mode3d = true; return ensureRCanvas(p, false).then(() => p.redraw()); } export { ensureRCanvas, drawRPadSnapshot, drawRFrameTitle, drawRFont, drawRAxis, drawRFrame, RObjectPainter, RPadPainter, RCanvasPainter };