Source: curves-grid.js

/**
 * Grid module of the GSfc4q project.
 * Source-code:  https://github.com/ppkrauss/Sfc4q
 * License: Apache2, http://www.apache.org/licenses/LICENSE-2.0
 */


/**
 * Grid of a [Space-Filling Curve](https://en.wikipedia.org/wiki/Space-filling_curve) (SFC) of recursive
 * 4-partitions of quadrilaterals. This class implements geometry and operations to handle grids.
 * It is used as complement of the Sfc4q class.
 *
 * Concepts:
 *
 * **grid** of the **unit square** (canvas box): the mathematical entity used as total area filled by the SFC, is the unit square. It can be transformed into any other quadrilateral, it is a reference **canvas geometry**. Partitions over unit square produces the grid of *blevel*. The SFC forms a path conecting centers of the cells of the grid, and SFC distance is used as index to identify the cells of the grid. The set of all *blevel* grids can be named "hierarchical grid", but sometimes we abbreviate to "grid". Grids of *isHalf* levels are named "degenerated grids". The unit square with no partition is the primordial cell, a grid of level zero.
 *
 * **box width** and **box height**: the  unit square is projected to a real "box", used as canvas of the grid.
 *
 * **sfc4**: any SFC object used as reference for *level*, *blevel*, *key*, *bkey*, *id0*, *id* and its labels and operations, mainly decode/encode to IJ coordinates. Typical objects are instances of the *GSfc4qLbl* class extensions to concrete curves, like *GSfc4qLbl_Morton* or *GSfc4qLbl_Hilbert* classes.
 *
 * **(i,j)** coordinates, or "integer [XY grid](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Positions#The_grid)": see grid of the "blind structure". ![](../assets/svg_coordinates.png)
 *   <br/>“i” and “j” are integers in the range [0,2^blevel-1]. IJ=(0,0) in the top-left corner.
 *   <br/>“i” scan columns from left to right, “j” scan lines from top to bottom.
 *
 * Concepts implemented by the Sfc4q class: *level*, *blevel*, *key*, *bkey*, *IJ* coordinates, (i,j).
 *
 */
class GridOfCurve {

  constructor(sfc4, box_width, box_height=null) {
    if (sfc4!==undefined)
      this.refresh(sfc4,box_width,box_height)
    else console.log("ERROR: GridOfCurve(null) is perhaps invalid")
  }

  refresh(sfc4, box_width, box_height=null) {
    // CONFIGS:
    if (sfc4)       this.sfc4 = sfc4; // else reuse or error
    this.box_width  = box_width || this.box_width || null; // null for GridOfCurve_D3.buildSvg()
    this.box_height = box_height || this.box_height || this.box_width;
    this.refreshProperties()
  }

  refreshProperties(novoLevel=null) {
    if (this.sfc4!==null && this.box_width) {
      if (novoLevel) this.sfc4.refresh(novoLevel);
      if (!this.box_height) this.box_height =this.box_width;
      this.needSwap = this.sfc4.needSwap && (this.box_width != this.box_height)
      this.cell_refWidth  = this.box_width/Number(this.sfc4.nRefRows)
      this.cell_refHeight = this.box_height/Number(this.sfc4.nRefRows)
      this.cell_area   = this.box_width*this.box_height/Number(this.sfc4.nBKeys) // MUST be constant!
      // properties of the unit grid, used in (s,t) coordinates:
      // usar SizedBigInt.bigint_div():
      //  this.cell_stWidth = 1n/this.sfc4.nRefRows    // and height==width
      //  this.cell_stArea  = 1n/this.sfc4.nBKeys
    }
  }

  /**
   * Translates (i,j) coordinates to (x,y) coordinates, using grid properties.
   * @param integer i: the grid row coordinate.
   * @param integer j: the grid column coordinate.
   * @return [float,float].
   */
  ij_to_xy(i,j,shift=0) { // if rotates, must be checked
    if (shift) [i,j] = [i+shift, j+shift]; // cast to float
    if (this.needSwap) {
      let [x,y] = this.sfc4.ij_swapSides(i,j,this.cell_refWidth,this.cell_refHeight)
      return [i*x, j*y]
    } else
      return [i*this.cell_refWidth, j*this.cell_refHeight]
  }

