isolines.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638
  1. /* eslint no-console: ["error", { allow: ["log"] }] */
  2. /* eslint-env browser,node */
  3. import {isoLineOptions} from './options.js';
  4. import {cell2Polygons, traceLinePaths} from './polygons.js';
  5. import {QuadTree} from './quadtree.js';
  6. /*
  7. * Compute the iso lines for a scalar 2D field given
  8. * a certain threshold by applying the Marching Squares
  9. * Algorithm. The function returns a list of path coordinates
  10. */
  11. function isoLines(input, threshold, options) {
  12. var settings,
  13. i,
  14. j,
  15. useQuadTree = false,
  16. multiLine = false,
  17. tree = null,
  18. root = null,
  19. data = null,
  20. cellGrid = null,
  21. linePolygons = null,
  22. ret = [];
  23. /* validation */
  24. if (!input) throw new Error('data is required');
  25. if (threshold === undefined || threshold === null) throw new Error('threshold is required');
  26. if ((!!options) && (typeof options !== 'object')) throw new Error('options must be an object');
  27. /* process options */
  28. settings = isoLineOptions(options);
  29. /* check for input data */
  30. if (input instanceof QuadTree) {
  31. tree = input;
  32. root = input.root;
  33. data = input.data;
  34. if (!settings.noQuadTree)
  35. useQuadTree = true;
  36. } else if (Array.isArray(input) && Array.isArray(input[0])) {
  37. data = input;
  38. } else {
  39. throw new Error('input is neither array of arrays nor object retrieved from \'QuadTree()\'');
  40. }
  41. /* check and prepare input threshold(s) */
  42. if (Array.isArray(threshold)) {
  43. multiLine = true;
  44. /* activate QuadTree optimization if not explicitly forbidden by user settings */
  45. if (!settings.noQuadTree)
  46. useQuadTree = true;
  47. /* check if all minV are numbers */
  48. for (i = 0; i < threshold.length; i++)
  49. if (isNaN(+threshold[i]))
  50. throw new Error('threshold[' + i + '] is not a number');
  51. } else {
  52. if (isNaN(+threshold))
  53. throw new Error('threshold must be a number or array of numbers');
  54. threshold = [ threshold ];
  55. }
  56. /* create QuadTree root node if not already present */
  57. if ((useQuadTree) && (!root)) {
  58. tree = new QuadTree(data);
  59. root = tree.root;
  60. data = tree.data;
  61. }
  62. if (settings.verbose) {
  63. if(settings.polygons)
  64. console.log('MarchingSquaresJS-isoLines: returning single lines (polygons) for each grid cell');
  65. else
  66. console.log('MarchingSquaresJS-isoLines: returning line paths (polygons) for entire data grid');
  67. if (multiLine)
  68. console.log('MarchingSquaresJS-isoLines: multiple lines requested, returning array of line paths instead of lines for a single threshold');
  69. }
  70. /* Done with all input validation, now let's start computing stuff */
  71. /* loop over all threhsold values */
  72. threshold.forEach(function(t, i) {
  73. linePolygons = [];
  74. /* store bounds for current computation in settings object */
  75. settings.threshold = t;
  76. if(settings.verbose)
  77. console.log('MarchingSquaresJS-isoLines: computing iso lines for threshold ' + t);
  78. if (settings.polygons) {
  79. /* compose list of polygons for each single cell */
  80. if (useQuadTree) {
  81. /* go through list of cells retrieved from QuadTree */
  82. root
  83. .cellsBelowThreshold(settings.threshold, true)
  84. .forEach(function(c) {
  85. linePolygons = linePolygons.concat(
  86. cell2Polygons(
  87. prepareCell(data,
  88. c.x,
  89. c.y,
  90. settings),
  91. c.x,
  92. c.y,
  93. settings
  94. ));
  95. });
  96. } else {
  97. /* go through entire array of input data */
  98. for (j = 0; j < data.length - 1; ++j) {
  99. for (i = 0; i < data[0].length - 1; ++i)
  100. linePolygons = linePolygons.concat(
  101. cell2Polygons(
  102. prepareCell(data,
  103. i,
  104. j,
  105. settings),
  106. i,
  107. j,
  108. settings
  109. ));
  110. }
  111. }
  112. } else {
  113. /* sparse grid of input data cells */
  114. cellGrid = [];
  115. for (i = 0; i < data[0].length - 1; ++i)
  116. cellGrid[i] = [];
  117. /* compose list of polygons for entire input grid */
  118. if (useQuadTree) {
  119. /* collect the cells */
  120. root
  121. .cellsBelowThreshold(settings.threshold, false)
  122. .forEach(function(c) {
  123. cellGrid[c.x][c.y] = prepareCell(data,
  124. c.x,
  125. c.y,
  126. settings);
  127. });
  128. } else {
  129. /* prepare cells */
  130. for (i = 0; i < data[0].length - 1; ++i) {
  131. for (j = 0; j < data.length - 1; ++j) {
  132. cellGrid[i][j] = prepareCell(data,
  133. i,
  134. j,
  135. settings);
  136. }
  137. }
  138. }
  139. linePolygons = traceLinePaths(data, cellGrid, settings);
  140. }
  141. /* finally, add polygons to output array */
  142. if (multiLine)
  143. ret.push(linePolygons);
  144. else
  145. ret = linePolygons;
  146. if(typeof settings.successCallback === 'function')
  147. settings.successCallback(ret, t);
  148. });
  149. return ret;
  150. }
  151. /*
  152. * Thats all for the public interface, below follows the actual
  153. * implementation
  154. */
  155. /*
  156. * ################################
  157. * Isocontour implementation below
  158. * ################################
  159. */
  160. function prepareCell(grid, x, y, settings) {
  161. var left,
  162. right,
  163. top,
  164. bottom,
  165. average,
  166. cell;
  167. var cval = 0;
  168. var x3 = grid[y + 1][x];
  169. var x2 = grid[y + 1][x + 1];
  170. var x1 = grid[y][x + 1];
  171. var x0 = grid[y][x];
  172. var threshold = settings.threshold;
  173. /*
  174. * Note that missing data within the grid will result
  175. * in horribly failing to trace full polygon paths
  176. */
  177. if(isNaN(x0) || isNaN(x1) || isNaN(x2) || isNaN(x3)) {
  178. return;
  179. }
  180. /*
  181. * Here we detect the type of the cell
  182. *
  183. * x3 ---- x2
  184. * | |
  185. * | |
  186. * x0 ---- x1
  187. *
  188. * with edge points
  189. *
  190. * x0 = (x,y),
  191. * x1 = (x + 1, y),
  192. * x2 = (x + 1, y + 1), and
  193. * x3 = (x, y + 1)
  194. *
  195. * and compute the polygon intersections with the edges
  196. * of the cell. Each edge value may be (i) smaller, or (ii)
  197. * greater or equal to the iso line threshold. We encode
  198. * this property using 1 bit of information, where
  199. *
  200. * 0 ... below,
  201. * 1 ... above or equal
  202. *
  203. * Then we store the cells value as vector
  204. *
  205. * cval = (x0, x1, x2, x3)
  206. *
  207. * where x0 is the least significant bit (0th),
  208. * x1 the 2nd bit, and so on. This essentially
  209. * enables us to work with a single integer number
  210. */
  211. cval |= ((x3 >= threshold) ? 8 : 0);
  212. cval |= ((x2 >= threshold) ? 4 : 0);
  213. cval |= ((x1 >= threshold) ? 2 : 0);
  214. cval |= ((x0 >= threshold) ? 1 : 0);
  215. /* make sure cval is a number */
  216. cval = +cval;
  217. /* compose the cell object */
  218. cell = {
  219. cval: cval,
  220. polygons: [],
  221. edges: {},
  222. x0: x0,
  223. x1: x1,
  224. x2: x2,
  225. x3: x3
  226. };
  227. /*
  228. * Compute interpolated intersections of the polygon(s)
  229. * with the cell borders and (i) add edges for polygon
  230. * trace-back, or (ii) a list of small closed polygons
  231. */
  232. switch (cval) {
  233. case 0:
  234. if (settings.polygons)
  235. cell.polygons.push([ [0, 0], [0, 1], [1, 1], [1, 0] ]);
  236. break;
  237. case 15:
  238. /* cell is outside (above) threshold, no polygons */
  239. break;
  240. case 14: /* 1110 */
  241. left = settings.interpolate(x0, x3, threshold);
  242. bottom = settings.interpolate(x0, x1, threshold);
  243. if (settings.polygons_full) {
  244. cell.edges.left = {
  245. path: [ [0, left], [bottom, 0] ],
  246. move: {
  247. x: 0,
  248. y: -1,
  249. enter: 'top'
  250. }
  251. };
  252. }
  253. if (settings.polygons)
  254. cell.polygons.push([ [0, 0], [0, left], [bottom, 0] ]);
  255. break;
  256. case 13: /* 1101 */
  257. bottom = settings.interpolate(x0, x1, threshold);
  258. right = settings.interpolate(x1, x2, threshold);
  259. if (settings.polygons_full) {
  260. cell.edges.bottom = {
  261. path: [ [bottom, 0], [1, right] ],
  262. move: {
  263. x: 1,
  264. y: 0,
  265. enter: 'left'
  266. }
  267. };
  268. }
  269. if (settings.polygons)
  270. cell.polygons.push([ [bottom, 0], [1, right], [1, 0] ]);
  271. break;
  272. case 11: /* 1011 */
  273. right = settings.interpolate(x1, x2, threshold);
  274. top = settings.interpolate(x3, x2, threshold);
  275. if (settings.polygons_full) {
  276. cell.edges.right = {
  277. path: [ [1, right], [top, 1] ],
  278. move: {
  279. x: 0,
  280. y: 1,
  281. enter: 'bottom'
  282. }
  283. };
  284. }
  285. if (settings.polygons)
  286. cell.polygons.push([ [1, right], [top, 1], [1, 1] ]);
  287. break;
  288. case 7: /* 0111 */
  289. left = settings.interpolate(x0, x3, threshold);
  290. top = settings.interpolate(x3, x2, threshold);
  291. if (settings.polygons_full) {
  292. cell.edges.top = {
  293. path: [ [top, 1], [0, left] ],
  294. move: {
  295. x: -1,
  296. y: 0,
  297. enter: 'right'
  298. }
  299. };
  300. }
  301. if (settings.polygons)
  302. cell.polygons.push([ [top, 1], [0, left], [0, 1] ]);
  303. break;
  304. case 1: /* 0001 */
  305. left = settings.interpolate(x0, x3, threshold);
  306. bottom = settings.interpolate(x0, x1, threshold);
  307. if (settings.polygons_full) {
  308. cell.edges.bottom = {
  309. path: [ [bottom, 0], [0, left] ],
  310. move: {
  311. x: -1,
  312. y: 0,
  313. enter: 'right'
  314. }
  315. };
  316. }
  317. if (settings.polygons)
  318. cell.polygons.push([ [bottom, 0], [0, left], [0, 1], [1, 1], [1, 0] ]);
  319. break;
  320. case 2: /* 0010 */
  321. bottom = settings.interpolate(x0, x1, threshold);
  322. right = settings.interpolate(x1, x2, threshold);
  323. if (settings.polygons_full) {
  324. cell.edges.right = {
  325. path: [ [1, right], [bottom, 0] ],
  326. move: {
  327. x: 0,
  328. y: -1,
  329. enter: 'top'
  330. }
  331. };
  332. }
  333. if (settings.polygons)
  334. cell.polygons.push([ [0, 0], [0, 1], [1, 1], [1, right], [bottom, 0] ]);
  335. break;
  336. case 4: /* 0100 */
  337. right = settings.interpolate(x1, x2, threshold);
  338. top = settings.interpolate(x3, x2, threshold);
  339. if (settings.polygons_full) {
  340. cell.edges.top = {
  341. path: [ [top, 1], [1, right] ],
  342. move: {
  343. x: 1,
  344. y: 0,
  345. enter: 'left'
  346. }
  347. };
  348. }
  349. if (settings.polygons)
  350. cell.polygons.push([ [0, 0], [0, 1], [top, 1], [1, right], [1, 0] ]);
  351. break;
  352. case 8: /* 1000 */
  353. left = settings.interpolate(x0, x3, threshold);
  354. top = settings.interpolate(x3, x2, threshold);
  355. if (settings.polygons_full) {
  356. cell.edges.left = {
  357. path: [ [0, left], [top, 1] ],
  358. move: {
  359. x: 0,
  360. y: 1,
  361. enter: 'bottom'
  362. }
  363. };
  364. }
  365. if (settings.polygons)
  366. cell.polygons.push([ [0, 0], [0, left], [top, 1], [1, 1], [1, 0] ]);
  367. break;
  368. case 12: /* 1100 */
  369. left = settings.interpolate(x0, x3, threshold);
  370. right = settings.interpolate(x1, x2, threshold);
  371. if (settings.polygons_full) {
  372. cell.edges.left = {
  373. path: [ [0, left], [1, right] ],
  374. move: {
  375. x: 1,
  376. y: 0,
  377. enter: 'left'
  378. }
  379. };
  380. }
  381. if (settings.polygons)
  382. cell.polygons.push([ [0, 0], [0, left], [1, right], [1, 0] ]);
  383. break;
  384. case 9: /* 1001 */
  385. bottom = settings.interpolate(x0, x1, threshold);
  386. top = settings.interpolate(x3, x2, threshold);
  387. if (settings.polygons_full) {
  388. cell.edges.bottom = {
  389. path: [ [bottom, 0], [top, 1] ],
  390. move: {
  391. x: 0,
  392. y: 1,
  393. enter: 'bottom'
  394. }
  395. };
  396. }
  397. if (settings.polygons)
  398. cell.polygons.push([ [bottom, 0], [top, 1], [1, 1], [1, 0] ]);
  399. break;
  400. case 3: /* 0011 */
  401. left = settings.interpolate(x0, x3, threshold);
  402. right = settings.interpolate(x1, x2, threshold);
  403. if (settings.polygons_full) {
  404. cell.edges.right = {
  405. path: [ [1, right], [0, left] ],
  406. move: {
  407. x: -1,
  408. y: 0,
  409. enter: 'right'
  410. }
  411. };
  412. }
  413. if (settings.polygons)
  414. cell.polygons.push([ [0, left], [0, 1], [1, 1], [1, right] ]);
  415. break;
  416. case 6: /* 0110 */
  417. bottom = settings.interpolate(x0, x1, threshold);
  418. top = settings.interpolate(x3, x2, threshold);
  419. if (settings.polygons_full) {
  420. cell.edges.top = {
  421. path: [ [top, 1], [bottom, 0] ],
  422. move: {
  423. x: 0,
  424. y: -1,
  425. enter: 'top'
  426. }
  427. };
  428. }
  429. if (settings.polygons)
  430. cell.polygons.push([ [0, 0], [0, 1], [top, 1], [bottom, 0] ]);
  431. break;
  432. case 10: /* 1010 */
  433. left = settings.interpolate(x0, x3, threshold);
  434. right = settings.interpolate(x1, x2, threshold);
  435. bottom = settings.interpolate(x0, x1, threshold);
  436. top = settings.interpolate(x3, x2, threshold);
  437. average = (x0 + x1 + x2 + x3) / 4;
  438. if (settings.polygons_full) {
  439. if (average < threshold) {
  440. cell.edges.left = {
  441. path: [ [0, left], [top, 1] ],
  442. move: {
  443. x: 0,
  444. y: 1,
  445. enter: 'bottom'
  446. }
  447. };
  448. cell.edges.right = {
  449. path: [ [1, right], [bottom, 0] ],
  450. move: {
  451. x: 0,
  452. y: -1,
  453. enter: 'top'
  454. }
  455. };
  456. } else {
  457. cell.edges.right = {
  458. path: [ [1, right], [top, 1] ],
  459. move: {
  460. x: 0,
  461. y: 1,
  462. enter: 'bottom'
  463. }
  464. };
  465. cell.edges.left = {
  466. path: [ [0, left], [bottom, 0] ],
  467. move: {
  468. x: 0,
  469. y: -1,
  470. enter: 'top'
  471. }
  472. };
  473. }
  474. }
  475. if (settings.polygons) {
  476. if (average < threshold) {
  477. cell.polygons.push([ [0, 0], [0, left], [top, 1], [1, 1], [1, right], [bottom, 0] ]);
  478. } else {
  479. cell.polygons.push([ [0, 0], [0, left], [bottom, 0] ]);
  480. cell.polygons.push([ [top, 1], [1, 1], [1, right] ]);
  481. }
  482. }
  483. break;
  484. case 5: /* 0101 */
  485. left = settings.interpolate(x0, x3, threshold);
  486. right = settings.interpolate(x1, x2, threshold);
  487. bottom = settings.interpolate(x0, x1, threshold);
  488. top = settings.interpolate(x3, x2, threshold);
  489. average = (x0 + x1 + x2 + x3) / 4;
  490. if (settings.polygons_full) {
  491. if (average < threshold) {
  492. cell.edges.bottom = {
  493. path: [ [bottom, 0], [0, left] ],
  494. move: {
  495. x: -1,
  496. y: 0,
  497. enter: 'right'
  498. }
  499. };
  500. cell.edges.top = {
  501. path: [ [top, 1], [1, right] ],
  502. move: {
  503. x: 1,
  504. y: 0,
  505. enter: 'left'
  506. }
  507. };
  508. } else {
  509. cell.edges.top = {
  510. path: [ [top, 1], [0, left] ],
  511. move: {
  512. x: -1,
  513. y: 0,
  514. enter: 'right'
  515. }
  516. };
  517. cell.edges.bottom = {
  518. path: [ [bottom, 0], [1, right] ],
  519. move: {
  520. x: 1,
  521. y: 0,
  522. enter: 'left'
  523. }
  524. };
  525. }
  526. }
  527. if (settings.polygons) {
  528. if (average < threshold) {
  529. cell.polygons.push([ [0, left], [0, 1], [top, 1], [1, right], [1, 0], [bottom, 0] ]);
  530. } else {
  531. cell.polygons.push([ [0, left], [0, 1], [top, 1] ]);
  532. cell.polygons.push([ [bottom, 0], [1, right], [1, 0] ]);
  533. }
  534. }
  535. break;
  536. }
  537. return cell;
  538. }
  539. export {
  540. isoLines,
  541. isoLines as isoContours
  542. };