modules/aggregation-layers/src/contour-layer/marching-squares.js (118 lines of code) (raw):
// All utility methods needed to implement Marching Squares algorithm
// Ref: https://en.wikipedia.org/wiki/Marching_squares
import {log} from '@deck.gl/core';
import {ISOLINES_CODE_OFFSET_MAP, ISOBANDS_CODE_OFFSET_MAP} from './marching-squares-codes';
export const CONTOUR_TYPE = {
ISO_LINES: 1,
ISO_BANDS: 2
};
const DEFAULT_THRESHOLD_DATA = {
zIndex: 0,
zOffset: 0.005
};
// Utility methods
function getVertexCode(weight, threshold) {
// threshold must be a single value or a range (array of size 2)
// Iso-bands
if (Array.isArray(threshold)) {
if (weight < threshold[0]) {
return 0;
}
return weight < threshold[1] ? 1 : 2;
}
// Iso-lines
return weight >= threshold ? 1 : 0;
}
// Returns marching square code for given cell
/* eslint-disable complexity, max-statements*/
export function getCode(opts) {
// Assumptions
// Origin is on bottom-left , and X increase to right, Y to top
// When processing one cell, we process 4 cells, by extending row to top and on column to right
// to create a 2X2 cell grid
const {cellWeights, x, y, width, height} = opts;
let threshold = opts.threshold;
if (opts.thresholdValue) {
log.deprecated('thresholdValue', 'threshold')();
threshold = opts.thresholdValue;
}
const isLeftBoundary = x < 0;
const isRightBoundary = x >= width - 1;
const isBottomBoundary = y < 0;
const isTopBoundary = y >= height - 1;
const isBoundary = isLeftBoundary || isRightBoundary || isBottomBoundary || isTopBoundary;
const weights = {};
const codes = {};
// TOP
if (isLeftBoundary || isTopBoundary) {
codes.top = 0;
} else {
weights.top = cellWeights[(y + 1) * width + x];
codes.top = getVertexCode(weights.top, threshold);
}
// TOP-RIGHT
if (isRightBoundary || isTopBoundary) {
codes.topRight = 0;
} else {
weights.topRight = cellWeights[(y + 1) * width + x + 1];
codes.topRight = getVertexCode(weights.topRight, threshold);
}
// RIGHT
if (isRightBoundary || isBottomBoundary) {
codes.right = 0;
} else {
weights.right = cellWeights[y * width + x + 1];
codes.right = getVertexCode(weights.right, threshold);
}
// CURRENT
if (isLeftBoundary || isBottomBoundary) {
codes.current = 0;
} else {
weights.current = cellWeights[y * width + x];
codes.current = getVertexCode(weights.current, threshold);
}
const {top, topRight, right, current} = codes;
let code = -1;
if (Number.isFinite(threshold)) {
code = (top << 3) | (topRight << 2) | (right << 1) | current;
}
if (Array.isArray(threshold)) {
code = (top << 6) | (topRight << 4) | (right << 2) | current;
}
let meanCode = 0;
// meanCode is only needed for saddle cases, and they should
// only occur when we are not processing a cell on boundary
// because when on a boundary either, bottom-row, top-row, left-column or right-column will have both 0 codes
if (!isBoundary) {
meanCode = getVertexCode(
(weights.top + weights.topRight + weights.right + weights.current) / 4,
threshold
);
}
return {code, meanCode};
}
/* eslint-enable complexity, max-statements*/
// Returns intersection vertices for given cellindex
// [x, y] refers current marching cell, reference vertex is always top-right corner
export function getVertices(opts) {
const {gridOrigin, cellSize, x, y, code, meanCode, type = CONTOUR_TYPE.ISO_LINES} = opts;
const thresholdData = Object.assign({}, DEFAULT_THRESHOLD_DATA, opts.thresholdData);
let offsets =
type === CONTOUR_TYPE.ISO_BANDS
? ISOBANDS_CODE_OFFSET_MAP[code]
: ISOLINES_CODE_OFFSET_MAP[code];
// handle saddle cases
if (!Array.isArray(offsets)) {
offsets = offsets[meanCode];
}
// Reference vertex is at top-right move to top-right corner
const vZ = thresholdData.zIndex * thresholdData.zOffset;
const rX = (x + 1) * cellSize[0];
const rY = (y + 1) * cellSize[1];
const refVertexX = gridOrigin[0] + rX;
const refVertexY = gridOrigin[1] + rY;
// offsets format
// ISO_LINES: [[1A, 1B], [2A, 2B]],
// ISO_BANDS: [[1A, 1B, 1C, ...], [2A, 2B, 2C, ...]],
// vertices format
// ISO_LINES: [[x1A, y1A], [x1B, y1B], [x2A, x2B], ...],
// ISO_BANDS: => confirms to SolidPolygonLayer's simple polygon format
// [
// [[x1A, y1A], [x1B, y1B], [x1C, y1C] ... ],
// ...
// ]
if (type === CONTOUR_TYPE.ISO_BANDS) {
const polygons = [];
offsets.forEach(polygonOffsets => {
const polygon = [];
polygonOffsets.forEach(xyOffset => {
const vX = refVertexX + xyOffset[0] * cellSize[0];
const vY = refVertexY + xyOffset[1] * cellSize[1];
polygon.push([vX, vY, vZ]);
});
polygons.push(polygon);
});
return polygons;
}
// default case is ISO_LINES
const lines = [];
offsets.forEach(xyOffsets => {
xyOffsets.forEach(offset => {
const vX = refVertexX + offset[0] * cellSize[0];
const vY = refVertexY + offset[1] * cellSize[1];
lines.push([vX, vY, vZ]);
});
});
return lines;
}