  ij_to_xyCenter(i,j) {
    return this.ij_to_xy(i,j,0.5)
  }

  /**
   * Translates (x,y) coordinates to (i,j) coordinates, using grid properties.
   * @param float x: the spatial X coordinate.
   * @param float y: the spatial Y coordinate.
   * @return [integer,integer].
   */
  xy_to_ij(x,y) {
    let ref_i = Math.floor(x/this.cell_refWidth),
        ref_j = Math.floor(y/this.cell_refHeight)
    if (this.needSwap) {
      console.log("BUG: needSwap need to develop xy_to_ij()")
      // for Hilbert can check S2 geometry, a (s,t) to (i,j) resolution.
      // use ij_nSwaps(ref_i,ref_j) as first guess
      // scan to obtain number of xSwaps and ySwaps... use the unit s,t before x,y.
    } else
      return [ref_i,ref_j];
  }

} // \GridOfCurve


/**
 * D3 grid, a GridOfCurve class extension.  D3 is the [D3js.org](https://D3js.org) framework, used to build the grid and its tools.
 */
class GridOfCurve_D3 extends GridOfCurve {

  constructor(conf, sfc4, layout, box_width, box_height=null, grdID=1, reftab='chartsContainer') {
    const toBuildHere=true, toBuildToolTip=true  // future configs
    if (typeof conf == 'object') {
      super(conf.sfc4, conf.box_width, conf.box_height)
      this.refresh_D3(conf.domRef_id, conf.grdID, conf.reftab)
      layout = conf.layout
    }  else if (conf!==undefined) {
      super(sfc4, box_width, box_height)
      this.refresh_D3(conf, grdID, reftab)
    }
    this.layout = layout || {rects:true, circles:true, labelMain:true, labelIJ:true};
    this.distinctColors = {}
    this.lblBase='std'
    this.lblChk = null
    this.D3canvas = null
    if (toBuildHere) this.build(true)
    if (toBuildToolTip) this.tooltip_build() // conferir se precisa rebuild das propriedades do data
    this.build_zoomTool() // need if
  }

  refresh_D3(domRef_id, grdID, reftab) {
    this.domRef_id = domRef_id || null
    this.grdID     = grdID     || null
    this.reftabDOM = document.getElementById(reftab);
    if (!this.reftabDOM) console.log("ops, need CORRECT 'reftab' for canvas sizes")
    // check need for call buildSvg() here
  }

  buildSvg(firstBuild=true,colwidth=40) {
    if (firstBuild || !this.D3_svg) {
        if (this.D3_svg)
          this.D3_svg.selectAll("*").remove(); // testar
        if (!this.box_width) {
          let refWidth = parseInt( this.reftabDOM.getBoundingClientRect().width/2.0 );
          this.box_width = Math.min(window.innerWidth, refWidth) - colwidth;
          this.refreshProperties()
        }
        if (this.box_width< conf_canvasWidth_min) {
            let perc = Math.round( 100*(conf_canvasWidth_min-this.box_width)/conf_canvasWidth_min )
            let msg = `This visualization not works with small screens.\nPlease use a screen ${perc}% bigger.`
            if (conf_alertLevel>1) alert(msg); else console.log(msg);
        }
        let theChart = '#'+ this.domRef_id +' svg.theChart';
        this.D3_svg0 = d3.select(theChart).attr("width", this.box_width+10).attr("height", this.box_width+10)
        this.D3canvas = this.D3_svg0
        this.D3_svg = this.D3_svg0.append("g"); // set reference D3 for all builds
        this.D3_svg.attr("class", "the_grid" + (this.grdID? ` grdID${this.grdID}`: '') );
    } else
      this.D3_svg.selectAll("*").remove(); // remove elements from svg/g, preserve SVG attributes and catchall
  } // \buildSvg

