Javascript:点多边形性能改进

时间:2017-10-08 18:55:55

标签: javascript polygon computational-geometry point point-in-polygon

我有一个对象数组。每个对象代表一个具有ID的点和一个具有x y坐标的数组。 ,例如:

let points = [{id: 1, coords: [1,2]}, {id: 2, coords: [2,3]}]

我还有一个包含x y坐标的数组。该数组表示多边形,例如:

let polygon = [[0,0], [0,3], [1,4], [0,2]]

多边形已关闭,因此数组的最后一个点链接到第一个。

我使用以下算法检查点是否在多边形内:

pointInPolygon = function (point, polygon) {
  // from https://github.com/substack/point-in-polygon
  let x = point.coords[0]
  let y = point.coords[1]
  let inside = false

  for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
    let xi = polygon[i][0]
    let yi = polygon[i][1]
    let xj = polygon[j][0]
    let yj = polygon[j][1]
    let intersect = ((yi > y) !== (yj > y)) &&
                    (x < (xj - xi) * (y - yi) / (yj - yi) + xi)
    if (intersect) inside = !inside
  }

  return inside
}

用户使用鼠标绘制多边形,其工作方式如下: http://bl.ocks.org/bycoffe/5575904

每次鼠标移动(获取新坐标)时,我们必须将当前鼠标位置添加到多边形,然后我们必须遍历所有点并在每个点上调用pointInPolygon函数迭代。我已经扼杀了这个事件以提高绩效:

