hist/RPavePainter.mjs

import { settings, isFunc, isStr, gStyle, nsREX } from '../core.mjs';
import { makeTranslate } from '../base/BasePainter.mjs';
import { RObjectPainter } from '../base/RObjectPainter.mjs';
import { ensureRCanvas } from '../gpad/RCanvasPainter.mjs';
import { addDragHandler } from '../gpad/TFramePainter.mjs';
import { createMenu } from '../gui/menu.mjs';


const ECorner = { kTopLeft: 1, kTopRight: 2, kBottomLeft: 3, kBottomRight: 4 };

/**
 * @summary Painter for RPave class
 *
 * @private
 */

class RPavePainter extends RObjectPainter {

   /** @summary Draw pave content
     * @desc assigned depending on pave class */
   async drawContent() { return this; }

   /** @summary Draw pave */
   async drawPave() {
      const rect = this.getPadPainter().getPadRect(),
            fp = this.getFramePainter();

      this.onFrame = fp && this.v7EvalAttr('onFrame', true);
      this.corner = this.v7EvalAttr('corner', ECorner.kTopRight);

      const visible = this.v7EvalAttr('visible', true),
            offsetx = this.v7EvalLength('offsetX', rect.width, 0.02),
            offsety = this.v7EvalLength('offsetY', rect.height, 0.02),
            pave_width = this.v7EvalLength('width', rect.width, 0.3),
            pave_height = this.v7EvalLength('height', rect.height, 0.3);

      this.createG();

      this.draw_g.classed('most_upper_primitives', true); // this primitive will remain on top of list

      if (!visible)
         return this;

      this.createv7AttLine('border_');

      this.createv7AttFill();

      const fr = this.onFrame ? fp.getFrameRect() : rect;
      let pave_x = 0, pave_y = 0;
      switch (this.corner) {
         case ECorner.kTopLeft:
            pave_x = fr.x + offsetx;
            pave_y = fr.y + offsety;
            break;
         case ECorner.kBottomLeft:
            pave_x = fr.x + offsetx;
            pave_y = fr.y + fr.height - offsety - pave_height;
            break;
         case ECorner.kBottomRight:
            pave_x = fr.x + fr.width - offsetx - pave_width;
            pave_y = fr.y + fr.height - offsety - pave_height;
            break;
         case ECorner.kTopRight:
         default:
            pave_x = fr.x + fr.width - offsetx - pave_width;
            pave_y = fr.y + offsety;
      }

      makeTranslate(this.draw_g, pave_x, pave_y);

      this.draw_g.append('svg:rect')
                 .attr('x', 0)
                 .attr('width', pave_width)
                 .attr('y', 0)
                 .attr('height', pave_height)
                 .call(this.lineatt.func)
                 .call(this.fillatt.func);

      this.pave_width = pave_width;
      this.pave_height = pave_height;

      // here should be fill and draw of text

      return this.drawContent().then(() => {
         if (!this.isBatchMode()) {
            // TODO: provide pave context menu as in v6
            if (settings.ContextMenu && this.paveContextMenu)
               this.draw_g.on('contextmenu', evnt => this.paveContextMenu(evnt));

            addDragHandler(this, { x: pave_x, y: pave_y, width: pave_width, height: pave_height,
                                   minwidth: 20, minheight: 20, redraw: d => this.sizeChanged(d) });
         }

         return this;
      });
   }