  bitsToColors(bs,len) { // enhancing prefixes
    if (typeof bs != 'string') return null; // ensure
    if (bs.length<len) bs = bs.padStart(len,'0')
    let d;
    if (len%2) { bs+='0'; len++; } // more one bit if not even
    if (len<6) {
      let R = '1'+bs.slice(0,1) + bs.slice(0,2).padEnd(2,'1')
      let G = '1'+bs.slice(3,4) + bs.slice(3,5).padEnd(2,'1')
      let B = (len>2? bs.slice(-2): '11')
      d=[ R, G, '1'+B+B.slice(0,1) ]      // binary RGB color
    } else if (len==12)
      return '#'+parseInt(bs,2).toString(16).padStart(3,'0')
    else {
      let R = bs.slice(0,4)
      let G = bs.slice(1,2)+bs.slice(3,len<13?5:6).padEnd(3,'1')
      let B = bs.slice(-4) // (bs.slice(0,1)=='0')? ('1'+bs.slice(-3)): bs.slice(-4)
      d=[R, G, B ]        // binary RGB color
    }
    let dHex = d.map(x => parseInt(x,2).toString(16) ).join('')
    return '#'+dHex;
  }

  dataBuild(stopOn=0,xpos=1,ypos=1,useDstClrs=true) {
    if (!stopOn) this.distinctColors = {}
    let nBKeys = Number(this.sfc4.nBKeys) // revisar!
    let nBKeysFrac = Math.round(nBKeys/150)
    if (this.sfc4.isHalf) nBKeys = nBKeys/2;
    let rw0 = this.cell_refWidth
    let rh0 = this.cell_refHeight
    var r = [].fill(null,0,nBKeys-1)  // será revisto e oTheFly!
    var mySfc = this.sfc4
    const lblBase = this.lblBase
    const lblIs32  = (lblBase=='32' && !(this.sfc4.level%2.5))
    const lblIs16  = (this.sfc4.level>2)
    const lblIsDec = (lblBase=='dec')
    this.lblChk = [
      lblIsDec,lblIs32,lblIs16,  // using check-order
      lblIsDec? "decimal": lblIs32? "base 32": lblIs16? "base 16h": "base 4h"
    ];
    if (stopOn===true) stopOn = Math.round(nBKeys/3) // 2*Math.sqrt(nBKeys)
    let maxIdLoop = (stopOn && nBKeys>4)? stopOn: nBKeys;
    for(let id=0; id<maxIdLoop; id++) {
      let [ij0,ij1] = this.sfc4.key_decode(id)
      let xy = this.ij_to_xy( ij0[0], ij0[1] )  //xy0
      let rw = rw0, rh = rh0;
      let idx = mySfc.setKey(id);
      let colorCode = this.bitsToColors( idx.id_toString('2'), idx.keyBits ) // toBitString
      let id4 = idx.id_toString('4h'),     id16   = idx.id_toString('16h'),
          id32 = idx.id_toString('32nvu')
      if (this.sfc4.isHalf) {
         let xy1 = this.ij_to_xy( ij1[0], ij1[1] )
         if (Math.abs(xy[0]-xy1[0]) > 0) rw = rw*2;  // revisar
         if (Math.abs(xy[1]-xy1[1]) > 0) rh = rh*2;
         xy[0] = Math.min(xy[0],xy1[0]);
         xy[1] = Math.min(xy[1],xy1[1]);
      }
      let idPub = lblIsDec? id: lblIs32? id32: lblIs16? id16: id4;
      if (!stopOn && useDstClrs && (nBKeys<150 || (id%nBKeysFrac)==1) )
        this.distinctColors[colorCode] = idPub;
      r[id] = { id:id, idPub:idPub, i:ij0[0], j:ij0[1], x:xy[0], y:xy[1], width:rw, height:rh, color:colorCode };
    }
    return r;
  }