handleCurrentMouseLocation = throttle(function (mouseLocation, points, polygon) {
    let pointIDsInPolygon = []
    polygon.push(mouseLocation)

    for (let point in points) {
        if (pointInPolygon(point, polygon) {
            pointIDsInPolygon.push(point.id)
        }
    }
    return pointIDsInPolygon
}, 100)

当点数不是那么高(<200)时这很好用,但在我目前的项目中,我们有超过4000点。迭代所有这些点并且每100毫秒为每个点调用pointInPolygon函数会使整个事情变得非常滞后。

我正在寻找一种更快的方法来实现这一目标。例如:也许,当鼠标绘制多边形时,不是每100毫秒触发一次此功能,我们可以查找鼠标位置的一些最近点并将其存储在closestPoints数组中。然后,当鼠标x / y高于/低于某个值时,它只会循环遍历closestPoints中的点和多边形中已有的点。但我不知道这些closestPoints会是什么,或者这整个方法是否有意义。但我觉得解决方案是减少每次必须循环的点数。

要清楚,我的项目中超过4000个点是固定的 - 它们不是动态生成的,但总是具有完全相同的坐标。实际上,这些点代表多边形的质心,它们代表地图上城市的边界。因此,例如,可以提前计算每个点的closestPoints(在这种情况下,我们将计算这些点,而不是像前一段中那样的鼠标位置)。

任何可以帮助我的计算几何专家?

2 个答案:

答案 0 :(得分:3)

如果我理解正确,从鼠标记录的新点将使多边形大一点。因此,如果在某个时刻,多边形由 n (0,1,...,n-1)和新点 p ,然后多边形变为(0,1,...,n-1,p)

因此,这意味着从多边形中移除了一条边,而是将两条边添加到其中。

例如,假设我们在多边形上有9个点,编号为0到8,其中第8个点是添加到它的最后一个点:

enter image description here

灰线是关闭多边形的边。

现在鼠标移动到第9点,这将添加到多边形:

enter image description here

从多边形中移除灰色边缘,并将两个绿色边缘添加到其中。现在遵守以下规则:

与更改之前的位置相比,灰色和两个绿色边缘形成的三角形中的点交换进出多边形。所有其他积分保留其先前的进/出状态。

因此,如果您保留每个点在内存中的状态,那么您只需要检查每个点是否在上述三角形内,如果是,则需要切换该点的状态。 / p>

由于包含在三角形中的测试所花费的时间少于为可能复杂的多边形测试相同的时间,这将导致更高效的算法。

如果你在(x 0 ,y 0 )处取角的三角形的边界矩形,你可以进一步提高效率,(x < sub> 1 ,y 0 ),(x 1 ,y 1 ),(x 0 ,y 1 。然后,您可以跳过 x y 坐标超出范围的点:

enter image description here

蓝色框外的任何一点都不会改变状态:如果在添加最后一个点9之前它在多边形内,它现在仍然是。仅对于框内的点,您需要执行pointInPolygon测试,但仅限于三角形,而不是整个多边形。如果该测试返回true,则必须切换测试点的状态。

方框中的组点

为了进一步加快这个过程,您可以将带有网格的平面划分为方框,其中每个点属于一个框,但是一个框通常会有很多点。要确定三角形中的哪些点,您可以首先确定哪些框与三角形重叠。

为此你不必测试每个盒子,但可以从三角形边缘的坐标派生盒子。

然后,只需要单独测试其余框中的点。您可以使用框大小,看看它如何影响性能。

这是一个实施这些想法的工作示例。有10000点,但我的PC上没有滞后:

&#13;
&#13;
canvas.width = document.body.clientWidth; 

const min = [0, 0], 
    max = [canvas.width, canvas.height],
    points = Array.from(Array(10000), i => {
        let x = Math.floor(Math.random() * (max[0]-min[0]) + min[0]);
        let y = Math.floor(Math.random() * (max[1]-min[1]) + min[1]);
        return [x, y];
    }),
    polygon = [],
    boxSize = Math.ceil((max[0] - min[0]) / 50),
    boxes = (function (xBoxes, yBoxes) {
        return Array.from(Array(yBoxes), _ => 
                Array.from(Array(xBoxes), _ => []));
    })(toBox(0, max[0])+1, toBox(1, max[1])+1),
    insidePoints = new Set,
    ctx = canvas.getContext('2d');

function drawPoint(p) {
    ctx.fillRect(p[0], p[1], 1, 1);
}

function drawPolygon(pol) {
    ctx.beginPath();
    ctx.moveTo(pol[0][0], pol[0][1]);
    for (const p of pol) {  
        ctx.lineTo(p[0], p[1]);
    }
    ctx.stroke();
}

function segmentMap(a, b, dim, coord) {
    // Find the coordinate where ab is intersected by a coaxial line at 
    // the given coord.
    // First some boundary conditions:
    const dim2 = 1 - dim;
    if (a[dim] === coord) {
        if (b[dim] === coord) return [a[dim2], b[dim2]];
        return [a[dim2]];
    }
    if (b[dim] === coord) return [b[dim2]];
    // See if there is no intersection:
    if ((coord > a[dim]) === (coord > b[dim])) return [];
    // There is an intersection point:
    const res = (coord - a[dim]) * (b[dim2] - a[dim2]) / (b[dim] - a[dim]) + a[dim2];
    return [res];
}

function isLeft(a, b, c) {
    // Return true if c lies at the left of ab:
    return (b[0] - a[0])*(c[1] - a[1]) - (b[1] - a[1])*(c[0] - a[0]) > 0;
}

function inTriangle(a, b, c, p) {
    // First do a bounding box check:
    if (p[0] < Math.min(a[0], b[0], c[0]) ||
            p[0] > Math.max(a[0], b[0], c[0]) ||
            p[1] < Math.min(a[1], b[1], c[1]) ||
            p[1] > Math.max(a[1], b[1], c[1])) return false;
    // Then check that the point is on the same side of each of the 
    // three edges:
    const x = isLeft(a, b, p),
        y = isLeft(b, c, p),
        z = isLeft(c, a, p);
    return x ? y && z : !y && !z;
}

function toBox(dim, coord) {
    return Math.floor((coord - min[dim]) / boxSize);
}
function toWorld(dim, box) {
    return box * boxSize + min[dim];
}

function drawBox(boxX, boxY) {
    let x = toWorld(0, boxX);
    let y = toWorld(1, boxY);
    drawPolygon([[x, y], [x + boxSize, y], [x + boxSize, y + boxSize], [x, y + boxSize], [x, y]]);
}

function triangleTest(a, b, c, points, insidePoints) {
    const markedBoxes = new Set(), // collection of boxes that overlap with triangle
        box = [];
    for (let dim = 0; dim < 2; dim++) {
        const dim2 = 1-dim,
            // Order triangle points by coordinate
            [d, e, f] = [a, b, c].sort( (p, q) => p[dim] - q[dim] ),
            lastBox = toBox(dim, f[dim]);
        for (box[dim] = toBox(dim, d[dim]); box[dim] <= lastBox; box[dim]++) {
            // Calculate intersections of the triangle edges with the row/column of boxes
            const coord = toWorld(dim, box[dim]),
                intersections = 
                        [...new Set([...segmentMap(a, b, dim, coord),
                                     ...segmentMap(b, c, dim, coord), 
                                     ...segmentMap(a, c, dim, coord)])];
            if (!intersections.length) continue;
            intersections.sort( (a,b) => a - b );
            const lastBox2 = toBox(dim2, intersections.slice(-1)[0]);
            // Mark all boxes between the two intersection points
            for (box[dim2] = toBox(dim2, intersections[0]); box[dim2] <= lastBox2; box[dim2]++) {
                markedBoxes.add(boxes[box[1]][box[0]]);
                if (box[dim]) {
                    markedBoxes.add(boxes[box[1]-dim][box[0]-(dim2)]);
                }
            }
        }
    }
    // Perform the triangle test for each individual point in the marked boxes
    for (const box of markedBoxes) {
        for (const p of box) {
            if (inTriangle(a, b, c, p)) {
                // Toggle in/out state of this point
                if (insidePoints.delete(p)) {
                    ctx.fillStyle = '#000000';
                } else {
                    ctx.fillStyle = '#e0e0e0';
                    insidePoints.add(p);
                }
                drawPoint(p);
            }
        }
    }
}

// Draw points
points.forEach(drawPoint);

// Distribute points into boxes
for (const p of points) {
    let hor = Math.floor((p[0] - min[0]) / boxSize);
    let ver = Math.floor((p[1] - min[1]) / boxSize);
    boxes[ver][hor].push(p);
}

canvas.addEventListener('mousemove', (e) => {
    if (e.buttons !== 1) return;
    polygon.push([Math.max(e.offsetX,0), Math.max(e.offsetY,0)]);
    ctx.strokeStyle = '#000000';
    drawPolygon(polygon);
    const len = polygon.length;
    if (len > 2) {
        triangleTest(polygon[0], polygon[len-2+len%2], polygon[len-1-len%2], points, insidePoints);
    }
});

canvas.addEventListener('mousedown', (e) => {
    // Start a new polygon
    polygon.length = 0;
});
&#13;
Drag mouse to draw a shape:
<canvas id="canvas"></canvas>
&#13;
&#13;
&#13;

答案 1 :(得分:0)

每次更新多边形时,请保留背景图像,以执行多边形填充。

然后测试任何内部点将独立于多边形复杂度而保持恒定时间。