   /** @summary Process interactive moving of the stats box */
   sizeChanged(drag) {
      this.pave_width = drag.width;
      this.pave_height = drag.height;

      const pave_x = drag.x,
            pave_y = drag.y,
            rect = this.getPadPainter().getPadRect(),
            fr = this.onFrame ? this.getFramePainter().getFrameRect() : rect,
            changes = {};
      let offsetx, offsety;

      switch (this.corner) {
         case ECorner.kTopLeft:
            offsetx = pave_x - fr.x;
            offsety = pave_y - fr.y;
            break;
         case ECorner.kBottomLeft:
            offsetx = pave_x - fr.x;
            offsety = fr.y + fr.height - pave_y - this.pave_height;
            break;
         case ECorner.kBottomRight:
            offsetx = fr.x + fr.width - pave_x - this.pave_width;
            offsety = fr.y + fr.height - pave_y - this.pave_height;
            break;
         case ECorner.kTopRight:
         default:
            offsetx = fr.x + fr.width - pave_x - this.pave_width;
            offsety = pave_y - fr.y;
      }

      this.v7AttrChange(changes, 'offsetX', offsetx / rect.width);
      this.v7AttrChange(changes, 'offsetY', offsety / rect.height);
      this.v7AttrChange(changes, 'width', this.pave_width / rect.width);
      this.v7AttrChange(changes, 'height', this.pave_height / rect.height);
      this.v7SendAttrChanges(changes, false); // do not invoke canvas update on the server

      this.draw_g.selectChild('rect')
                 .attr('width', this.pave_width)
                 .attr('height', this.pave_height);

      this.drawContent();
   }

   /** @summary Redraw RPave object */
   async redraw(/* reason */) {
      return this.drawPave();
   }

   /** @summary draw RPave object */
   static async draw(dom, pave, opt) {
      const painter = new RPavePainter(dom, pave, opt, 'pave');
      return ensureRCanvas(painter, false).then(() => painter.drawPave());
   }

}


/**
 * @summary Painter for RLegend class
 *
 * @private
 */

class RLegendPainter extends RPavePainter {

   /** @summary draw RLegend content */
   async drawContent() {
      const legend = this.getObject(),
            textFont = this.v7EvalFont('text', { size: 12, color: 'black', align: 22 }),
            width = this.pave_width,
            height = this.pave_height,
            pp = this.getPadPainter();

      let nlines = legend.fEntries.length;
      if (legend.fTitle) nlines++;

      if (!nlines || !pp) return this;

      const stepy = height / nlines, margin_x = 0.02 * width;

      textFont.setSize(height/(nlines * 1.2));
      return this.startTextDrawingAsync(textFont, 'font').then(() => {
         let posy = 0;

         if (legend.fTitle) {
            this.drawText({ latex: 1, width: width - 2*margin_x, height: stepy, x: margin_x, y: posy, text: legend.fTitle });
            posy += stepy;
         }

         for (let i = 0; i < legend.fEntries.length; ++i) {
            const entry = legend.fEntries[i], w4 = Math.round(width/4);
            let objp = null;

            this.drawText({ latex: 1, width: 0.75*width - 3*margin_x, height: stepy, x: 2*margin_x + w4, y: posy, text: entry.fLabel });

            if (entry.fDrawableId !== 'custom')
               objp = pp.findSnap(entry.fDrawableId, true);
            else if (entry.fDrawable.fIO) {
               objp = new RObjectPainter(this.getPadPainter(), entry.fDrawable.fIO);
               if (entry.fLine) objp.createv7AttLine();
               if (entry.fFill) objp.createv7AttFill();
               if (entry.fMarker) objp.createv7AttMarker();
            }

            if (entry.fFill && objp?.fillatt) {
               this.draw_g
                  .append('svg:path')
                  .attr('d', `M${Math.round(margin_x)},${Math.round(posy + stepy*0.1)}h${w4}v${Math.round(stepy*0.8)}h${-w4}z`)
                  .call(objp.fillatt.func);
            }

            if (entry.fLine && objp?.lineatt) {
               this.draw_g
                  .append('svg:path')
                  .attr('d', `M${Math.round(margin_x)},${Math.round(posy + stepy/2)}h${w4}`)
                  .call(objp.lineatt.func);
            }

            if (entry.fError && objp?.lineatt) {
               this.draw_g
                  .append('svg:path')
                  .attr('d', `M${Math.round(margin_x + width/8)},${Math.round(posy + stepy*0.2)}v${Math.round(stepy*0.6)}`)
                  .call(objp.lineatt.func);
            }

            if (entry.fMarker && objp?.markeratt) {
               this.draw_g.append('svg:path')
                  .attr('d', objp.markeratt.create(margin_x + width/8, posy + stepy/2))
                  .call(objp.markeratt.func);
            }

            posy += stepy;
         }

         return this.finishTextDrawing();
      });
   }