  build(firstBuild=true, line_width=2) { //  draw grid!
    if (conf_alertLevel>1) console.log("debug build:",this.sfc4.curveName)
    this.buildSvg(firstBuild);
    var myThis = this
    let rw = this.cell_refWidth
    var mySfc = this.sfc4
    let nBKeys = Number(mySfc.nBKeys)
    let bits = (mySfc.nBKeys-1n).toString(2).length
    var sbi = new SizedBigInt(); // this.sfc4.setID_byKey(3):
    let D3DataEnter = this.D3_svg.selectAll()
      .data( this.dataBuild() )
      .enter()

    if (this.layout.rects && rw>15) // // // Red rectangular grid:
      D3DataEnter.append("rect")
      .attr("x", d => d.x ).attr("y", d => d.y )
      .attr("width",d => d.width).attr("height",d => {return d.height})
      .style("fill","#FFF").style("stroke","#F00");

    let fracTime = ((nBKeys>1000)? 9000: (nBKeys>200)? 3800: 2800)/nBKeys;
    //let fracFirst = nBKeys/((nBKeys>1000)? 100: (nBKeys>10)? 10: 2);
    if (this.layout.circles)  // // // Colured centroid and point-grid circles:
      D3DataEnter.append("circle")
      .attr('cx', d => d.x + d.width/2)
      .attr('cy', d => d.y + d.height/2)
      .attr("r", rw/3)  // (rw<20)? 5:10
      .style("fill", d=> d.color )
      .call( (s,lev) => {if (lev<4) s.style("stroke", "#F00")}, mySfc.level)
      // animation:
      .style("opacity", 0).transition()
        .duration(0)
        .delay(  (_, i) => i*fracTime + (i?4:0)*fracTime/Math.log2(2+i)  )
        .style("opacity", 1);

    if (this.layout.labelMain && rw>20) { // cell ID label text
      let test = this.dataBuild(true);
      if (this.sfc4.nBKeys>4n && test.length>1) this.D3_svg.selectAll()
        .data( test )
        .enter().append("text")
          .attr("x", d => d.x+rw/2.1 -1 )
          .attr("y", d => d.y+rw/1.7 +1)
          .text( d => d.idPub )
          .style("font-size", (rw<40)? "12pt":"14pt")
          .attr("fill","#FFF").style("style","label");

      D3DataEnter.append("text")
      .attr("x", d => d.x+rw/2.1 )
      .attr("y", d => d.y+rw/1.7 )
      .text( d => d.idPub )
      .style("font-size", (rw<40)? "10pt":"12pt").style("style","label")
      /* .call( (s,cond) => {
        if (cond) s.attr("transform", "translate(2,2) rotate(90)")
        console.log(cond)
      }, mySfc.isHalf && d.width<d.height);
      */
    }

    if (this.layout.labelIJ && rw>35) // (i,j) label text
      D3DataEnter.append("text")
      .attr("x", d => d.x+0.3 + (mySfc.isHalf? 0.8: 0))
      .attr("y", d => d.y+10 )
      .text( d => `${d.i},${d.j}` )
      .style("font-size", "8pt").attr("fill","#A44").style("style","label");
  } // \build

  buildCaption(svgSelect='#rainbow svg')  {
    const myC = this.distinctColors,
          wd = 10,
          wdExtra = wd+30,
          yShift = 15
    var colors = Object.keys(this.distinctColors)
    var myC_len = colors.length
    var myC_lenFrac = Math.round(myC_len/12)
    var h = this.box_width/(myC_len+1)
    colors.sort()
    let rb = d3.select(svgSelect)
    rb.selectAll("*").remove();
    rb.attr("width", wdExtra)
      .attr("height", this.box_width+20);

    rb.selectAll().data( colors ).enter().append("rect")
      .attr("x", 0 )
      .attr("y", (d,i) => i*h + yShift)
      .attr("width",wd).attr("height",h)
      .style("fill",d=>d);
    rb.selectAll().data( colors ).enter().append("text")
      .attr("x", wd )
      .attr("y", (d,i) => h+i*h+yShift )
      .text( (d,i) => (myC_len<9 || (i%myC_lenFrac)==0)? myC[d]: '' )
      .style("font-size","8pt").attr("fill","#000").style("style","label");
  }

  tooltip_msg(ij) {
    const lck = this.lblChk
    if (lck===null) return '';
    let id = this.sfc4.sbiID.val // number
    let id4  = this.sfc4.id_toString('4h')
    let id16 = this.sfc4.id_toString('16h')
    let id32 = this.sfc4.id_toString('32nvu')
    let idPub     = lck[0]? id: lck[1]? id32: lck[2]? id16: id4;
    let hex = (!lck[0] && !lck[1] && lck[2])?
              '':    `base 16h: ${adTag(id16)}`;
    let dec = lck[0]?  '':  `decimal: ${adTag(id)}${hex? '<br/>':''}`;
    let b4  = (idPub!=id4)? `<br/>base4h: ${adTag(id4)}`: '';
    return `${lck[3]}: ${adTag(idPub)}<hr/> ${dec}${hex}${b4}<br/>(<i>i,j</i>)=(${ij[0]},${ij[1]})`;
  }

