hist/TF2Painter.mjs

import { createHistogram, setHistogramTitle, kNoStats, settings, gStyle, clTF2, clTH2F, isFunc } from '../core.mjs';
import { TH2Painter } from '../hist/TH2Painter.mjs';
import { proivdeEvalPar } from '../base/func.mjs';
import { produceTAxisLogScale, scanTF1Options } from '../hist/TF1Painter.mjs';
import { ObjectPainter } from '../base/ObjectPainter.mjs';
import { DrawOptions, floatToString } from '../base/BasePainter.mjs';
import { THistPainter } from '../hist2d/THistPainter.mjs';


/**
  * @summary Painter for TF2 object
  *
  * @private
  */

class TF2Painter extends TH2Painter {

   #use_saved_points; // use saved points for drawing

   /** @summary Returns drawn object name */
   getObjectName() { return this.$func?.fName ?? 'func'; }

   /** @summary Returns drawn object class name */
   getClassName() { return this.$func?._typename ?? clTF2; }

   /** @summary Returns true while function is drawn */
   isTF1() { return true; }

   /** @summary Returns primary function which was then drawn as histogram */
   getPrimaryObject() { return this.$func; }

   /** @summary Update histogram */
   updateObject(obj /* , opt */) {
      if (!obj || (this.getClassName() !== obj._typename)) return false;
      delete obj.evalPar;
      const histo = this.getHisto();

      if (this.webcanv_hist) {
         const h0 = this.getPadPainter()?.findInPrimitives('Func', clTH2F);
         if (h0) this.updateAxes(histo, h0, this.getFramePainter());
      }

      this.$func = obj;
      this.createTF2Histogram(obj, histo);
      this.scanContent();
      return true;
   }

   /** @summary Redraw TF2
     * @private */
   redraw(reason) {
      if (!this.#use_saved_points && (reason === 'logx' || reason === 'logy' || reason === 'zoom')) {
         this.createTF2Histogram(this.$func, this.getHisto());
         this.scanContent();
      }

      return super.redraw(reason);
   }