   /** @summary draw RLegend object */
   static async draw(dom, legend, opt) {
      const painter = new RLegendPainter(dom, legend, opt, 'legend');
      return ensureRCanvas(painter, false).then(() => painter.drawPave());
   }

} // class RLegendPainter


/**
 * @summary Painter for RPaveText class
 *
 * @private
 */

class RPaveTextPainter extends RPavePainter {

   /** @summary draw RPaveText content */
   async drawContent() {
      const pavetext = this.getObject(),
            textFont = this.v7EvalFont('text', { size: 12, color: 'black', align: 22 }),
            width = this.pave_width,
            height = this.pave_height,
            nlines = pavetext.fText.length;

      if (!nlines) return;

      const stepy = height / nlines, margin_x = 0.02 * width;

      textFont.setSize(height/(nlines * 1.2));

      return this.startTextDrawingAsync(textFont, 'font').then(() => {
         for (let i = 0, posy = 0; i < pavetext.fText.length; ++i, posy += stepy)
            this.drawText({ latex: 1, width: width - 2*margin_x, height: stepy, x: margin_x, y: posy, text: pavetext.fText[i] });

         return this.finishTextDrawing(undefined, true);
      });
   }

   /** @summary draw RPaveText object */
   static async draw(dom, pave, opt) {
      const painter = new RPaveTextPainter(dom, pave, opt, 'pavetext');
      return ensureRCanvas(painter, false).then(() => painter.drawPave());
   }

} // class RPaveTextPainter

/**
 * @summary Painter for RHistStats class
 *
 * @private
 */

class RHistStatsPainter extends RPavePainter {

   /** @summary clear entries from stat box */
   clearStat() {
      this.stats_lines = [];
   }

   /** @summary add text entry to stat box */
   addText(line) {
      this.stats_lines.push(line);
   }

   /** @summary update statistic from the server */
   updateStatistic(reply) {
      this.stats_lines = reply.lines;
      this.drawStatistic(this.stats_lines);
   }

   /** @summary fill statistic */
   fillStatistic() {
      const pp = this.getPadPainter();
      if (pp?._fast_drawing) return false;

      const obj = this.getObject();
      if (obj.fLines !== undefined) {
         this.stats_lines = obj.fLines;
         delete obj.fLines;
         return true;
      }

      if (this.v7OfflineMode()) {
         const main = this.getMainPainter();
         if (!isFunc(main?.fillStatistic)) return false;
         // we take statistic from main painter
         return main.fillStatistic(this, gStyle.fOptStat, gStyle.fOptFit);
      }

      // show lines which are exists, maybe server request will be received later
      return (this.stats_lines !== undefined);
   }

   /** @summary Draw content */
   async drawContent() {
      if (this.fillStatistic())
         return this.drawStatistic(this.stats_lines);

      return this;
   }

   /** @summary Change mask */
   changeMask(nbit) {
      const obj = this.getObject(), mask = 1 << nbit;
      if (obj.fShowMask & mask)
         obj.fShowMask &= ~mask;
      else
         obj.fShowMask |= mask;

      if (this.fillStatistic())
         this.drawStatistic(this.stats_lines);
   }

   /** @summary Context menu */
   statsContextMenu(evnt) {
      evnt.preventDefault();
      evnt.stopPropagation(); // disable main context menu

      createMenu(evnt, this).then(menu => {
         const obj = this.getObject(),
             action = this.changeMask.bind(this);

         menu.header('Stat Box');

         for (let n = 0; n < obj.fEntries.length; ++n)
            menu.addchk((obj.fShowMask & (1<<n)), obj.fEntries[n], n, action);

         return this.fillObjectExecMenu(menu);
      }).then(menu => menu.show());
   }

