org.joml.PolygonsIntersection Maven / Gradle / Ivy
/*
* The MIT License
*
* Copyright (c) 2016-2020 Kai Burjack
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.joml;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* Class for polygon/point intersection tests when testing many points against one or many static concave or convex, simple polygons.
*
* This is an implementation of the algorithm described in http://alienryderflex.com and augmented with using a
* custom interval tree to avoid testing all polygon edges against a point, but only those that intersect the imaginary ray along the same y co-ordinate of the
* search point. This algorithm additionally also supports multiple polygons.
*
* This class is thread-safe and can be used in a multithreaded environment when testing many points against the same polygon concurrently.
*
* Reference: http://alienryderflex.com
*
* @author Kai Burjack
*/
public class PolygonsIntersection {
static class ByStartComparator implements Comparator {
public int compare(Object o1, Object o2) {
Interval i1 = (Interval) o1;
Interval i2 = (Interval) o2;
return Float.compare(i1.start, i2.start);
}
}
static class ByEndComparator implements Comparator {
public int compare(Object o1, Object o2) {
Interval i1 = (Interval) o1;
Interval i2 = (Interval) o2;
return Float.compare(i2.end, i1.end);
}
}
static class Interval {
float start, end;
int i, j, polyIndex;
}
static class IntervalTreeNode {
float center;
float childrenMinMax;
IntervalTreeNode left;
IntervalTreeNode right;
List/* */ byBeginning;
List/* */ byEnding;
static boolean computeEvenOdd(float[] verticesXY, Interval ival, float x, float y, boolean evenOdd, BitSet inPolys) {
boolean newEvenOdd = evenOdd;
int i = ival.i;
int j = ival.j;
float yi = verticesXY[2 * i + 1];
float yj = verticesXY[2 * j + 1];
float xi = verticesXY[2 * i + 0];
float xj = verticesXY[2 * j + 0];
if ((yi < y && yj >= y || yj < y && yi >= y) && (xi <= x || xj <= x)) {
float xDist = xi + (y - yi) / (yj - yi) * (xj - xi) - x;
newEvenOdd ^= xDist < 0.0f;
if (newEvenOdd != evenOdd && inPolys != null) {
inPolys.flip(ival.polyIndex);
}
}
return newEvenOdd;
}
boolean traverse(float[] verticesXY, float x, float y, boolean evenOdd, BitSet inPolys) {
boolean newEvenOdd = evenOdd;
if (y == center && byBeginning != null) {
int size = byBeginning.size();
for (int b = 0; b < size; b++) {
Interval ival = (Interval) byBeginning.get(b);
newEvenOdd = computeEvenOdd(verticesXY, ival, x, y, newEvenOdd, inPolys);
}
} else if (y < center) {
if (left != null && left.childrenMinMax >= y)
newEvenOdd = left.traverse(verticesXY, x, y, newEvenOdd, inPolys);
if (byBeginning != null) {
int size = byBeginning.size();
for (int b = 0; b < size; b++) {
Interval ival = (Interval) byBeginning.get(b);
if (ival.start > y)
break;
newEvenOdd = computeEvenOdd(verticesXY, ival, x, y, newEvenOdd, inPolys);
}
}
} else if (y > center) {
if (right != null && right.childrenMinMax <= y)
newEvenOdd = right.traverse(verticesXY, x, y, newEvenOdd, inPolys);
if (byEnding != null) {
int size = byEnding.size();
for (int b = 0; b < size; b++) {
Interval ival = (Interval) byEnding.get(b);
if (ival.end < y)
break;
newEvenOdd = computeEvenOdd(verticesXY, ival, x, y, newEvenOdd, inPolys);
}
}
}
return newEvenOdd;
}
}
private static final ByStartComparator byStartComparator = new ByStartComparator();
private static final ByEndComparator byEndComparator = new ByEndComparator();
protected final float[] verticesXY;
private float minX, minY, maxX, maxY;
private float centerX, centerY, radiusSquared;
private IntervalTreeNode tree;
/**
* Create a new {@link PolygonsIntersection} object with the given polygon vertices.
*
* The verticesXY
array contains the x and y coordinates of all vertices. This array will not be copied so its content must remain constant for
* as long as the PolygonPointIntersection is used with it.
*
* @param verticesXY
* contains the x and y coordinates of all vertices
* @param polygons
* defines the start vertices of a new polygon. The first vertex of the first polygon is always the
* vertex with index 0. In order to define a hole simply define a polygon that is completely inside another polygon
* @param count
* the number of vertices to use from the verticesXY
array, staring with index 0
*/
public PolygonsIntersection(float[] verticesXY, int[] polygons, int count) {
this.verticesXY = verticesXY;
// Do all the allocations and initializations during this constructor
preprocess(count, polygons);
}
private IntervalTreeNode buildNode(List intervals, float center) {
List left = null;
List right = null;
List byStart = null;
List byEnd = null;
float leftMin = 1E38f, leftMax = -1E38f, rightMin = 1E38f, rightMax = -1E38f;
float thisMin = 1E38f, thisMax = -1E38f;
for (int i = 0; i < intervals.size(); i++) {
Interval ival = (Interval) intervals.get(i);
if (ival.start < center && ival.end < center) {
if (left == null)
left = new ArrayList();
left.add(ival);
leftMin = leftMin < ival.start ? leftMin : ival.start;
leftMax = leftMax > ival.end ? leftMax : ival.end;
} else if (ival.start > center && ival.end > center) {
if (right == null)
right = new ArrayList();
right.add(ival);
rightMin = rightMin < ival.start ? rightMin : ival.start;
rightMax = rightMax > ival.end ? rightMax : ival.end;
} else {
if (byStart == null || byEnd == null) {
byStart = new ArrayList();
byEnd = new ArrayList();
}
thisMin = ival.start < thisMin ? ival.start : thisMin;
thisMax = ival.end > thisMax ? ival.end : thisMax;
byStart.add(ival);
byEnd.add(ival);
}
}
if (byStart != null) {
Collections.sort(byStart, byStartComparator);
Collections.sort(byEnd, byEndComparator);
}
IntervalTreeNode tree = new IntervalTreeNode();
tree.byBeginning = byStart;
tree.byEnding = byEnd;
tree.center = center;
if (left != null) {
tree.left = buildNode(left, (leftMin + leftMax) / 2.0f);
tree.left.childrenMinMax = leftMax;
}
if (right != null) {
tree.right = buildNode(right, (rightMin + rightMax) / 2.0f);
tree.right.childrenMinMax = rightMin;
}
return tree;
}
private void preprocess(int count, int[] polygons) {
int i, j = 0;
minX = minY = 1E38f;
maxX = maxY = -1E38f;
List intervals = new ArrayList(count);
int first = 0;
int currPoly = 0;
for (i = 1; i < count; i++) {
if (polygons != null && polygons.length > currPoly && polygons[currPoly] == i) {
/* New polygon starts. End the current. */
float prevy = verticesXY[2 * (i - 1) + 1];
float firsty = verticesXY[2 * first + 1];
Interval ival = new Interval();
ival.start = prevy < firsty ? prevy : firsty;
ival.end = firsty > prevy ? firsty : prevy;
ival.i = i - 1;
ival.j = first;
ival.polyIndex = currPoly;
intervals.add(ival);
first = i;
currPoly++;
i++;
j = i - 1;
}
float yi = verticesXY[2 * i + 1];
float xi = verticesXY[2 * i + 0];
float yj = verticesXY[2 * j + 1];
minX = xi < minX ? xi : minX;
minY = yi < minY ? yi : minY;
maxX = xi > maxX ? xi : maxX;
maxY = yi > maxY ? yi : maxY;
Interval ival = new Interval();
ival.start = yi < yj ? yi : yj;
ival.end = yj > yi ? yj : yi;
ival.i = i;
ival.j = j;
ival.polyIndex = currPoly;
intervals.add(ival);
j = i;
}
// Close current polygon
float yi = verticesXY[2 * (i - 1) + 1];
float xi = verticesXY[2 * (i - 1) + 0];
float yj = verticesXY[2 * first + 1];
minX = xi < minX ? xi : minX;
minY = yi < minY ? yi : minY;
maxX = xi > maxX ? xi : maxX;
maxY = yi > maxY ? yi : maxY;
Interval ival = new Interval();
ival.start = yi < yj ? yi : yj;
ival.end = yj > yi ? yj : yi;
ival.i = i - 1;
ival.j = first;
ival.polyIndex = currPoly;
intervals.add(ival);
// compute bounding sphere and rectangle
centerX = (maxX + minX) * 0.5f;
centerY = (maxY + minY) * 0.5f;
float dx = maxX - centerX;
float dy = maxY - centerY;
radiusSquared = dx * dx + dy * dy;
// build interval tree
tree = buildNode(intervals, centerY);
}
/**
* Test whether the given point (x, y)
lies inside any polygon stored in this {@link PolygonsIntersection} object.
*
* This method is thread-safe and can be used to test many points concurrently.
*
* In order to obtain the index of the polygon the point is inside of, use {@link #testPoint(float, float, BitSet)}
*
* @see #testPoint(float, float, BitSet)
*
* @param x
* the x coordinate of the point to test
* @param y
* the y coordinate of the point to test
* @return true
iff the point lies inside any polygon; false
otherwise
*/
public boolean testPoint(float x, float y) {
return testPoint(x, y, null);
}
/**
* Test whether the given point (x, y)
lies inside any polygon stored in this {@link PolygonsIntersection} object.
*
* This method is thread-safe and can be used to test many points concurrently.
*
* @param x
* the x coordinate of the point to test
* @param y
* the y coordinate of the point to test
* @param inPolys
* if not null
then the i-th bit is set if the given point is inside the i-th polygon
* @return true
iff the point lies inside the polygon and not inside a hole; false
otherwise
*/
public boolean testPoint(float x, float y, BitSet inPolys) {
// check bounding sphere first
float dx = (x - centerX);
float dy = (y - centerY);
if (inPolys != null)
inPolys.clear();
if (dx * dx + dy * dy > radiusSquared)
return false;
// check bounding box next
if (maxX < x || maxY < y || minX > x || minY > y)
return false;
// ask interval tree for all polygon edges intersecting 'y' and perform
// the even/odd/crosscutting/raycast algorithm on them and also return
// the polygon index of the polygon the point is in by setting the appropriate
// bit in the given BitSet.
boolean res = tree.traverse(verticesXY, x, y, false, inPolys);
return res;
}
}