  tooltip_build() {
  	const domRef = d3.select('#'+ this.domRef_id);
  	const tpNode = domRef.select('div.theChartTooltip');
  	var mySVG   = this.D3_svg;
  	var myThis  = this;
    var mySfc = this.sfc4  //const
    var lastCellPos = [null,null];
  	this.D3canvas  // BUG on catchall!
    	.on('mouseover', function() {
      	tpNode.style("display", "inline");
    	})
    	.on('mouseout', function() {
        tpNode.style("display", "none");
        // must to draw and drop rectangle, not reuse background.
        /* revisar class xy, melhor usar Key como seletor, k123, visto que é seletor local
        if (lastCellPos[0]!==null) {
      		mySVG.select(`rect.xy${lastCellPos[0]}-${lastCellPos[1]}`).style("fill", '#FFF');
      		if (this.sfc4.isHalf) //NAO PRECISA MAIS pois é key
      			mySVG.select(`rect.xy${lastCellPos[3]}-${lastCellPos[4]}`).style("fill", '#FFF'); // lastCellPos[2]
      	}
        */
      	lastCellPos = [null,null];
  	})
    .on('mousemove', function () {
      // falta pintar um retangulo
      var coords = d3.mouse(mySVG.node());
      var grd_IJ = myThis.xy_to_ij(coords[0], coords[1]);
      if (lastCellPos[0]!=grd_IJ[0] || lastCellPos[1]!=grd_IJ[1]) { // only to reduce CPU costs
        mySfc.setBkey_byIJ(grd_IJ)
        let msg = myThis.tooltip_msg(grd_IJ)
        tpNode.html(msg)
        lastCellPos = grd_IJ
      }
      tpNode.style('left', d3.event.pageX+'px')
            .style('top', d3.event.pageY+'px');
    })
    .append("rect")  // the "catch all mouse event" area
      .attr("x",0).attr("y",0)
      .attr("class", "catchall") // see https://stackoverflow.com/a/16923563/287948 and  http://jsfiddle.net/H3W3k/
      .attr("width",myThis.box_width).attr("height",myThis.box_width)
      .style('visibility', 'hidden')
      //.attr("opacity", 0.0).style("fill","#FFF").style("stroke","#F00"); // need only the external line
    ; // \D3canvas

  } // \Tooltip

  build_zoomTool() {
    // Canvas zoom/pan
    let bxw = this.box_width+10;
    let canvas = this.D3canvas
    this.D3_svg0.call(d3.zoom()
        .translateExtent([ [0,0], [bxw,bxw] ])
        .scaleExtent([1, Infinity])
        .on("zoom", function() {
          if (xzoomNode.value==1)
            canvas.attr("transform", d3.event.transform);
        })
    );
} // build_zoomTool


} // \GridOfCurve_D3


////////////// non-exported components of this module.

function adTag(s,tag="code") { return `<${tag}>${s}</${tag}>`; }


function baseItens(sbi_id,ij) {
  // Here define the presentation order of bases.
  let crvID   = sbi_id.val // or int
  var crvID4  = sbi_id.toString('4h');  // parseInt(crvID).toString(4).padStart(globOrder,'0');
  var crvID16 = sbi_id.toString('16h'); // parseInt(crvID).toString(16).padStart(globOrder,'0');
  var crvID32 = sbi_id.toString('32nvu');  // parseInt(crvID).toString(32).padStart(globOrder/2,'0');
  let msgb32 = (globOrder_exact % 2.5)? '': `<hr/>base32nvu: ${adTag(crvID32)}`;
  let msg;
  if (globOrder_exact>2) msg = `base16h: ${adTag(crvID16)}${msgb32}<hr/>base4: ${adTag(crvID4)}`;
  else msg = `base4h: ${adTag(crvID4)}<hr/>base16h: ${adTag(crvID16)}`;
  return msg + `<br/>decimal: ${adTag(crvID)}<br/>(i,j)=(${ij[0]},${ij[1]})`;
}