   /** @summary Draw statistic */
   async drawStatistic(lines) {
      if (!lines) return this;
      const textFont = this.v7EvalFont('stats_text', { size: 12, color: 'black', align: 22 }),
            width = this.pave_width,
            height = this.pave_height,
            nlines = lines.length;
      let first_stat = 0, num_cols = 0, maxlen = 0;

      // adjust font size
      for (let j = 0; j < nlines; ++j) {
         const line = lines[j];
         if (j > 0) maxlen = Math.max(maxlen, line.length);
         if ((j === 0) || (line.indexOf('|') < 0)) continue;
         if (first_stat === 0) first_stat = j;
         const parts = line.split('|');
         if (parts.length > num_cols)
            num_cols = parts.length;
      }

      // for characters like 'p' or 'y' several more pixels required to stay in the box when drawn in last line
      const stepy = height / nlines, margin_x = 0.02 * width;
      let has_head = false,
          text_g = this.draw_g.selectChild('.statlines');
      if (text_g.empty())
         text_g = this.draw_g.append('svg:g').attr('class', 'statlines');
      else
         text_g.selectAll('*').remove();

      textFont.setSize(height/(nlines * 1.2));
      return this.startTextDrawingAsync(textFont, 'font', text_g).then(() => {
         if (nlines === 1)
            this.drawText({ width, height, text: lines[0], latex: 1, draw_g: text_g });
         else {
            for (let j = 0; j < nlines; ++j) {
               const posy = j*stepy;

               if (first_stat && (j >= first_stat)) {
                  const parts = lines[j].split('|');
                  for (let n = 0; n < parts.length; ++n) {
                     this.drawText({ align: 'middle', x: width * n / num_cols, y: posy, latex: 0,
                                    width: width/num_cols, height: stepy, text: parts[n], draw_g: text_g });
                  }
               } else if (lines[j].indexOf('=') < 0) {
                  if (j === 0) {
                     has_head = true;
                     const max_hlen = Math.max(maxlen, Math.round((width-2*margin_x)/stepy/0.65));
                     if (lines[j].length > max_hlen + 5)
                        lines[j] = lines[j].slice(0, max_hlen+2) + '...';
                  }
                  this.drawText({ align: (j === 0) ? 'middle' : 'start', x: margin_x, y: posy,
                                 width: width - 2*margin_x, height: stepy, text: lines[j], draw_g: text_g });
               } else {
                  const parts = lines[j].split('='), args = [];

                  for (let n = 0; n < 2; ++n) {
                     const arg = {
                        align: (n === 0) ? 'start' : 'end', x: margin_x, y: posy,
                        width: width-2*margin_x, height: stepy, text: parts[n], draw_g: text_g,
                        _expected_width: width-2*margin_x, _args: args,
                        post_process(painter) {
                        if (this._args[0].ready && this._args[1].ready)
                           painter.scaleTextDrawing(1.05*(this._args[0].result_width && this._args[1].result_width)/this.__expected_width, this.draw_g);
                        }
                     };
                     args.push(arg);
                  }

                  for (let n = 0; n < 2; ++n)
                     this.drawText(args[n]);
               }
            }
         }

         let lpath = '';

         if (has_head)
            lpath += 'M0,' + Math.round(stepy) + 'h' + width;

         if ((first_stat > 0) && (num_cols > 1)) {
            for (let nrow = first_stat; nrow < nlines; ++nrow)
               lpath += 'M0,' + Math.round(nrow * stepy) + 'h' + width;
            for (let ncol = 0; ncol < num_cols - 1; ++ncol)
               lpath += 'M' + Math.round(width / num_cols * (ncol + 1)) + ',' + Math.round(first_stat * stepy) + 'V' + height;
         }

         if (lpath) this.draw_g.append('svg:path').attr('d', lpath);

         return this.finishTextDrawing(text_g);
      });
   }

   /** @summary Redraw stats box */
   async redraw(reason) {
      if (reason && isStr(reason) && (reason.indexOf('zoom') === 0) && this.v7NormalMode()) {
         const req = {
            _typename: `${nsREX}RHistStatBoxBase::RRequest`,
            mask: this.getObject().fShowMask // lines to show in stat box
         };

         this.v7SubmitRequest('stat', req, reply => this.updateStatistic(reply));
      }

      return this.drawPave();
   }

   /** @summary draw RHistStats object */
   static async draw(dom, stats, opt) {
      const painter = new RHistStatsPainter(dom, stats, opt, stats);
      return ensureRCanvas(painter, false).then(() => painter.drawPave());
   }

} // class RHistStatsPainter

export { RPavePainter, RLegendPainter, RPaveTextPainter, RHistStatsPainter };