   /** @summary Create histogram for TF2 drawing
     * @private */
   createTF2Histogram(func, hist) {
      let nsave = func.fSave.length - 6;
      if ((nsave > 0) && (nsave !== (func.fSave[nsave+4]+1) * (func.fSave[nsave+5]+1)))
         nsave = 0;

      this.#use_saved_points = (nsave > 0) && (settings.PreferSavedPoints || (this.use_saved > 1));

      const fp = this.getFramePainter(),
            pad = this.getPadPainter()?.getRootPad(true),
            logx = pad?.fLogx, logy = pad?.fLogy,
            gr = fp?.getGrFuncs(this.second_x, this.second_y);
      let xmin = func.fXmin, xmax = func.fXmax,
          ymin = func.fYmin, ymax = func.fYmax,
          npx = Math.max(func.fNpx, 20),
          npy = Math.max(func.fNpy, 20);

      if (gr?.zoom_xmin !== gr?.zoom_xmax) {
         const dx = (xmax - xmin) / npx;
         if ((xmin < gr.zoom_xmin) && (gr.zoom_xmin < xmax))
            xmin = Math.max(xmin, gr.zoom_xmin - dx);
         if ((xmin < gr.zoom_xmax) && (gr.zoom_xmax < xmax))
            xmax = Math.min(xmax, gr.zoom_xmax + dx);
      }

      if (gr?.zoom_ymin !== gr?.zoom_ymax) {
         const dy = (ymax - ymin) / npy;
         if ((ymin < gr.zoom_ymin) && (gr.zoom_ymin < ymax))
            ymin = Math.max(ymin, gr.zoom_ymin - dy);
         if ((ymin < gr.zoom_ymax) && (gr.zoom_ymax < ymax))
            ymax = Math.min(ymax, gr.zoom_ymax + dy);
      }

      const ensureBins = (nx, ny) => {
         if (hist.fNcells !== (nx + 2) * (ny + 2)) {
            hist.fNcells = (nx + 2) * (ny + 2);
            hist.fArray = new Float32Array(hist.fNcells);
         }
         hist.fArray.fill(0);
         hist.fXaxis.fNbins = nx;
         hist.fXaxis.fXbins = [];
         hist.fYaxis.fNbins = ny;
         hist.fYaxis.fXbins = [];
      };

      delete this._fail_eval;

      if (!this.#use_saved_points) {
         let iserror = false;

         if (!func.evalPar && !proivdeEvalPar(func))
            iserror = true;

         ensureBins(npx, npy);
         hist.fXaxis.fXmin = xmin;
         hist.fXaxis.fXmax = xmax;
         hist.fYaxis.fXmin = ymin;
         hist.fYaxis.fXmax = ymax;

         if (logx)
            produceTAxisLogScale(hist.fXaxis, npx, xmin, xmax);
         if (logy)
            produceTAxisLogScale(hist.fYaxis, npy, ymin, ymax);

         for (let j = 0; (j < npy) && !iserror; ++j) {
            for (let i = 0; (i < npx) && !iserror; ++i) {
               const x = hist.fXaxis.GetBinCenter(i+1),
                     y = hist.fYaxis.GetBinCenter(j+1);
               let z = 0;

               try {
                  z = func.evalPar(x, y);
               } catch {
                  iserror = true;
               }

               if (!iserror)
                  hist.setBinContent(hist.getBin(i + 1, j + 1), Number.isFinite(z) ? z : 0);
            }
         }

         if (iserror)
            this._fail_eval = true;

         if (iserror && (nsave > 6))
            this.#use_saved_points = true;
      }

      if (this.#use_saved_points) {
         npx = Math.round(func.fSave[nsave+4]);
         npy = Math.round(func.fSave[nsave+5]);
         xmin = func.fSave[nsave];
         xmax = func.fSave[nsave+1];
         ymin = func.fSave[nsave+2];
         ymax = func.fSave[nsave+3];
         const dx = (xmax - xmin) / npx, dy = (ymax - ymin) / npy, getSave = (x, y) => {
            if (x < xmin || x > xmax || dx <= 0) return 0;
            if (y < ymin || y > ymax || dy <= 0) return 0;
            const ibin = Math.min(npx-1, Math.floor((x-xmin)/dx)),
                  jbin = Math.min(npy-1, Math.floor((y-ymin)/dy)),
                  xlow = xmin + ibin*dx,
                  ylow = ymin + jbin*dy,
                  t = (x-xlow)/dx,
                  u = (y-ylow)/dy,
                  k1 = jbin*(npx+1) + ibin,
                  k2 = jbin*(npx+1) + ibin +1,
                  k3 = (jbin+1)*(npx+1) + ibin +1,
                  k4 = (jbin+1)*(npx+1) + ibin;
            return (1-t)*(1-u)*func.fSave[k1] +t*(1-u)*func.fSave[k2] +t*u*func.fSave[k3] + (1-t)*u*func.fSave[k4];
         };

         ensureBins(func.fNpx, func.fNpy);
         hist.fXaxis.fXmin = func.fXmin;
         hist.fXaxis.fXmax = func.fXmax;
         hist.fYaxis.fXmin = func.fYmin;
         hist.fYaxis.fXmax = func.fYmax;

         for (let j = 0; j < func.fNpy; ++j) {
            const y = hist.fYaxis.GetBinCenter(j + 1);
            for (let i = 0; i < func.fNpx; ++i) {
               const x = hist.fXaxis.GetBinCenter(i + 1),
                     z = getSave(x, y);
               hist.setBinContent(hist.getBin(i+1, j+1), Number.isFinite(z) ? z : 0);
            }
         }
      }

      hist.fName = 'Func';
      setHistogramTitle(hist, func.fTitle);
      hist.fMinimum = func.fMinimum;
      hist.fMaximum = func.fMaximum;
      // fHistogram->SetContour(fContour.fN, levels);
      hist.fLineColor = func.fLineColor;
      hist.fLineStyle = func.fLineStyle;
      hist.fLineWidth = func.fLineWidth;
      hist.fFillColor = func.fFillColor;
      hist.fFillStyle = func.fFillStyle;
      hist.fMarkerColor = func.fMarkerColor;
      hist.fMarkerStyle = func.fMarkerStyle;
      hist.fMarkerSize = func.fMarkerSize;
      hist.fBits |= kNoStats;

      return hist;
   }

