base/BasePainter.mjs

  1. import { select as d3_select } from '../d3.mjs';
  2. import { settings, internals, isNodeJs, isFunc, isStr, isObject, btoa_func, getDocument } from '../core.mjs';
  3. import { getColor, addColor } from './colors.mjs';
  4. /** @summary Standard prefix for SVG file context as data url
  5. * @private */
  6. const prSVG = 'data:image/svg+xml;charset=utf-8,',
  7. /** @summary Standard prefix for JSON file context as data url
  8. * @private */
  9. prJSON = 'data:application/json;charset=utf-8,';
  10. /** @summary Returns visible rect of element
  11. * @param {object} elem - d3.select object with element
  12. * @param {string} [kind] - which size method is used
  13. * @desc kind = 'bbox' use getBBox, works only with SVG
  14. * kind = 'full' - full size of element, using getBoundingClientRect function
  15. * kind = 'nopadding' - excludes padding area
  16. * With node.js can use 'width' and 'height' attributes when provided in element
  17. * @private */
  18. function getElementRect(elem, sizearg) {
  19. if (!elem || elem.empty())
  20. return { x: 0, y: 0, width: 0, height: 0 };
  21. if ((isNodeJs() && (sizearg !== 'bbox')) || elem.property('_batch_mode'))
  22. return { x: 0, y: 0, width: parseInt(elem.attr('width')), height: parseInt(elem.attr('height')) };
  23. const styleValue = name => {
  24. let value = elem.style(name);
  25. if (!value || !isStr(value)) return 0;
  26. value = parseFloat(value.replace('px', ''));
  27. return !Number.isFinite(value) ? 0 : Math.round(value);
  28. };
  29. let rect = elem.node().getBoundingClientRect();
  30. if ((sizearg === 'bbox') && (parseFloat(rect.width) > 0))
  31. rect = elem.node().getBBox();
  32. const res = { x: 0, y: 0, width: parseInt(rect.width), height: parseInt(rect.height) };
  33. if (rect.left !== undefined) {
  34. res.x = parseInt(rect.left);
  35. res.y = parseInt(rect.top);
  36. } else if (rect.x !== undefined) {
  37. res.x = parseInt(rect.x);
  38. res.y = parseInt(rect.y);
  39. }
  40. if ((sizearg === undefined) || (sizearg === 'nopadding')) {
  41. // this is size exclude padding area
  42. res.width -= styleValue('padding-left') + styleValue('padding-right');
  43. res.height -= styleValue('padding-top') + styleValue('padding-bottom');
  44. }
  45. return res;
  46. }
  47. /** @summary Calculate absolute position of provided element in canvas
  48. * @private */
  49. function getAbsPosInCanvas(sel, pos) {
  50. if (!pos)
  51. return pos;
  52. while (!sel.empty() && !sel.classed('root_canvas')) {
  53. const cl = sel.attr('class');
  54. if (cl && ((cl.indexOf('root_frame') >= 0) || (cl.indexOf('__root_pad_') >= 0))) {
  55. pos.x += sel.property('draw_x') || 0;
  56. pos.y += sel.property('draw_y') || 0;
  57. }
  58. sel = d3_select(sel.node().parentNode);
  59. }
  60. return pos;
  61. }
  62. /** @summary Converts numeric value to string according to specified format.
  63. * @param {number} value - value to convert
  64. * @param {string} [fmt='6.4g'] - format can be like 5.4g or 4.2e or 6.4f
  65. * @param {boolean} [ret_fmt] - when true returns array with value and actual format like ['0.1','6.4f']
  66. * @return {string|Array} - converted value or array with value and actual format
  67. * @private */
  68. function floatToString(value, fmt, ret_fmt) {
  69. if (!fmt)
  70. fmt = '6.4g';
  71. else if (fmt === 'g')
  72. fmt = '7.5g';
  73. fmt = fmt.trim();
  74. const len = fmt.length;
  75. if (len < 2)
  76. return ret_fmt ? [value.toFixed(4), '6.4f'] : value.toFixed(4);
  77. const kind = fmt[len-1].toLowerCase(),
  78. compact = (len > 1) && (fmt[len-2] === 'c') ? 'c' : '';
  79. fmt = fmt.slice(0, len - (compact ? 2 : 1));
  80. if (kind === 'g') {
  81. const se = floatToString(value, fmt+'ce', true),
  82. sg = floatToString(value, fmt+'cf', true),
  83. res = se[0].length < sg[0].length || ((sg[0] === '0') && value) ? se : sg;
  84. return ret_fmt ? res : res[0];
  85. }
  86. let isexp, prec = fmt.indexOf('.');
  87. prec = (prec < 0) ? 4 : parseInt(fmt.slice(prec+1));
  88. if (!Number.isInteger(prec) || (prec <= 0))
  89. prec = 4;
  90. switch (kind) {
  91. case 'e':
  92. isexp = true;
  93. break;
  94. case 'f':
  95. isexp = false;
  96. break;
  97. default:
  98. isexp = false;
  99. prec = 4;
  100. }
  101. if (isexp) {
  102. let se = value.toExponential(prec);
  103. if (compact) {
  104. const pnt = se.indexOf('.'),
  105. pe = se.toLowerCase().indexOf('e');
  106. if ((pnt > 0) && (pe > pnt)) {
  107. let p = pe;
  108. while ((p > pnt) && (se[p-1] === '0'))
  109. p--;
  110. if (p === pnt + 1)
  111. p--;
  112. if (p !== pe)
  113. se = se.slice(0, p) + se.slice(pe);
  114. }
  115. }
  116. return ret_fmt ? [se, `${prec+2}.${prec}${compact}e`] : se;
  117. }
  118. let sg = value.toFixed(prec);
  119. if (compact) {
  120. let l = 0;
  121. while ((l < sg.length) && (sg[l] === '0' || sg[l] === '-' || sg[l] === '.'))
  122. l++;
  123. let diff = sg.length - l - prec;
  124. if (sg.indexOf('.') > l) diff--;
  125. if (diff) {
  126. prec -= diff;
  127. if (prec < 0)
  128. prec = 0;
  129. else if (prec > 20)
  130. prec = 20;
  131. sg = value.toFixed(prec);
  132. }
  133. const pnt = sg.indexOf('.');
  134. if (pnt > 0) {
  135. let p = sg.length;
  136. while ((p > pnt) && (sg[p-1] === '0'))
  137. p--;
  138. if (p === pnt + 1)
  139. p--;
  140. sg = sg.slice(0, p);
  141. }
  142. if (sg === '-0')
  143. sg = '0';
  144. }
  145. return ret_fmt ? [sg, `${prec+2}.${prec}${compact}f`] : sg;
  146. }
  147. /** @summary Draw options interpreter
  148. * @private */
  149. class DrawOptions {
  150. constructor(opt) {
  151. this.opt = isStr(opt) ? opt.toUpperCase().trim() : '';
  152. this.part = '';
  153. }
  154. /** @summary Returns true if remaining options are empty or contain only separators symbols. */
  155. empty() { return !this.opt ? true : !this.opt.replace(/[ ;_,]/g, ''); }
  156. /** @summary Returns remaining part of the draw options. */
  157. remain() { return this.opt; }
  158. /** @summary Checks if given option exists */
  159. check(name, postpart) {
  160. const pos = this.opt.indexOf(name);
  161. if (pos < 0)
  162. return false;
  163. this.opt = this.opt.slice(0, pos) + this.opt.slice(pos + name.length);
  164. this.part = '';
  165. if (!postpart)
  166. return true;
  167. let pos2 = pos;
  168. const is_array = postpart === 'array';
  169. if (is_array) {
  170. if (this.opt[pos2] !== '[')
  171. return false;
  172. while ((pos2 < this.opt.length) && (this.opt[pos2] !== ']'))
  173. pos2++;
  174. if (++pos2 > this.opt.length)
  175. return false;
  176. } else {
  177. while ((pos2 < this.opt.length) && (this.opt[pos2] !== ' ') && (this.opt[pos2] !== ',') && (this.opt[pos2] !== ';'))
  178. pos2++;
  179. }
  180. if (pos2 > pos) {
  181. this.part = this.opt.slice(pos, pos2);
  182. this.opt = this.opt.slice(0, pos) + this.opt.slice(pos2);
  183. }
  184. if (is_array) {
  185. try {
  186. this.array = JSON.parse(this.part);
  187. } catch {
  188. this.array = undefined;
  189. }
  190. return this.array?.length !== undefined;
  191. }
  192. if (postpart !== 'color')
  193. return true;
  194. if (((this.part.length === 6) || (this.part.length === 8)) && this.part.match(/^[a-fA-F0-9]+/)) {
  195. this.color = addColor('#' + this.part);
  196. return true;
  197. }
  198. this.color = this.partAsInt(1) - 1;
  199. if (this.color >= 0)
  200. return true;
  201. for (let col = 0; col < 8; ++col) {
  202. if (getColor(col).toUpperCase() === this.part) {
  203. this.color = col;
  204. return true;
  205. }
  206. }
  207. return false;
  208. }
  209. /** @summary Returns remaining part of found option as integer. */
  210. partAsInt(offset, dflt) {
  211. let mult = 1;
  212. const last = this.part ? this.part.at(-1) : '';
  213. if (last === 'K')
  214. mult = 1e3;
  215. else if (last === 'M')
  216. mult = 1e6;
  217. else if (last === 'G')
  218. mult = 1e9;
  219. let val = this.part.replace(/^\D+/g, '');
  220. val = val ? parseInt(val, 10) : Number.NaN;
  221. return !Number.isInteger(val) ? (dflt || 0) : mult*val + (offset || 0);
  222. }
  223. /** @summary Returns remaining part of found option as float. */
  224. partAsFloat(offset, dflt) {
  225. let val = this.part.replace(/^\D+/g, '');
  226. val = val ? parseFloat(val) : Number.NaN;
  227. return !Number.isFinite(val) ? (dflt || 0) : val + (offset || 0);
  228. }
  229. } // class DrawOptions
  230. /** @summary Simple random generator with controlled seed
  231. * @private */
  232. class TRandom {
  233. constructor(i) {
  234. if (i !== undefined) this.seed(i);
  235. }
  236. /** @summary Seed simple random generator */
  237. seed(i) {
  238. i = Math.abs(i);
  239. if (i > 1e8)
  240. i = Math.abs(1e8 * Math.sin(i));
  241. else if (i < 1)
  242. i *= 1e8;
  243. this.m_w = Math.round(i);
  244. this.m_z = 987654321;
  245. }
  246. /** @summary Produce random value between 0 and 1 */
  247. random() {
  248. if (this.m_z === undefined) return Math.random();
  249. this.m_z = (36969 * (this.m_z & 65535) + (this.m_z >> 16)) & 0xffffffff;
  250. this.m_w = (18000 * (this.m_w & 65535) + (this.m_w >> 16)) & 0xffffffff;
  251. let result = ((this.m_z << 16) + this.m_w) & 0xffffffff;
  252. result /= 4294967296;
  253. return result + 0.5;
  254. }
  255. } // class TRandom
  256. /** @summary Build smooth SVG curve using Bezier
  257. * @desc Reuse code from https://stackoverflow.com/questions/62855310
  258. * @private */
  259. function buildSvgCurve(p, args) {
  260. if (!args)
  261. args = {};
  262. if (!args.line)
  263. args.calc = true;
  264. else if (args.ndig === undefined)
  265. args.ndig = 0;
  266. let npnts = p.length;
  267. if (npnts < 3) args.line = true;
  268. args.t = args.t ?? 0.2;
  269. if ((args.ndig === undefined) || args.height) {
  270. args.maxy = p[0].gry;
  271. args.mindiff = 100;
  272. for (let i = 1; i < npnts; i++) {
  273. args.maxy = Math.max(args.maxy, p[i].gry);
  274. args.mindiff = Math.min(args.mindiff, Math.abs(p[i].grx - p[i-1].grx), Math.abs(p[i].gry - p[i-1].gry));
  275. }
  276. if (args.ndig === undefined)
  277. args.ndig = args.mindiff > 20 ? 0 : (args.mindiff > 5 ? 1 : 2);
  278. }
  279. const end_point = (pnt1, pnt2, sign) => {
  280. const len = Math.sqrt((pnt2.gry - pnt1.gry)**2 + (pnt2.grx - pnt1.grx)**2) * args.t,
  281. a2 = Math.atan2(pnt2.dgry, pnt2.dgrx),
  282. a1 = Math.atan2(sign*(pnt2.gry - pnt1.gry), sign*(pnt2.grx - pnt1.grx));
  283. pnt1.dgrx = len * Math.cos(2*a1 - a2);
  284. pnt1.dgry = len * Math.sin(2*a1 - a2);
  285. }, conv = val => {
  286. if (!args.ndig || (Math.round(val) === val))
  287. return val.toFixed(0);
  288. let s = val.toFixed(args.ndig), p1 = s.length - 1;
  289. while (s[p1] === '0') p1--;
  290. if (s[p1] === '.') p1--;
  291. s = s.slice(0, p1+1);
  292. return (s === '-0') ? '0' : s;
  293. };
  294. if (args.calc) {
  295. for (let i = 1; i < npnts - 1; i++) {
  296. p[i].dgrx = (p[i+1].grx - p[i-1].grx) * args.t;
  297. p[i].dgry = (p[i+1].gry - p[i-1].gry) * args.t;
  298. }
  299. if (npnts > 2) {
  300. end_point(p[0], p[1], 1);
  301. end_point(p[npnts - 1], p[npnts - 2], -1);
  302. } else if (p.length === 2) {
  303. p[0].dgrx = (p[1].grx - p[0].grx) * args.t;
  304. p[0].dgry = (p[1].gry - p[0].gry) * args.t;
  305. p[1].dgrx = -p[0].dgrx;
  306. p[1].dgry = -p[0].dgry;
  307. }
  308. }
  309. let path = `${args.cmd ?? 'M'}${conv(p[0].grx)},${conv(p[0].gry)}`;
  310. if (!args.line) {
  311. let i0 = 1;
  312. if (args.qubic) {
  313. npnts--; i0++;
  314. path += `Q${conv(p[1].grx-p[1].dgrx)},${conv(p[1].gry-p[1].dgry)},${conv(p[1].grx)},${conv(p[1].gry)}`;
  315. }
  316. path += `C${conv(p[i0-1].grx+p[i0-1].dgrx)},${conv(p[i0-1].gry+p[i0-1].dgry)},${conv(p[i0].grx-p[i0].dgrx)},${conv(p[i0].gry-p[i0].dgry)},${conv(p[i0].grx)},${conv(p[i0].gry)}`;
  317. // continue with simpler points
  318. for (let i = i0 + 1; i < npnts; i++)
  319. path += `S${conv(p[i].grx-p[i].dgrx)},${conv(p[i].gry-p[i].dgry)},${conv(p[i].grx)},${conv(p[i].gry)}`;
  320. if (args.qubic)
  321. path += `Q${conv(p[npnts].grx-p[npnts].dgrx)},${conv(p[npnts].gry-p[npnts].dgry)},${conv(p[npnts].grx)},${conv(p[npnts].gry)}`;
  322. } else if (npnts < 10000) {
  323. // build simple curve
  324. let acc_x = 0, acc_y = 0, currx = Math.round(p[0].grx), curry = Math.round(p[0].gry);
  325. const flush = () => {
  326. if (acc_x) { path += 'h' + acc_x; acc_x = 0; }
  327. if (acc_y) { path += 'v' + acc_y; acc_y = 0; }
  328. };
  329. for (let n = 1; n < npnts; ++n) {
  330. const bin = p[n],
  331. dx = Math.round(bin.grx) - currx,
  332. dy = Math.round(bin.gry) - curry;
  333. if (dx && dy) {
  334. flush();
  335. path += `l${dx},${dy}`;
  336. } else if (!dx && dy) {
  337. if ((acc_y === 0) || ((dy < 0) !== (acc_y < 0))) flush();
  338. acc_y += dy;
  339. } else if (dx && !dy) {
  340. if ((acc_x === 0) || ((dx < 0) !== (acc_x < 0))) flush();
  341. acc_x += dx;
  342. }
  343. currx += dx; curry += dy;
  344. }
  345. flush();
  346. } else {
  347. // build line with trying optimize many vertical moves
  348. let currx = Math.round(p[0].grx), curry = Math.round(p[0].gry),
  349. cminy = curry, cmaxy = curry, prevy = curry;
  350. for (let n = 1; n < npnts; ++n) {
  351. const bin = p[n],
  352. lastx = Math.round(bin.grx),
  353. lasty = Math.round(bin.gry),
  354. dx = lastx - currx;
  355. if (dx === 0) {
  356. // if X not change, just remember amplitude and
  357. cminy = Math.min(cminy, lasty);
  358. cmaxy = Math.max(cmaxy, lasty);
  359. prevy = lasty;
  360. continue;
  361. }
  362. if (cminy !== cmaxy) {
  363. if (cminy !== curry)
  364. path += `v${cminy-curry}`;
  365. path += `v${cmaxy-cminy}`;
  366. if (cmaxy !== prevy)
  367. path += `v${prevy-cmaxy}`;
  368. curry = prevy;
  369. }
  370. const dy = lasty - curry;
  371. if (dy)
  372. path += `l${dx},${dy}`;
  373. else
  374. path += `h${dx}`;
  375. currx = lastx; curry = lasty;
  376. prevy = cminy = cmaxy = lasty;
  377. }
  378. if (cminy !== cmaxy) {
  379. if (cminy !== curry)
  380. path += `v${cminy-curry}`;
  381. path += `v${cmaxy-cminy}`;
  382. if (cmaxy !== prevy)
  383. path += `v${prevy-cmaxy}`;
  384. }
  385. }
  386. if (args.height)
  387. args.close = `L${conv(p.at(-1).grx)},${conv(Math.max(args.maxy, args.height))}H${conv(p[0].grx)}Z`;
  388. return path;
  389. }
  390. /** @summary Compress SVG code, produced from drawing
  391. * @desc removes extra info or empty elements
  392. * @private */
  393. function compressSVG(svg) {
  394. svg = svg.replace(/url\(&quot;#(\w+)&quot;\)/g, 'url(#$1)') // decode all URL
  395. .replace(/ class="\w*"/g, '') // remove all classes
  396. .replace(/ pad="\w*"/g, '') // remove all pad ids
  397. .replace(/ title=""/g, '') // remove all empty titles
  398. .replace(/ style=""/g, '') // remove all empty styles
  399. .replace(/<g objname="\w*" objtype="\w*"/g, '<g') // remove object ids
  400. .replace(/<g transform="translate\([0-9,]+\)"><\/g>/g, '') // remove all empty groups with transform
  401. .replace(/<g transform="translate\([0-9,]+\)" style="display: none;"><\/g>/g, '') // remove hidden title
  402. .replace(/<g><\/g>/g, ''); // remove all empty groups
  403. // remove all empty frame svg, typically appears in 3D drawings, maybe should be improved in frame painter itself
  404. svg = svg.replace(/<svg x="0" y="0" overflow="hidden" width="\d+" height="\d+" viewBox="0 0 \d+ \d+"><\/svg>/g, '');
  405. return svg;
  406. }
  407. /**
  408. * @summary Base painter class
  409. *
  410. */
  411. class BasePainter {
  412. #divid; // either id of DOM element or element itself
  413. #selected_main; // d3.select for dom elements
  414. #hitemname; // item name in the hpainter
  415. #hdrawopt; // draw option in the hpainter
  416. #hpainter; // assigned hpainter
  417. /** @summary constructor
  418. * @param {object|string} [dom] - dom element or id of dom element */
  419. constructor(dom) {
  420. this.#divid = null; // either id of DOM element or element itself
  421. if (dom) this.setDom(dom);
  422. }
  423. /** @summary Assign painter to specified DOM element
  424. * @param {string|object} elem - element ID or DOM Element
  425. * @desc Normally DOM element should be already assigned in constructor
  426. * @protected */
  427. setDom(elem) {
  428. if (elem !== undefined) {
  429. this.#divid = elem;
  430. this.#selected_main = null;
  431. }
  432. }
  433. /** @summary Returns assigned dom element */
  434. getDom() { return this.#divid; }
  435. /** @summary Selects main HTML element assigned for drawing
  436. * @desc if main element was layout, returns main element inside layout
  437. * @param {string} [is_direct] - if 'origin' specified, returns original element even if actual drawing moved to some other place
  438. * @return {object} d3.select object for main element for drawing */
  439. selectDom(is_direct) {
  440. if (!this.#divid)
  441. return d3_select(null);
  442. let res = this.#selected_main;
  443. if (!res) {
  444. if (isStr(this.#divid)) {
  445. let id = this.#divid;
  446. if (id[0] !== '#') id = '#' + id;
  447. res = d3_select(id);
  448. if (!res.empty())
  449. this.#divid = res.node();
  450. } else
  451. res = d3_select(this.#divid);
  452. this.#selected_main = res;
  453. }
  454. if (!res || res.empty() || (is_direct === 'origin'))
  455. return res;
  456. const use_enlarge = res.property('use_enlarge'),
  457. layout = res.property('layout') || 'simple',
  458. layout_selector = (layout === 'simple') ? '' : res.property('layout_selector');
  459. if (layout_selector)
  460. res = res.select(layout_selector);
  461. // one could redirect here
  462. if (!is_direct && !res.empty() && use_enlarge)
  463. res = d3_select(getDocument().getElementById('jsroot_enlarge_div'));
  464. return res;
  465. }
  466. /** @summary Access/change top painter
  467. * @private */
  468. #accessTopPainter(on) {
  469. const chld = this.selectDom().node()?.firstChild;
  470. if (!chld) return null;
  471. if (on === true)
  472. chld.painter = this;
  473. else if (on === false)
  474. delete chld.painter;
  475. return chld.painter;
  476. }
  477. /** @summary Set painter, stored in first child element
  478. * @desc Only make sense after first drawing is completed and any child element add to configured DOM
  479. * @protected */
  480. setTopPainter() { this.#accessTopPainter(true); }
  481. /** @summary Return top painter set for the selected dom element
  482. * @protected */
  483. getTopPainter() { return this.#accessTopPainter(); }
  484. /** @summary Clear reference on top painter
  485. * @protected */
  486. clearTopPainter() { this.#accessTopPainter(false); }
  487. /** @summary Generic method to cleanup painter
  488. * @desc Removes all visible elements and all internal data */
  489. cleanup(keep_origin) {
  490. this.clearTopPainter();
  491. const origin = this.selectDom('origin');
  492. if (!origin.empty() && !keep_origin) origin.html('');
  493. this.#divid = null;
  494. this.#selected_main = undefined;
  495. if (isFunc(this.#hpainter?.removePainter))
  496. this.#hpainter.removePainter(this);
  497. this.#hitemname = undefined;
  498. this.#hdrawopt = undefined;
  499. this.#hpainter = undefined;
  500. }
  501. /** @summary Checks if draw elements were resized and drawing should be updated
  502. * @return {boolean} true if resize was detected
  503. * @protected
  504. * @abstract */
  505. checkResize(/* arg */) {}
  506. /** @summary Function checks if geometry of main div was changed.
  507. * @desc take into account enlarge state, used only in PadPainter class
  508. * @return size of area when main div is drawn
  509. * @private */
  510. testMainResize(check_level, new_size, height_factor) {
  511. const enlarge = this.enlargeMain('state'),
  512. origin = this.selectDom('origin'),
  513. main = this.selectDom(),
  514. lmt = 5; // minimal size
  515. if ((enlarge !== 'on') && new_size?.width && new_size?.height) {
  516. origin.style('width', new_size.width + 'px')
  517. .style('height', new_size.height + 'px');
  518. }
  519. const rect_origin = getElementRect(origin, true),
  520. can_resize = origin.attr('can_resize');
  521. let do_resize = false;
  522. if (can_resize === 'height')
  523. if (height_factor && Math.abs(rect_origin.width * height_factor - rect_origin.height) > 0.1 * rect_origin.width) do_resize = true;
  524. if (((rect_origin.height <= lmt) || (rect_origin.width <= lmt)) &&
  525. can_resize && can_resize !== 'false') do_resize = true;
  526. if (do_resize && (enlarge !== 'on')) {
  527. // if zero size and can_resize attribute set, change container size
  528. if (rect_origin.width > lmt) {
  529. height_factor = height_factor || 0.66;
  530. origin.style('height', Math.round(rect_origin.width * height_factor) + 'px');
  531. } else if (can_resize !== 'height')
  532. origin.style('width', '200px').style('height', '100px');
  533. }
  534. const rect = getElementRect(main),
  535. old_h = main.property('_jsroot_height'),
  536. old_w = main.property('_jsroot_width');
  537. rect.changed = false;
  538. if (old_h && old_w && (old_h > 0) && (old_w > 0)) {
  539. if ((old_h !== rect.height) || (old_w !== rect.width))
  540. rect.changed = (check_level > 1) || (rect.width / old_w < 0.99) || (rect.width / old_w > 1.01) || (rect.height / old_h < 0.99) || (rect.height / old_h > 1.01);
  541. } else
  542. rect.changed = true;
  543. if (rect.changed)
  544. main.property('_jsroot_height', rect.height).property('_jsroot_width', rect.width);
  545. // after change enlarge state always mark main element as resized
  546. if (origin.property('did_enlarge')) {
  547. rect.changed = true;
  548. origin.property('did_enlarge', false);
  549. }
  550. return rect;
  551. }
  552. /** @summary Try enlarge main drawing element to full HTML page.
  553. * @param {string|boolean} action - defines that should be done
  554. * @desc Possible values for action parameter:
  555. * - true - try to enlarge
  556. * - false - revert enlarge state
  557. * - 'toggle' - toggle enlarge state
  558. * - 'state' - only returns current enlarge state
  559. * - 'verify' - check if element can be enlarged
  560. * if action not specified, just return possibility to enlarge main div
  561. * @protected */
  562. enlargeMain(action, skip_warning) {
  563. const main = this.selectDom(true),
  564. origin = this.selectDom('origin'),
  565. doc = getDocument();
  566. if (main.empty() || !settings.CanEnlarge || (origin.property('can_enlarge') === false)) return false;
  567. if ((action === undefined) || (action === 'verify')) return true;
  568. const state = origin.property('use_enlarge') ? 'on' : 'off';
  569. if (action === 'state') return state;
  570. if (action === 'toggle') action = (state === 'off');
  571. let enlarge = d3_select(doc.getElementById('jsroot_enlarge_div'));
  572. if ((action === true) && (state !== 'on')) {
  573. if (!enlarge.empty()) return false;
  574. enlarge = d3_select(doc.body)
  575. .append('div')
  576. .attr('id', 'jsroot_enlarge_div')
  577. .attr('style', 'position: fixed; margin: 0px; border: 0px; padding: 0px; left: 1px; top: 1px; bottom: 1px; right: 1px; background: white; opacity: 0.95; z-index: 100; overflow: hidden;');
  578. const rect1 = getElementRect(main),
  579. rect2 = getElementRect(enlarge);
  580. // if new enlarge area not big enough, do not do it
  581. if ((rect2.width <= rect1.width) || (rect2.height <= rect1.height)) {
  582. if (rect2.width * rect2.height < rect1.width * rect1.height) {
  583. if (!skip_warning)
  584. console.log(`Enlarged area ${rect2.width} x ${rect2.height} smaller then original drawing ${rect1.width} x ${rect1.height}`);
  585. enlarge.remove();
  586. return false;
  587. }
  588. }
  589. while (main.node().childNodes.length)
  590. enlarge.node().appendChild(main.node().firstChild);
  591. origin.property('use_enlarge', true);
  592. origin.property('did_enlarge', true);
  593. return true;
  594. }
  595. if ((action === false) && (state !== 'off')) {
  596. while (enlarge.node()?.childNodes.length)
  597. main.node().appendChild(enlarge.node().firstChild);
  598. enlarge.remove();
  599. origin.property('use_enlarge', false);
  600. origin.property('did_enlarge', true);
  601. return true;
  602. }
  603. return false;
  604. }
  605. /** @summary Set item name, associated with the painter
  606. * @desc Used by {@link HierarchyPainter}
  607. * @private */
  608. setItemName(name, opt, hpainter) {
  609. this.#hitemname = isStr(name) ? name : undefined;
  610. // only update draw option, never delete.
  611. if (isStr(opt))
  612. this.#hdrawopt = opt;
  613. this.#hpainter = hpainter;
  614. }
  615. /** @summary Returns assigned histogram painter */
  616. getHPainter() { return this.#hpainter; }
  617. /** @summary Returns assigned item name
  618. * @desc Used with {@link HierarchyPainter} to identify drawn item name */
  619. getItemName() { return this.#hitemname ?? null; }
  620. /** @summary Returns assigned item draw option
  621. * @desc Used with {@link HierarchyPainter} to identify drawn item option */
  622. getItemDrawOpt() { return this.#hdrawopt ?? ''; }
  623. } // class BasePainter
  624. /** @summary Load and initialize JSDOM from nodes
  625. * @return {Promise} with d3 selection for d3_body
  626. * @private */
  627. async function _loadJSDOM() {
  628. return import('jsdom').then(handle => {
  629. if (!internals.nodejs_window) {
  630. internals.nodejs_window = (new handle.JSDOM('<!DOCTYPE html>hello')).window;
  631. internals.nodejs_document = internals.nodejs_window.document; // used with three.js
  632. internals.nodejs_body = d3_select(internals.nodejs_document).select('body'); // get d3 handle for body
  633. }
  634. return { JSDOM: handle.JSDOM, doc: internals.nodejs_document, body: internals.nodejs_body };
  635. });
  636. }
  637. /** @summary Return translate string for transform attribute of some svg element
  638. * @return string or null if x and y are zeros
  639. * @private */
  640. function makeTranslate(g, x, y, scale = 1) {
  641. if (!isObject(g)) {
  642. scale = y; y = x; x = g; g = null;
  643. }
  644. let res = y ? `translate(${x},${y})` : (x ? `translate(${x})` : null);
  645. if (scale && scale !== 1) {
  646. if (res) res += ' ';
  647. else res = '';
  648. res += `scale(${scale.toFixed(3)})`;
  649. }
  650. return g ? g.attr('transform', res) : res;
  651. }
  652. /** @summary Configure special style used for highlight or dragging elements
  653. * @private */
  654. function addHighlightStyle(elem, drag) {
  655. if (drag) {
  656. elem.style('stroke', 'steelblue')
  657. .style('fill-opacity', '0.1');
  658. } else {
  659. elem.style('stroke', '#4572A7')
  660. .style('fill', '#4572A7')
  661. .style('opacity', '0');
  662. }
  663. }
  664. /** @summary Create image based on SVG
  665. * @param {string} svg - svg code of the image
  666. * @param {string} [image_format] - image format like 'png', 'jpeg' or 'webp'
  667. * @param {Objects} [args] - optional arguments
  668. * @param {boolean} [args.as_buffer] - return image as buffer
  669. * @return {Promise} with produced image in base64 form or as Buffer (or canvas when no image_format specified)
  670. * @private */
  671. async function svgToImage(svg, image_format, args) {
  672. if ((args === true) || (args === false))
  673. args = { as_buffer: args };
  674. if (image_format === 'svg')
  675. return svg;
  676. if (image_format === 'pdf')
  677. return internals.makePDF ? internals.makePDF(svg, args) : null;
  678. // required with df104.py/df105.py example with RCanvas or any special symbols in TLatex
  679. const doctype = '<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">';
  680. if (isNodeJs()) {
  681. svg = encodeURIComponent(doctype + svg);
  682. svg = svg.replace(/%([0-9A-F]{2})/g, (match, p1) => {
  683. const c = String.fromCharCode('0x'+p1);
  684. return c === '%' ? '%25' : c;
  685. });
  686. const img_src = 'data:image/svg+xml;base64,' + btoa_func(decodeURIComponent(svg));
  687. return import('canvas').then(async handle => {
  688. return handle.default.loadImage(img_src).then(img => {
  689. const canvas = handle.default.createCanvas(img.width, img.height);
  690. canvas.getContext('2d').drawImage(img, 0, 0, img.width, img.height);
  691. if (args?.as_buffer)
  692. return canvas.toBuffer('image/' + image_format);
  693. return image_format ? canvas.toDataURL('image/' + image_format) : canvas;
  694. });
  695. });
  696. }
  697. const img_src = URL.createObjectURL(new Blob([doctype + svg], { type: 'image/svg+xml;charset=utf-8' }));
  698. return new Promise(resolveFunc => {
  699. const image = document.createElement('img');
  700. image.onload = function() {
  701. URL.revokeObjectURL(img_src);
  702. const canvas = document.createElement('canvas');
  703. canvas.width = image.width;
  704. canvas.height = image.height;
  705. canvas.getContext('2d').drawImage(image, 0, 0);
  706. if (args?.as_buffer && image_format)
  707. canvas.toBlob(blob => blob.arrayBuffer().then(resolveFunc), 'image/' + image_format);
  708. else
  709. resolveFunc(image_format ? canvas.toDataURL('image/' + image_format) : canvas);
  710. };
  711. image.onerror = function(arg) {
  712. URL.revokeObjectURL(img_src);
  713. console.log(`IMAGE ERROR ${arg}`);
  714. resolveFunc(null);
  715. };
  716. image.setAttribute('src', img_src);
  717. });
  718. }
  719. /** @summary Convert ROOT TDatime object into Date
  720. * @desc Always use UTC to avoid any variation between timezones */
  721. function getTDatime(dt) {
  722. const y = (dt.fDatime >>> 26) + 1995,
  723. m = ((dt.fDatime << 6) >>> 28) - 1,
  724. d = (dt.fDatime << 10) >>> 27,
  725. h = (dt.fDatime << 15) >>> 27,
  726. min = (dt.fDatime << 20) >>> 26,
  727. s = (dt.fDatime << 26) >>> 26;
  728. return new Date(Date.UTC(y, m, d, h, min, s));
  729. }
  730. /** @summary Convert Date object into string used configured time zone
  731. * @desc Time zone stored in settings.TimeZone */
  732. function convertDate(dt) {
  733. let res = '';
  734. if (settings.TimeZone && isStr(settings.TimeZone)) {
  735. try {
  736. res = dt.toLocaleString('en-GB', { timeZone: settings.TimeZone });
  737. } catch {
  738. res = '';
  739. }
  740. }
  741. return res || dt.toLocaleString('en-GB');
  742. }
  743. /** @summary Box decorations
  744. * @private */
  745. function getBoxDecorations(xx, yy, ww, hh, bmode, pww, phh) {
  746. const side1 = `M${xx},${yy}h${ww}l${-pww},${phh}h${2*pww-ww}v${hh-2*phh}l${-pww},${phh}z`,
  747. side2 = `M${xx+ww},${yy+hh}v${-hh}l${-pww},${phh}v${hh-2*phh}h${2*pww-ww}l${-pww},${phh}z`;
  748. return bmode > 0 ? [side1, side2] : [side2, side1];
  749. }
  750. export { prSVG, prJSON, getElementRect, getAbsPosInCanvas, getTDatime, convertDate,
  751. DrawOptions, TRandom, floatToString, buildSvgCurve, compressSVG, getBoxDecorations,
  752. BasePainter, _loadJSDOM, makeTranslate, addHighlightStyle, svgToImage };