raster-marching-squares
Version:
Create isobands and isolines directly from a Raster with the marching-squares algorithm
364 lines (321 loc) • 11.7 kB
JavaScript
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" };
}