   /** @summary Extract function ranges */
   extractAxesProperties(ndim) {
      super.extractAxesProperties(ndim);

      const func = this.$func, nsave = func?.fSave.length ?? 0;

      if (nsave > 6 && this.#use_saved_points) {
         this.xmin = Math.min(this.xmin, func.fSave[nsave-6]);
         this.xmax = Math.max(this.xmax, func.fSave[nsave-5]);
         this.ymin = Math.min(this.ymin, func.fSave[nsave-4]);
         this.ymax = Math.max(this.ymax, func.fSave[nsave-3]);
      }
      if (func) {
         this.xmin = Math.min(this.xmin, func.fXmin);
         this.xmax = Math.max(this.xmax, func.fXmax);
         this.ymin = Math.min(this.ymin, func.fYmin);
         this.ymax = Math.max(this.ymax, func.fYmax);
      }
   }

   /** @summary return tooltips for TF2 */
   getTF2Tooltips(pnt) {
      const lines = [this.getObjectHint()],
            funcs = this.getFramePainter()?.getGrFuncs(this.options.second_x, this.options.second_y);

      if (!funcs || !isFunc(this.$func?.evalPar)) {
         lines.push('grx = ' + pnt.x, 'gry = ' + pnt.y);
         return lines;
      }

      const x = funcs.revertAxis('x', pnt.x),
            y = funcs.revertAxis('y', pnt.y);
      let z = 0, iserror = false;

       try {
          z = this.$func.evalPar(x, y);
       } catch {
          iserror = true;
       }

      lines.push('x = ' + funcs.axisAsText('x', x),
                 'y = ' + funcs.axisAsText('y', y),
                 'value = ' + (iserror ? '<fail>' : floatToString(z, gStyle.fStatFormat)));
      return lines;
   }

   /** @summary process tooltip event for TF2 object */
   processTooltipEvent(pnt) {
      if (this.#use_saved_points)
         return super.processTooltipEvent(pnt);

      let ttrect = this.draw_g?.selectChild('.tooltip_bin');

      if (!this.draw_g || !pnt) {
         ttrect?.remove();
         return null;
      }

      const res = { name: this.$func?.fName, title: this.$func?.fTitle,
                  x: pnt.x, y: pnt.y,
                  color1: this.lineatt?.color ?? 'green',
                  color2: this.fillatt?.getFillColorAlt('blue') ?? 'blue',
                  lines: this.getTF2Tooltips(pnt), exact: true, menu: true };

      if (pnt.disabled)
         ttrect.remove();
      else {
         if (ttrect.empty()) {
            ttrect = this.draw_g.append('svg:circle')
                                .attr('class', 'tooltip_bin')
                                .style('pointer-events', 'none')
                                .style('fill', 'none')
                                .attr('r', (this.lineatt?.width ?? 1) + 4);
         }

         ttrect.attr('cx', pnt.x)
               .attr('cy', pnt.y);
         if (this.lineatt)
            ttrect.call(this.lineatt.func);
      }

      return res;
   }

   /** @summary fill information for TWebCanvas
     * @desc Used to inform web canvas when evaluation failed
     * @private */
   fillWebObjectOptions(opt) {
      opt.fcust = this._fail_eval && !this.use_saved ? 'func_fail' : '';
   }

   /** @summary draw TF2 object */
   static async draw(dom, tf2, opt) {
      const web = scanTF1Options(opt);
      opt = web.opt;
      delete web.opt;

      const d = new DrawOptions(opt);
      if (d.empty())
         opt = 'cont3';
      else if (d.opt === 'SAME')
         opt = 'cont2 same';

      let hist;

      if (web.webcanv_hist) {
         const dummy = new ObjectPainter(dom);
         hist = dummy.getPadPainter()?.findInPrimitives('Func', clTH2F);
      }

      if (!hist) {
         hist = createHistogram(clTH2F, 20, 20);
         hist.fBits |= kNoStats;
      }

      const painter = new TF2Painter(dom, hist);

      painter.$func = tf2;
      Object.assign(painter, web);
      painter.createTF2Histogram(tf2, hist);

      return THistPainter._drawHist(painter, opt);
   }

} // class TF2Painter

export { TF2Painter };