UNPKG

raster-marching-squares

Version:

Create isobands and isolines directly from a Raster with the marching-squares algorithm

364 lines (321 loc) 11.7 kB
export var isolines = function(data, geoTransform, intervals){ var lines = { "type": "FeatureCollection", "features": [] }; for(var i=0; i<intervals.length; i++){ var value = intervals[i]; var coords = projectedIsoline(data, geoTransform, value); lines.features.push({"type": "Feature", "geometry": { "type": "MultiLineString", "coordinates": coords}, "properties": [{"value": value}]} ); } return lines; }; export var projectedIsoline = function(data, geoTransform, value){ if(typeof(geoTransform) != typeof(new Array()) || geoTransform.length != 6) throw new Error("GeoTransform must be a 6 elements array"); var coords = isoline(data, value); for(var i = 0; i<coords.length; i++){ for(var j = 0; j<coords[i].length; j++){ var coordsGeo = applyGeoTransform(coords[i][j][0], coords[i][j][1], geoTransform); coords[i][j][0]= coordsGeo[0]; coords[i][j][1]= coordsGeo[1]; } } return coords; }; /** Xgeo = GT(0) + Xpixel*GT(1) + Yline*GT(2) Ygeo = GT(3) + Xpixel*GT(4) + Yline*GT(5) */ var applyGeoTransform = function(x, y, geoTransform){ var xgeo = geoTransform[0] + x*geoTransform[1] + y*geoTransform[2]; var ygeo = geoTransform[3] + x*geoTransform[4] + y*geoTransform[5]; return [xgeo, ygeo]; }; export var isoline = function(data, threshold, options){ var defaultSettings = { successCallback: null, progressCallback: null, verbose: false }; var settings = {}; /* process options */ options = options ? options : {}; var optionKeys = Object.keys(defaultSettings); for(var i = 0; i < optionKeys.length; i++){ var key = optionKeys[i]; var val = options[key]; val = ((typeof val !== 'undefined') && (val !== null)) ? val : defaultSettings[key]; settings[key] = val; } if(settings.verbose) console.log("computing isocontour for " + threshold); var ret = ContourGrid2Paths(computeContourGrid(data, threshold)); if(typeof settings.successCallback === 'function') settings.successCallback(ret); return ret; }; /* Thats all for the public interface, below follows the actual implementation */ /* ################################ Isocontour implementation below ################################ */ /* assume that x1 == 1 && x0 == 0 */ function interpolateX(y, y0, y1){ return (y - y0) / (y1 - y0); } /* compute the isocontour 4-bit grid */ function computeContourGrid(data, threshold){ var rows = data.length - 1; var cols = data[0].length - 1; var ContourGrid = { rows: rows, cols: cols, cells: [] }; for(var j = 0; j < rows; ++j){ ContourGrid.cells[j] = []; for(var i = 0; i < cols; ++i){ /* compose the 4-bit corner representation */ var cval = 0; var tl = data[j+1][i]; var tr = data[j+1][i+1]; var br = data[j][i+1]; var bl = data[j][i]; if(isNaN(tl) || isNaN(tr) || isNaN(br) || isNaN(bl)){ continue; } cval |= ((tl >= threshold) ? 8 : 0); cval |= ((tr >= threshold) ? 4 : 0); cval |= ((br >= threshold) ? 2 : 0); cval |= ((bl >= threshold) ? 1 : 0); /* resolve ambiguity for cval == 5 || 10 via averaging */ var flipped = false; if(cval == 5 || cval == 10){ var average = (tl + tr + br + bl) / 4; if(cval == 5 && (average < threshold)){ cval = 10; flipped = true; } else if(cval == 10 && (average < threshold)){ cval = 5; flipped = true; } } /* add cell to ContourGrid if it contains edges */ if(cval !== 0 && cval !== 15){ var top, bottom, left, right; top = bottom = left = right = 0.5; /* interpolate edges of cell */ if(cval == 1){ left = 1 - interpolateX(threshold, tl, bl); bottom = 1 - interpolateX(threshold, br, bl); } else if(cval == 2){ bottom = interpolateX(threshold, bl, br); right = 1 - interpolateX(threshold, tr, br); } else if(cval == 3){ left = 1 - interpolateX(threshold, tl, bl); right = 1 - interpolateX(threshold, tr, br); } else if(cval == 4){ top = interpolateX(threshold, tl, tr); right = interpolateX(threshold, br, tr); } else if(cval == 5){ top = interpolateX(threshold, tl, tr); right = interpolateX(threshold, br, tr); bottom = 1 - interpolateX(threshold, br, bl); left = 1 - interpolateX(threshold, tl, bl); } else if(cval == 6){ bottom = interpolateX(threshold, bl, br); top = interpolateX(threshold, tl, tr); } else if(cval == 7){ left = 1 - interpolateX(threshold, tl, bl); top = interpolateX(threshold, tl, tr); } else if(cval == 8){ left = interpolateX(threshold, bl, tl); top = 1 - interpolateX(threshold, tr, tl); } else if(cval == 9){ bottom = 1 - interpolateX(threshold, br, bl); top = 1 - interpolateX(threshold, tr, tl); } else if(cval == 10){ top = 1 - interpolateX(threshold, tr, tl); right = 1 - interpolateX(threshold, tr, br); bottom = interpolateX(threshold, bl, br); left = interpolateX(threshold, bl, tl); } else if(cval == 11){ top = 1 - interpolateX(threshold, tr, tl); right = 1 - interpolateX(threshold, tr, br); } else if(cval == 12){ left = interpolateX(threshold, bl, tl); right = interpolateX(threshold, br, tr); } else if(cval == 13){ bottom = 1 - interpolateX(threshold, br, bl); right = interpolateX(threshold, br, tr); } else if(cval == 14){ left = interpolateX(threshold, bl, tl); bottom = interpolateX(threshold, bl, br); } else { console.log("Illegal cval detected: " + cval); } ContourGrid.cells[j][i] = { cval: cval, flipped: flipped, top: top, right: right, bottom: bottom, left: left }; } } } return ContourGrid; } function isSaddle(cell){ return cell.cval == 5 || cell.cval == 10; } function isTrivial(cell){ return cell.cval === 0 || cell.cval == 15; } function clearCell(cell){ if((!isTrivial(cell)) && (cell.cval != 5) && (cell.cval != 10)){ cell.cval = 15; } } function getXY(cell, edge){ if(edge === "top"){ return [cell.top, 1.0]; } else if(edge === "bottom"){ return [cell.bottom, 0.0]; } else if(edge === "right"){ return [1.0, cell.right]; } else if(edge === "left"){ return [0.0, cell.left]; } } function ContourGrid2Paths(grid){ var paths = []; var path_idx = 0; var rows = grid.rows; var cols = grid.cols; var epsilon = 1e-7; grid.cells.forEach(function(g, j){ g.forEach(function(gg, i){ if((typeof gg !== 'undefined') && (!isSaddle(gg)) && (!isTrivial(gg))){ var p = tracePath(grid.cells, j, i); var merged = false; /* we may try to merge paths at this point */ if(p.info == "mergeable"){ /* search backwards through the path array to find an entry that starts with where the current path ends... */ var x = p.path[p.path.length - 1][0], y = p.path[p.path.length - 1][1]; for(var k = path_idx - 1; k >= 0; k--){ if((Math.abs(paths[k][0][0] - x) <= epsilon) && (Math.abs(paths[k][0][1] - y) <= epsilon)){ for(var l = p.path.length - 2; l >= 0; --l){ paths[k].unshift(p.path[l]); } merged = true; break; } } } if(!merged) paths[path_idx++] = p.path; } }); }); return paths; } /* construct consecutive line segments from starting cell by walking arround the enclosed area clock-wise */ function tracePath(grid, j, i){ var maxj = grid.length; var p = []; var dxContour = [0, 0, 1, 1, 0, 0, 0, 0, -1, 0, 1, 1, -1, 0, -1, 0]; var dyContour = [0, -1, 0, 0, 1, 1, 1, 1, 0, -1, 0, 0, 0, -1, 0, 0]; var dx, dy; var startEdge = ["none", "left", "bottom", "left", "right", "none", "bottom", "left", "top", "top", "none", "top", "right", "right", "bottom", "none"]; var nextEdge = ["none", "bottom", "right", "right", "top", "top", "top", "top", "left", "bottom", "right", "right", "left", "bottom", "left", "none"]; var startCell = grid[j][i]; var currentCell = grid[j][i]; var cval = currentCell.cval; var edge = startEdge[cval]; var pt = getXY(currentCell, edge); /* push initial segment */ p.push([i + pt[0], j + pt[1]]); edge = nextEdge[cval]; pt = getXY(currentCell, edge); p.push([i + pt[0], j + pt[1]]); clearCell(currentCell); /* now walk arround the enclosed area in clockwise-direction */ var k = i + dxContour[cval]; var l = j + dyContour[cval]; var prev_cval = cval; while((k >= 0) && (l >= 0) && (l < maxj) && ((k != i) || (l != j))){ currentCell = grid[l][k]; if(typeof currentCell === 'undefined'){ /* path ends here */ //console.log(k + " " + l + " is undefined, stopping path!"); break; } cval = currentCell.cval; if((cval === 0) || (cval === 15)){ return { path: p, info: "mergeable" }; } edge = nextEdge[cval]; dx = dxContour[cval]; dy = dyContour[cval]; if((cval == 5) || (cval == 10)){ /* select upper or lower band, depending on previous cells cval */ if(cval == 5){ if(currentCell.flipped){ /* this is actually a flipped case 10 */ if(dyContour[prev_cval] == -1){ edge = "left"; dx = -1; dy = 0; } else { edge = "right"; dx = 1; dy = 0; } } else { /* real case 5 */ if(dxContour[prev_cval] == -1){ edge = "bottom"; dx = 0; dy = -1; } } } else if(cval == 10){ if(currentCell.flipped){ /* this is actually a flipped case 5 */ if(dxContour[prev_cval] == -1){ edge = "top"; dx = 0; dy = 1; } else { edge = "bottom"; dx = 0; dy = -1; } } else { /* real case 10 */ if(dyContour[prev_cval] == 1){ edge = "left"; dx = -1; dy = 0; } } } } pt = getXY(currentCell, edge); p.push([k + pt[0], l + pt[1]]); clearCell(currentCell); k += dx; l += dy; prev_cval = cval; } return { path: p, info: "closed" }; }