| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638 |
- /* eslint no-console: ["error", { allow: ["log"] }] */
- /* eslint-env browser,node */
- import {isoLineOptions} from './options.js';
- import {cell2Polygons, traceLinePaths} from './polygons.js';
- import {QuadTree} from './quadtree.js';
- /*
- * Compute the iso lines for a scalar 2D field given
- * a certain threshold by applying the Marching Squares
- * Algorithm. The function returns a list of path coordinates
- */
- function isoLines(input, threshold, options) {
- var settings,
- i,
- j,
- useQuadTree = false,
- multiLine = false,
- tree = null,
- root = null,
- data = null,
- cellGrid = null,
- linePolygons = null,
- ret = [];
- /* validation */
- if (!input) throw new Error('data is required');
- if (threshold === undefined || threshold === null) throw new Error('threshold is required');
- if ((!!options) && (typeof options !== 'object')) throw new Error('options must be an object');
- /* process options */
- settings = isoLineOptions(options);
- /* check for input data */
- if (input instanceof QuadTree) {
- tree = input;
- root = input.root;
- data = input.data;
- if (!settings.noQuadTree)
- useQuadTree = true;
- } else if (Array.isArray(input) && Array.isArray(input[0])) {
- data = input;
- } else {
- throw new Error('input is neither array of arrays nor object retrieved from \'QuadTree()\'');
- }
- /* check and prepare input threshold(s) */
- if (Array.isArray(threshold)) {
- multiLine = true;
- /* activate QuadTree optimization if not explicitly forbidden by user settings */
- if (!settings.noQuadTree)
- useQuadTree = true;
- /* check if all minV are numbers */
- for (i = 0; i < threshold.length; i++)
- if (isNaN(+threshold[i]))
- throw new Error('threshold[' + i + '] is not a number');
- } else {
- if (isNaN(+threshold))
- throw new Error('threshold must be a number or array of numbers');
- threshold = [ threshold ];
- }
- /* create QuadTree root node if not already present */
- if ((useQuadTree) && (!root)) {
- tree = new QuadTree(data);
- root = tree.root;
- data = tree.data;
- }
- if (settings.verbose) {
- if(settings.polygons)
- console.log('MarchingSquaresJS-isoLines: returning single lines (polygons) for each grid cell');
- else
- console.log('MarchingSquaresJS-isoLines: returning line paths (polygons) for entire data grid');
- if (multiLine)
- console.log('MarchingSquaresJS-isoLines: multiple lines requested, returning array of line paths instead of lines for a single threshold');
- }
- /* Done with all input validation, now let's start computing stuff */
- /* loop over all threhsold values */
- threshold.forEach(function(t, i) {
- linePolygons = [];
- /* store bounds for current computation in settings object */
- settings.threshold = t;
- if(settings.verbose)
- console.log('MarchingSquaresJS-isoLines: computing iso lines for threshold ' + t);
- if (settings.polygons) {
- /* compose list of polygons for each single cell */
- if (useQuadTree) {
- /* go through list of cells retrieved from QuadTree */
- root
- .cellsBelowThreshold(settings.threshold, true)
- .forEach(function(c) {
- linePolygons = linePolygons.concat(
- cell2Polygons(
- prepareCell(data,
- c.x,
- c.y,
- settings),
- c.x,
- c.y,
- settings
- ));
- });
- } else {
- /* go through entire array of input data */
- for (j = 0; j < data.length - 1; ++j) {
- for (i = 0; i < data[0].length - 1; ++i)
- linePolygons = linePolygons.concat(
- cell2Polygons(
- prepareCell(data,
- i,
- j,
- settings),
- i,
- j,
- settings
- ));
- }
- }
- } else {
- /* sparse grid of input data cells */
- cellGrid = [];
- for (i = 0; i < data[0].length - 1; ++i)
- cellGrid[i] = [];
- /* compose list of polygons for entire input grid */
- if (useQuadTree) {
- /* collect the cells */
- root
- .cellsBelowThreshold(settings.threshold, false)
- .forEach(function(c) {
- cellGrid[c.x][c.y] = prepareCell(data,
- c.x,
- c.y,
- settings);
- });
- } else {
- /* prepare cells */
- for (i = 0; i < data[0].length - 1; ++i) {
- for (j = 0; j < data.length - 1; ++j) {
- cellGrid[i][j] = prepareCell(data,
- i,
- j,
- settings);
- }
- }
- }
- linePolygons = traceLinePaths(data, cellGrid, settings);
- }
- /* finally, add polygons to output array */
- if (multiLine)
- ret.push(linePolygons);
- else
- ret = linePolygons;
- if(typeof settings.successCallback === 'function')
- settings.successCallback(ret, t);
- });
- return ret;
- }
- /*
- * Thats all for the public interface, below follows the actual
- * implementation
- */
- /*
- * ################################
- * Isocontour implementation below
- * ################################
- */
- function prepareCell(grid, x, y, settings) {
- var left,
- right,
- top,
- bottom,
- average,
- cell;
- var cval = 0;
- var x3 = grid[y + 1][x];
- var x2 = grid[y + 1][x + 1];
- var x1 = grid[y][x + 1];
- var x0 = grid[y][x];
- var threshold = settings.threshold;
- /*
- * Note that missing data within the grid will result
- * in horribly failing to trace full polygon paths
- */
- if(isNaN(x0) || isNaN(x1) || isNaN(x2) || isNaN(x3)) {
- return;
- }
- /*
- * Here we detect the type of the cell
- *
- * x3 ---- x2
- * | |
- * | |
- * x0 ---- x1
- *
- * with edge points
- *
- * x0 = (x,y),
- * x1 = (x + 1, y),
- * x2 = (x + 1, y + 1), and
- * x3 = (x, y + 1)
- *
- * and compute the polygon intersections with the edges
- * of the cell. Each edge value may be (i) smaller, or (ii)
- * greater or equal to the iso line threshold. We encode
- * this property using 1 bit of information, where
- *
- * 0 ... below,
- * 1 ... above or equal
- *
- * Then we store the cells value as vector
- *
- * cval = (x0, x1, x2, x3)
- *
- * where x0 is the least significant bit (0th),
- * x1 the 2nd bit, and so on. This essentially
- * enables us to work with a single integer number
- */
- cval |= ((x3 >= threshold) ? 8 : 0);
- cval |= ((x2 >= threshold) ? 4 : 0);
- cval |= ((x1 >= threshold) ? 2 : 0);
- cval |= ((x0 >= threshold) ? 1 : 0);
- /* make sure cval is a number */
- cval = +cval;
- /* compose the cell object */
- cell = {
- cval: cval,
- polygons: [],
- edges: {},
- x0: x0,
- x1: x1,
- x2: x2,
- x3: x3
- };
- /*
- * Compute interpolated intersections of the polygon(s)
- * with the cell borders and (i) add edges for polygon
- * trace-back, or (ii) a list of small closed polygons
- */
- switch (cval) {
- case 0:
- if (settings.polygons)
- cell.polygons.push([ [0, 0], [0, 1], [1, 1], [1, 0] ]);
- break;
- case 15:
- /* cell is outside (above) threshold, no polygons */
- break;
- case 14: /* 1110 */
- left = settings.interpolate(x0, x3, threshold);
- bottom = settings.interpolate(x0, x1, threshold);
- if (settings.polygons_full) {
- cell.edges.left = {
- path: [ [0, left], [bottom, 0] ],
- move: {
- x: 0,
- y: -1,
- enter: 'top'
- }
- };
- }
- if (settings.polygons)
- cell.polygons.push([ [0, 0], [0, left], [bottom, 0] ]);
- break;
- case 13: /* 1101 */
- bottom = settings.interpolate(x0, x1, threshold);
- right = settings.interpolate(x1, x2, threshold);
- if (settings.polygons_full) {
- cell.edges.bottom = {
- path: [ [bottom, 0], [1, right] ],
- move: {
- x: 1,
- y: 0,
- enter: 'left'
- }
- };
- }
- if (settings.polygons)
- cell.polygons.push([ [bottom, 0], [1, right], [1, 0] ]);
- break;
- case 11: /* 1011 */
- right = settings.interpolate(x1, x2, threshold);
- top = settings.interpolate(x3, x2, threshold);
- if (settings.polygons_full) {
- cell.edges.right = {
- path: [ [1, right], [top, 1] ],
- move: {
- x: 0,
- y: 1,
- enter: 'bottom'
- }
- };
- }
- if (settings.polygons)
- cell.polygons.push([ [1, right], [top, 1], [1, 1] ]);
- break;
- case 7: /* 0111 */
- left = settings.interpolate(x0, x3, threshold);
- top = settings.interpolate(x3, x2, threshold);
- if (settings.polygons_full) {
- cell.edges.top = {
- path: [ [top, 1], [0, left] ],
- move: {
- x: -1,
- y: 0,
- enter: 'right'
- }
- };
- }
- if (settings.polygons)
- cell.polygons.push([ [top, 1], [0, left], [0, 1] ]);
- break;
- case 1: /* 0001 */
- left = settings.interpolate(x0, x3, threshold);
- bottom = settings.interpolate(x0, x1, threshold);
- if (settings.polygons_full) {
- cell.edges.bottom = {
- path: [ [bottom, 0], [0, left] ],
- move: {
- x: -1,
- y: 0,
- enter: 'right'
- }
- };
- }
- if (settings.polygons)
- cell.polygons.push([ [bottom, 0], [0, left], [0, 1], [1, 1], [1, 0] ]);
- break;
- case 2: /* 0010 */
- bottom = settings.interpolate(x0, x1, threshold);
- right = settings.interpolate(x1, x2, threshold);
- if (settings.polygons_full) {
- cell.edges.right = {
- path: [ [1, right], [bottom, 0] ],
- move: {
- x: 0,
- y: -1,
- enter: 'top'
- }
- };
- }
- if (settings.polygons)
- cell.polygons.push([ [0, 0], [0, 1], [1, 1], [1, right], [bottom, 0] ]);
- break;
- case 4: /* 0100 */
- right = settings.interpolate(x1, x2, threshold);
- top = settings.interpolate(x3, x2, threshold);
- if (settings.polygons_full) {
- cell.edges.top = {
- path: [ [top, 1], [1, right] ],
- move: {
- x: 1,
- y: 0,
- enter: 'left'
- }
- };
- }
- if (settings.polygons)
- cell.polygons.push([ [0, 0], [0, 1], [top, 1], [1, right], [1, 0] ]);
- break;
- case 8: /* 1000 */
- left = settings.interpolate(x0, x3, threshold);
- top = settings.interpolate(x3, x2, threshold);
- if (settings.polygons_full) {
- cell.edges.left = {
- path: [ [0, left], [top, 1] ],
- move: {
- x: 0,
- y: 1,
- enter: 'bottom'
- }
- };
- }
- if (settings.polygons)
- cell.polygons.push([ [0, 0], [0, left], [top, 1], [1, 1], [1, 0] ]);
- break;
- case 12: /* 1100 */
- left = settings.interpolate(x0, x3, threshold);
- right = settings.interpolate(x1, x2, threshold);
- if (settings.polygons_full) {
- cell.edges.left = {
- path: [ [0, left], [1, right] ],
- move: {
- x: 1,
- y: 0,
- enter: 'left'
- }
- };
- }
- if (settings.polygons)
- cell.polygons.push([ [0, 0], [0, left], [1, right], [1, 0] ]);
- break;
- case 9: /* 1001 */
- bottom = settings.interpolate(x0, x1, threshold);
- top = settings.interpolate(x3, x2, threshold);
- if (settings.polygons_full) {
- cell.edges.bottom = {
- path: [ [bottom, 0], [top, 1] ],
- move: {
- x: 0,
- y: 1,
- enter: 'bottom'
- }
- };
- }
- if (settings.polygons)
- cell.polygons.push([ [bottom, 0], [top, 1], [1, 1], [1, 0] ]);
- break;
- case 3: /* 0011 */
- left = settings.interpolate(x0, x3, threshold);
- right = settings.interpolate(x1, x2, threshold);
- if (settings.polygons_full) {
- cell.edges.right = {
- path: [ [1, right], [0, left] ],
- move: {
- x: -1,
- y: 0,
- enter: 'right'
- }
- };
- }
- if (settings.polygons)
- cell.polygons.push([ [0, left], [0, 1], [1, 1], [1, right] ]);
- break;
- case 6: /* 0110 */
- bottom = settings.interpolate(x0, x1, threshold);
- top = settings.interpolate(x3, x2, threshold);
- if (settings.polygons_full) {
- cell.edges.top = {
- path: [ [top, 1], [bottom, 0] ],
- move: {
- x: 0,
- y: -1,
- enter: 'top'
- }
- };
- }
- if (settings.polygons)
- cell.polygons.push([ [0, 0], [0, 1], [top, 1], [bottom, 0] ]);
- break;
- case 10: /* 1010 */
- left = settings.interpolate(x0, x3, threshold);
- right = settings.interpolate(x1, x2, threshold);
- bottom = settings.interpolate(x0, x1, threshold);
- top = settings.interpolate(x3, x2, threshold);
- average = (x0 + x1 + x2 + x3) / 4;
- if (settings.polygons_full) {
- if (average < threshold) {
- cell.edges.left = {
- path: [ [0, left], [top, 1] ],
- move: {
- x: 0,
- y: 1,
- enter: 'bottom'
- }
- };
- cell.edges.right = {
- path: [ [1, right], [bottom, 0] ],
- move: {
- x: 0,
- y: -1,
- enter: 'top'
- }
- };
- } else {
- cell.edges.right = {
- path: [ [1, right], [top, 1] ],
- move: {
- x: 0,
- y: 1,
- enter: 'bottom'
- }
- };
- cell.edges.left = {
- path: [ [0, left], [bottom, 0] ],
- move: {
- x: 0,
- y: -1,
- enter: 'top'
- }
- };
- }
- }
- if (settings.polygons) {
- if (average < threshold) {
- cell.polygons.push([ [0, 0], [0, left], [top, 1], [1, 1], [1, right], [bottom, 0] ]);
- } else {
- cell.polygons.push([ [0, 0], [0, left], [bottom, 0] ]);
- cell.polygons.push([ [top, 1], [1, 1], [1, right] ]);
- }
- }
- break;
- case 5: /* 0101 */
- left = settings.interpolate(x0, x3, threshold);
- right = settings.interpolate(x1, x2, threshold);
- bottom = settings.interpolate(x0, x1, threshold);
- top = settings.interpolate(x3, x2, threshold);
- average = (x0 + x1 + x2 + x3) / 4;
- if (settings.polygons_full) {
- if (average < threshold) {
- cell.edges.bottom = {
- path: [ [bottom, 0], [0, left] ],
- move: {
- x: -1,
- y: 0,
- enter: 'right'
- }
- };
- cell.edges.top = {
- path: [ [top, 1], [1, right] ],
- move: {
- x: 1,
- y: 0,
- enter: 'left'
- }
- };
- } else {
- cell.edges.top = {
- path: [ [top, 1], [0, left] ],
- move: {
- x: -1,
- y: 0,
- enter: 'right'
- }
- };
- cell.edges.bottom = {
- path: [ [bottom, 0], [1, right] ],
- move: {
- x: 1,
- y: 0,
- enter: 'left'
- }
- };
- }
- }
- if (settings.polygons) {
- if (average < threshold) {
- cell.polygons.push([ [0, left], [0, 1], [top, 1], [1, right], [1, 0], [bottom, 0] ]);
- } else {
- cell.polygons.push([ [0, left], [0, 1], [top, 1] ]);
- cell.polygons.push([ [bottom, 0], [1, right], [1, 0] ]);
- }
- }
- break;
- }
- return cell;
- }
- export {
- isoLines,
- isoLines as isoContours
- };
|