Source: curves-grid.js

  1. /**
  2. * Grid module of the GSfc4q project.
  3. * Source-code: https://github.com/ppkrauss/Sfc4q
  4. * License: Apache2, http://www.apache.org/licenses/LICENSE-2.0
  5. */
  6. /**
  7. * Grid of a [Space-Filling Curve](https://en.wikipedia.org/wiki/Space-filling_curve) (SFC) of recursive
  8. * 4-partitions of quadrilaterals. This class implements geometry and operations to handle grids.
  9. * It is used as complement of the Sfc4q class.
  10. *
  11. * Concepts:
  12. *
  13. * **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.
  14. *
  15. * **box width** and **box height**: the unit square is projected to a real "box", used as canvas of the grid.
  16. *
  17. * **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.
  18. *
  19. * **(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)
  20. * <br/>“i” and “j” are integers in the range [0,2^blevel-1]. IJ=(0,0) in the top-left corner.
  21. * <br/>“i” scan columns from left to right, “j” scan lines from top to bottom.
  22. *
  23. * Concepts implemented by the Sfc4q class: *level*, *blevel*, *key*, *bkey*, *IJ* coordinates, (i,j).
  24. *
  25. */
  26. class GridOfCurve {
  27. constructor(sfc4, box_width, box_height=null) {
  28. if (sfc4!==undefined)
  29. this.refresh(sfc4,box_width,box_height)
  30. else console.log("ERROR: GridOfCurve(null) is perhaps invalid")
  31. }
  32. refresh(sfc4, box_width, box_height=null) {
  33. // CONFIGS:
  34. if (sfc4) this.sfc4 = sfc4; // else reuse or error
  35. this.box_width = box_width || this.box_width || null; // null for GridOfCurve_D3.buildSvg()
  36. this.box_height = box_height || this.box_height || this.box_width;
  37. this.refreshProperties()
  38. }
  39. refreshProperties(novoLevel=null) {
  40. if (this.sfc4!==null && this.box_width) {
  41. if (novoLevel) this.sfc4.refresh(novoLevel);
  42. if (!this.box_height) this.box_height =this.box_width;
  43. this.needSwap = this.sfc4.needSwap && (this.box_width != this.box_height)
  44. this.cell_refWidth = this.box_width/Number(this.sfc4.nRefRows)
  45. this.cell_refHeight = this.box_height/Number(this.sfc4.nRefRows)
  46. this.cell_area = this.box_width*this.box_height/Number(this.sfc4.nBKeys) // MUST be constant!
  47. // properties of the unit grid, used in (s,t) coordinates:
  48. // usar SizedBigInt.bigint_div():
  49. // this.cell_stWidth = 1n/this.sfc4.nRefRows // and height==width
  50. // this.cell_stArea = 1n/this.sfc4.nBKeys
  51. }
  52. }
  53. /**
  54. * Translates (i,j) coordinates to (x,y) coordinates, using grid properties.
  55. * @param integer i: the grid row coordinate.
  56. * @param integer j: the grid column coordinate.
  57. * @return [float,float].
  58. */
  59. ij_to_xy(i,j,shift=0) { // if rotates, must be checked
  60. if (shift) [i,j] = [i+shift, j+shift]; // cast to float
  61. if (this.needSwap) {
  62. let [x,y] = this.sfc4.ij_swapSides(i,j,this.cell_refWidth,this.cell_refHeight)
  63. return [i*x, j*y]
  64. } else
  65. return [i*this.cell_refWidth, j*this.cell_refHeight]
  66. }
  67. ij_to_xyCenter(i,j) {
  68. return this.ij_to_xy(i,j,0.5)
  69. }
  70. /**
  71. * Translates (x,y) coordinates to (i,j) coordinates, using grid properties.
  72. * @param float x: the spatial X coordinate.
  73. * @param float y: the spatial Y coordinate.
  74. * @return [integer,integer].
  75. */
  76. xy_to_ij(x,y) {
  77. let ref_i = Math.floor(x/this.cell_refWidth),
  78. ref_j = Math.floor(y/this.cell_refHeight)
  79. if (this.needSwap) {
  80. console.log("BUG: needSwap need to develop xy_to_ij()")
  81. // for Hilbert can check S2 geometry, a (s,t) to (i,j) resolution.
  82. // use ij_nSwaps(ref_i,ref_j) as first guess
  83. // scan to obtain number of xSwaps and ySwaps... use the unit s,t before x,y.
  84. } else
  85. return [ref_i,ref_j];
  86. }
  87. } // \GridOfCurve
  88. /**
  89. * D3 grid, a GridOfCurve class extension. D3 is the [D3js.org](https://D3js.org) framework, used to build the grid and its tools.
  90. */
  91. class GridOfCurve_D3 extends GridOfCurve {
  92. constructor(conf, sfc4, layout, box_width, box_height=null, grdID=1, reftab='chartsContainer') {
  93. const toBuildHere=true, toBuildToolTip=true // future configs
  94. if (typeof conf == 'object') {
  95. super(conf.sfc4, conf.box_width, conf.box_height)
  96. this.refresh_D3(conf.domRef_id, conf.grdID, conf.reftab)
  97. layout = conf.layout
  98. } else if (conf!==undefined) {
  99. super(sfc4, box_width, box_height)
  100. this.refresh_D3(conf, grdID, reftab)
  101. }
  102. this.layout = layout || {rects:true, circles:true, labelMain:true, labelIJ:true};
  103. this.distinctColors = {}
  104. this.lblBase='std'
  105. this.lblChk = null
  106. this.D3canvas = null
  107. if (toBuildHere) this.build(true)
  108. if (toBuildToolTip) this.tooltip_build() // conferir se precisa rebuild das propriedades do data
  109. this.build_zoomTool() // need if
  110. }
  111. refresh_D3(domRef_id, grdID, reftab) {
  112. this.domRef_id = domRef_id || null
  113. this.grdID = grdID || null
  114. this.reftabDOM = document.getElementById(reftab);
  115. if (!this.reftabDOM) console.log("ops, need CORRECT 'reftab' for canvas sizes")
  116. // check need for call buildSvg() here
  117. }
  118. buildSvg(firstBuild=true,colwidth=40) {
  119. if (firstBuild || !this.D3_svg) {
  120. if (this.D3_svg)
  121. this.D3_svg.selectAll("*").remove(); // testar
  122. if (!this.box_width) {
  123. let refWidth = parseInt( this.reftabDOM.getBoundingClientRect().width/2.0 );
  124. this.box_width = Math.min(window.innerWidth, refWidth) - colwidth;
  125. this.refreshProperties()
  126. }
  127. if (this.box_width< conf_canvasWidth_min) {
  128. let perc = Math.round( 100*(conf_canvasWidth_min-this.box_width)/conf_canvasWidth_min )
  129. let msg = `This visualization not works with small screens.\nPlease use a screen ${perc}% bigger.`
  130. if (conf_alertLevel>1) alert(msg); else console.log(msg);
  131. }
  132. let theChart = '#'+ this.domRef_id +' svg.theChart';
  133. this.D3_svg0 = d3.select(theChart).attr("width", this.box_width+10).attr("height", this.box_width+10)
  134. this.D3canvas = this.D3_svg0
  135. this.D3_svg = this.D3_svg0.append("g"); // set reference D3 for all builds
  136. this.D3_svg.attr("class", "the_grid" + (this.grdID? ` grdID${this.grdID}`: '') );
  137. } else
  138. this.D3_svg.selectAll("*").remove(); // remove elements from svg/g, preserve SVG attributes and catchall
  139. } // \buildSvg
  140. bitsToColors(bs,len) { // enhancing prefixes
  141. if (typeof bs != 'string') return null; // ensure
  142. if (bs.length<len) bs = bs.padStart(len,'0')
  143. let d;
  144. if (len%2) { bs+='0'; len++; } // more one bit if not even
  145. if (len<6) {
  146. let R = '1'+bs.slice(0,1) + bs.slice(0,2).padEnd(2,'1')
  147. let G = '1'+bs.slice(3,4) + bs.slice(3,5).padEnd(2,'1')
  148. let B = (len>2? bs.slice(-2): '11')
  149. d=[ R, G, '1'+B+B.slice(0,1) ] // binary RGB color
  150. } else if (len==12)
  151. return '#'+parseInt(bs,2).toString(16).padStart(3,'0')
  152. else {
  153. let R = bs.slice(0,4)
  154. let G = bs.slice(1,2)+bs.slice(3,len<13?5:6).padEnd(3,'1')
  155. let B = bs.slice(-4) // (bs.slice(0,1)=='0')? ('1'+bs.slice(-3)): bs.slice(-4)
  156. d=[R, G, B ] // binary RGB color
  157. }
  158. let dHex = d.map(x => parseInt(x,2).toString(16) ).join('')
  159. return '#'+dHex;
  160. }
  161. dataBuild(stopOn=0,xpos=1,ypos=1,useDstClrs=true) {
  162. if (!stopOn) this.distinctColors = {}
  163. let nBKeys = Number(this.sfc4.nBKeys) // revisar!
  164. let nBKeysFrac = Math.round(nBKeys/150)
  165. if (this.sfc4.isHalf) nBKeys = nBKeys/2;
  166. let rw0 = this.cell_refWidth
  167. let rh0 = this.cell_refHeight
  168. var r = [].fill(null,0,nBKeys-1) // será revisto e oTheFly!
  169. var mySfc = this.sfc4
  170. const lblBase = this.lblBase
  171. const lblIs32 = (lblBase=='32' && !(this.sfc4.level%2.5))
  172. const lblIs16 = (this.sfc4.level>2)
  173. const lblIsDec = (lblBase=='dec')
  174. this.lblChk = [
  175. lblIsDec,lblIs32,lblIs16, // using check-order
  176. lblIsDec? "decimal": lblIs32? "base 32": lblIs16? "base 16h": "base 4h"
  177. ];
  178. if (stopOn===true) stopOn = Math.round(nBKeys/3) // 2*Math.sqrt(nBKeys)
  179. let maxIdLoop = (stopOn && nBKeys>4)? stopOn: nBKeys;
  180. for(let id=0; id<maxIdLoop; id++) {
  181. let [ij0,ij1] = this.sfc4.key_decode(id)
  182. let xy = this.ij_to_xy( ij0[0], ij0[1] ) //xy0
  183. let rw = rw0, rh = rh0;
  184. let idx = mySfc.setKey(id);
  185. let colorCode = this.bitsToColors( idx.id_toString('2'), idx.keyBits ) // toBitString
  186. let id4 = idx.id_toString('4h'), id16 = idx.id_toString('16h'),
  187. id32 = idx.id_toString('32nvu')
  188. if (this.sfc4.isHalf) {
  189. let xy1 = this.ij_to_xy( ij1[0], ij1[1] )
  190. if (Math.abs(xy[0]-xy1[0]) > 0) rw = rw*2; // revisar
  191. if (Math.abs(xy[1]-xy1[1]) > 0) rh = rh*2;
  192. xy[0] = Math.min(xy[0],xy1[0]);
  193. xy[1] = Math.min(xy[1],xy1[1]);
  194. }
  195. let idPub = lblIsDec? id: lblIs32? id32: lblIs16? id16: id4;
  196. if (!stopOn && useDstClrs && (nBKeys<150 || (id%nBKeysFrac)==1) )
  197. this.distinctColors[colorCode] = idPub;
  198. r[id] = { id:id, idPub:idPub, i:ij0[0], j:ij0[1], x:xy[0], y:xy[1], width:rw, height:rh, color:colorCode };
  199. }
  200. return r;
  201. }
  202. build(firstBuild=true, line_width=2) { // draw grid!
  203. if (conf_alertLevel>1) console.log("debug build:",this.sfc4.curveName)
  204. this.buildSvg(firstBuild);
  205. var myThis = this
  206. let rw = this.cell_refWidth
  207. var mySfc = this.sfc4
  208. let nBKeys = Number(mySfc.nBKeys)
  209. let bits = (mySfc.nBKeys-1n).toString(2).length
  210. var sbi = new SizedBigInt(); // this.sfc4.setID_byKey(3):
  211. let D3DataEnter = this.D3_svg.selectAll()
  212. .data( this.dataBuild() )
  213. .enter()
  214. if (this.layout.rects && rw>15) // // // Red rectangular grid:
  215. D3DataEnter.append("rect")
  216. .attr("x", d => d.x ).attr("y", d => d.y )
  217. .attr("width",d => d.width).attr("height",d => {return d.height})
  218. .style("fill","#FFF").style("stroke","#F00");
  219. let fracTime = ((nBKeys>1000)? 9000: (nBKeys>200)? 3800: 2800)/nBKeys;
  220. //let fracFirst = nBKeys/((nBKeys>1000)? 100: (nBKeys>10)? 10: 2);
  221. if (this.layout.circles) // // // Colured centroid and point-grid circles:
  222. D3DataEnter.append("circle")
  223. .attr('cx', d => d.x + d.width/2)
  224. .attr('cy', d => d.y + d.height/2)
  225. .attr("r", rw/3) // (rw<20)? 5:10
  226. .style("fill", d=> d.color )
  227. .call( (s,lev) => {if (lev<4) s.style("stroke", "#F00")}, mySfc.level)
  228. // animation:
  229. .style("opacity", 0).transition()
  230. .duration(0)
  231. .delay( (_, i) => i*fracTime + (i?4:0)*fracTime/Math.log2(2+i) )
  232. .style("opacity", 1);
  233. if (this.layout.labelMain && rw>20) { // cell ID label text
  234. let test = this.dataBuild(true);
  235. if (this.sfc4.nBKeys>4n && test.length>1) this.D3_svg.selectAll()
  236. .data( test )
  237. .enter().append("text")
  238. .attr("x", d => d.x+rw/2.1 -1 )
  239. .attr("y", d => d.y+rw/1.7 +1)
  240. .text( d => d.idPub )
  241. .style("font-size", (rw<40)? "12pt":"14pt")
  242. .attr("fill","#FFF").style("style","label");
  243. D3DataEnter.append("text")
  244. .attr("x", d => d.x+rw/2.1 )
  245. .attr("y", d => d.y+rw/1.7 )
  246. .text( d => d.idPub )
  247. .style("font-size", (rw<40)? "10pt":"12pt").style("style","label")
  248. /* .call( (s,cond) => {
  249. if (cond) s.attr("transform", "translate(2,2) rotate(90)")
  250. console.log(cond)
  251. }, mySfc.isHalf && d.width<d.height);
  252. */
  253. }
  254. if (this.layout.labelIJ && rw>35) // (i,j) label text
  255. D3DataEnter.append("text")
  256. .attr("x", d => d.x+0.3 + (mySfc.isHalf? 0.8: 0))
  257. .attr("y", d => d.y+10 )
  258. .text( d => `${d.i},${d.j}` )
  259. .style("font-size", "8pt").attr("fill","#A44").style("style","label");
  260. } // \build
  261. buildCaption(svgSelect='#rainbow svg') {
  262. const myC = this.distinctColors,
  263. wd = 10,
  264. wdExtra = wd+30,
  265. yShift = 15
  266. var colors = Object.keys(this.distinctColors)
  267. var myC_len = colors.length
  268. var myC_lenFrac = Math.round(myC_len/12)
  269. var h = this.box_width/(myC_len+1)
  270. colors.sort()
  271. let rb = d3.select(svgSelect)
  272. rb.selectAll("*").remove();
  273. rb.attr("width", wdExtra)
  274. .attr("height", this.box_width+20);
  275. rb.selectAll().data( colors ).enter().append("rect")
  276. .attr("x", 0 )
  277. .attr("y", (d,i) => i*h + yShift)
  278. .attr("width",wd).attr("height",h)
  279. .style("fill",d=>d);
  280. rb.selectAll().data( colors ).enter().append("text")
  281. .attr("x", wd )
  282. .attr("y", (d,i) => h+i*h+yShift )
  283. .text( (d,i) => (myC_len<9 || (i%myC_lenFrac)==0)? myC[d]: '' )
  284. .style("font-size","8pt").attr("fill","#000").style("style","label");
  285. }
  286. tooltip_msg(ij) {
  287. const lck = this.lblChk
  288. if (lck===null) return '';
  289. let id = this.sfc4.sbiID.val // number
  290. let id4 = this.sfc4.id_toString('4h')
  291. let id16 = this.sfc4.id_toString('16h')
  292. let id32 = this.sfc4.id_toString('32nvu')
  293. let idPub = lck[0]? id: lck[1]? id32: lck[2]? id16: id4;
  294. let hex = (!lck[0] && !lck[1] && lck[2])?
  295. '': `base 16h: ${adTag(id16)}`;
  296. let dec = lck[0]? '': `decimal: ${adTag(id)}${hex? '<br/>':''}`;
  297. let b4 = (idPub!=id4)? `<br/>base4h: ${adTag(id4)}`: '';
  298. return `${lck[3]}: ${adTag(idPub)}<hr/> ${dec}${hex}${b4}<br/>(<i>i,j</i>)=(${ij[0]},${ij[1]})`;
  299. }
  300. tooltip_build() {
  301. const domRef = d3.select('#'+ this.domRef_id);
  302. const tpNode = domRef.select('div.theChartTooltip');
  303. var mySVG = this.D3_svg;
  304. var myThis = this;
  305. var mySfc = this.sfc4 //const
  306. var lastCellPos = [null,null];
  307. this.D3canvas // BUG on catchall!
  308. .on('mouseover', function() {
  309. tpNode.style("display", "inline");
  310. })
  311. .on('mouseout', function() {
  312. tpNode.style("display", "none");
  313. // must to draw and drop rectangle, not reuse background.
  314. /* revisar class xy, melhor usar Key como seletor, k123, visto que é seletor local
  315. if (lastCellPos[0]!==null) {
  316. mySVG.select(`rect.xy${lastCellPos[0]}-${lastCellPos[1]}`).style("fill", '#FFF');
  317. if (this.sfc4.isHalf) //NAO PRECISA MAIS pois é key
  318. mySVG.select(`rect.xy${lastCellPos[3]}-${lastCellPos[4]}`).style("fill", '#FFF'); // lastCellPos[2]
  319. }
  320. */
  321. lastCellPos = [null,null];
  322. })
  323. .on('mousemove', function () {
  324. // falta pintar um retangulo
  325. var coords = d3.mouse(mySVG.node());
  326. var grd_IJ = myThis.xy_to_ij(coords[0], coords[1]);
  327. if (lastCellPos[0]!=grd_IJ[0] || lastCellPos[1]!=grd_IJ[1]) { // only to reduce CPU costs
  328. mySfc.setBkey_byIJ(grd_IJ)
  329. let msg = myThis.tooltip_msg(grd_IJ)
  330. tpNode.html(msg)
  331. lastCellPos = grd_IJ
  332. }
  333. tpNode.style('left', d3.event.pageX+'px')
  334. .style('top', d3.event.pageY+'px');
  335. })
  336. .append("rect") // the "catch all mouse event" area
  337. .attr("x",0).attr("y",0)
  338. .attr("class", "catchall") // see https://stackoverflow.com/a/16923563/287948 and http://jsfiddle.net/H3W3k/
  339. .attr("width",myThis.box_width).attr("height",myThis.box_width)
  340. .style('visibility', 'hidden')
  341. //.attr("opacity", 0.0).style("fill","#FFF").style("stroke","#F00"); // need only the external line
  342. ; // \D3canvas
  343. } // \Tooltip
  344. build_zoomTool() {
  345. // Canvas zoom/pan
  346. let bxw = this.box_width+10;
  347. let canvas = this.D3canvas
  348. this.D3_svg0.call(d3.zoom()
  349. .translateExtent([ [0,0], [bxw,bxw] ])
  350. .scaleExtent([1, Infinity])
  351. .on("zoom", function() {
  352. if (xzoomNode.value==1)
  353. canvas.attr("transform", d3.event.transform);
  354. })
  355. );
  356. } // build_zoomTool
  357. } // \GridOfCurve_D3
  358. ////////////// non-exported components of this module.
  359. function adTag(s,tag="code") { return `<${tag}>${s}</${tag}>`; }
  360. function baseItens(sbi_id,ij) {
  361. // Here define the presentation order of bases.
  362. let crvID = sbi_id.val // or int
  363. var crvID4 = sbi_id.toString('4h'); // parseInt(crvID).toString(4).padStart(globOrder,'0');
  364. var crvID16 = sbi_id.toString('16h'); // parseInt(crvID).toString(16).padStart(globOrder,'0');
  365. var crvID32 = sbi_id.toString('32nvu'); // parseInt(crvID).toString(32).padStart(globOrder/2,'0');
  366. let msgb32 = (globOrder_exact % 2.5)? '': `<hr/>base32nvu: ${adTag(crvID32)}`;
  367. let msg;
  368. if (globOrder_exact>2) msg = `base16h: ${adTag(crvID16)}${msgb32}<hr/>base4: ${adTag(crvID4)}`;
  369. else msg = `base4h: ${adTag(crvID4)}<hr/>base16h: ${adTag(crvID16)}`;
  370. return msg + `<br/>decimal: ${adTag(crvID)}<br/>(i,j)=(${ij[0]},${ij[1]})`;
  371. }