blob: 49dfefa5ac2ec8c0561fedf03bd5003207f77ae6 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.lucene.geo;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.lucene.index.PointValues.Relation;
/**
* 2D polygon implementation represented as a balanced interval tree of edges.
* <p>
* Loosely based on the algorithm described in <a href="http://www-ma2.upc.es/geoc/Schirra-pointPolygon.pdf">
* http://www-ma2.upc.es/geoc/Schirra-pointPolygon.pdf</a>.
* @lucene.internal
*/
// Both Polygon.contains() and Polygon.crossesSlowly() loop all edges, and first check that the edge is within a range.
// we just organize the edges to do the same computations on the same subset of edges more efficiently.
public class Polygon2D extends EdgeTree {
// each component/hole is a node in an augmented 2d kd-tree: we alternate splitting between latitude/longitude,
// and pull up max values for both dimensions to each parent node (regardless of split).
/** tree of holes, or null */
protected final Polygon2D holes;
private final AtomicBoolean containsBoundary = new AtomicBoolean(false);
protected Polygon2D(final double minLat, final double maxLat, final double minLon, final double maxLon, double[] lats, double[] lons, Polygon2D holes) {
super(minLat, maxLat, minLon, maxLon, lats, lons);
this.holes = holes;
}
protected Polygon2D(Polygon polygon, Polygon2D holes) {
this(polygon.minLat, polygon.maxLat, polygon.minLon, polygon.maxLon, polygon.getPolyLats(), polygon.getPolyLons(), holes);
}
/**
* Returns true if the point is contained within this polygon.
* <p>
* See <a href="https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html">
* https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html</a> for more information.
*/
public boolean contains(double latitude, double longitude) {
if (latitude <= maxY && longitude <= maxX) {
if (componentContains(latitude, longitude)) {
return true;
}
if (left != null) {
if (((Polygon2D)left).contains(latitude, longitude)) {
return true;
}
}
if (right != null && ((splitX == false && latitude >= minLat) || (splitX && longitude >= minLon))) {
if (((Polygon2D)right).contains(latitude, longitude)) {
return true;
}
}
}
return false;
}
/** Returns true if the point is contained within this polygon component. */
private boolean componentContains(double latitude, double longitude) {
// check bounding box
if (latitude < minLat || latitude > maxLat || longitude < minLon || longitude > maxLon) {
return false;
}
containsBoundary.set(false);
if (contains(tree, latitude, longitude, containsBoundary)) {
if (holes != null && holes.contains(latitude, longitude)) {
return false;
}
return true;
}
return false;
}
@Override
protected Relation componentRelate(double minLat, double maxLat, double minLon, double maxLon) {
// check any holes
if (holes != null) {
Relation holeRelation = holes.relate(minLat, maxLat, minLon, maxLon);
if (holeRelation == Relation.CELL_CROSSES_QUERY) {
return Relation.CELL_CROSSES_QUERY;
} else if (holeRelation == Relation.CELL_INSIDE_QUERY) {
return Relation.CELL_OUTSIDE_QUERY;
}
}
// check each corner: if < 4 && > 0 are present, its cheaper than crossesSlowly
int numCorners = numberOfCorners(minLat, maxLat, minLon, maxLon);
if (numCorners == 4) {
if (tree.crossesBox(minLat, maxLat, minLon, maxLon, false)) {
return Relation.CELL_CROSSES_QUERY;
}
return Relation.CELL_INSIDE_QUERY;
} else if (numCorners == 0) {
if (minLat >= tree.lat1 && maxLat <= tree.lat1 && minLon >= tree.lon2 && maxLon <= tree.lon2) {
return Relation.CELL_CROSSES_QUERY;
}
if (tree.crossesBox(minLat, maxLat, minLon, maxLon, false)) {
return Relation.CELL_CROSSES_QUERY;
}
return Relation.CELL_OUTSIDE_QUERY;
}
return Relation.CELL_CROSSES_QUERY;
}
@Override
protected Relation componentRelateTriangle(double ax, double ay, double bx, double by, double cx, double cy) {
// check any holes
if (holes != null) {
Relation holeRelation = holes.relateTriangle(ax, ay, bx, by, cx, cy);
if (holeRelation == Relation.CELL_CROSSES_QUERY) {
return Relation.CELL_CROSSES_QUERY;
} else if (holeRelation == Relation.CELL_INSIDE_QUERY) {
return Relation.CELL_OUTSIDE_QUERY;
}
}
if (ax == bx && bx == cx && ay == by && by == cy) {
// indexed "triangle" is a point: shortcut by checking contains
return contains(ay, ax) ? Relation.CELL_INSIDE_QUERY : Relation.CELL_OUTSIDE_QUERY;
} else if ((ax == cx && ay == cy) || (bx == cx && by == cy)) {
// indexed "triangle" is a line segment: shortcut by calling appropriate method
return relateIndexedLineSegment(ax, ay, bx, by);
}
// indexed "triangle" is a triangle:
return relateIndexedTriangle(ax, ay, bx, by, cx, cy);
}
/** relates an indexed line segment (a "flat triangle") with the polygon */
private Relation relateIndexedLineSegment(double a2x, double a2y, double b2x, double b2y) {
// check endpoints of the line segment
int numCorners = 0;
if (componentContains(a2y, a2x)) {
++numCorners;
}
if (componentContains(b2y, b2x)) {
++numCorners;
}
if (numCorners == 2) {
if (tree.crossesLine(a2x, a2y, b2x, b2y)) {
return Relation.CELL_CROSSES_QUERY;
}
return Relation.CELL_INSIDE_QUERY;
} else if (numCorners == 0) {
if (tree.crossesLine(a2x, a2y, b2x, b2y)) {
return Relation.CELL_CROSSES_QUERY;
}
return Relation.CELL_OUTSIDE_QUERY;
}
return Relation.CELL_CROSSES_QUERY;
}
/** relates an indexed triangle with the polygon */
private Relation relateIndexedTriangle(double ax, double ay, double bx, double by, double cx, double cy) {
// check each corner: if < 3 && > 0 are present, its cheaper than crossesSlowly
int numCorners = numberOfTriangleCorners(ax, ay, bx, by, cx, cy);
if (numCorners == 3) {
if (tree.crossesTriangle(ax, ay, bx, by, cx, cy)) {
return Relation.CELL_CROSSES_QUERY;
}
return Relation.CELL_INSIDE_QUERY;
} else if (numCorners == 0) {
if (pointInTriangle(tree.lon1, tree.lat1, ax, ay, bx, by, cx, cy) == true) {
return Relation.CELL_CROSSES_QUERY;
}
if (tree.crossesTriangle(ax, ay, bx, by, cx, cy)) {
return Relation.CELL_CROSSES_QUERY;
}
return Relation.CELL_OUTSIDE_QUERY;
}
return Relation.CELL_CROSSES_QUERY;
}
private int numberOfTriangleCorners(double ax, double ay, double bx, double by, double cx, double cy) {
int containsCount = 0;
if (componentContains(ay, ax)) {
containsCount++;
}
if (componentContains(by, bx)) {
containsCount++;
}
if (containsCount == 1) {
return containsCount;
}
if (componentContains(cy, cx)) {
containsCount++;
}
return containsCount;
}
// returns 0, 4, or something in between
private int numberOfCorners(double minLat, double maxLat, double minLon, double maxLon) {
int containsCount = 0;
if (componentContains(minLat, minLon)) {
containsCount++;
}
if (componentContains(minLat, maxLon)) {
containsCount++;
}
if (containsCount == 1) {
return containsCount;
}
if (componentContains(maxLat, maxLon)) {
containsCount++;
}
if (containsCount == 2) {
return containsCount;
}
if (componentContains(maxLat, minLon)) {
containsCount++;
}
return containsCount;
}
/** Builds a Polygon2D from multipolygon */
public static Polygon2D create(Polygon... polygons) {
Polygon2D components[] = new Polygon2D[polygons.length];
for (int i = 0; i < components.length; i++) {
Polygon gon = polygons[i];
Polygon gonHoles[] = gon.getHoles();
Polygon2D holes = null;
if (gonHoles.length > 0) {
holes = create(gonHoles);
}
components[i] = new Polygon2D(gon, holes);
}
return (Polygon2D)createTree(components, 0, components.length - 1, false);
}
/**
* Returns true if the point crosses this edge subtree an odd number of times
* <p>
* See <a href="https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html">
* https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html</a> for more information.
*/
// ported to java from https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html
// original code under the BSD license (https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html#License%20to%20Use)
//
// Copyright (c) 1970-2003, Wm. Randolph Franklin
//
// 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:
//
// 1. Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimers.
// 2. Redistributions in binary form must reproduce the above copyright
// notice in the documentation and/or other materials provided with
// the distribution.
// 3. The name of W. Randolph Franklin may not be used to endorse or
// promote products derived from this Software without specific
// prior written permission.
//
// 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.
private static boolean contains(Edge edge, double lat, double lon, AtomicBoolean isOnEdge) {
boolean res = false;
if (isOnEdge.get() == false && lat <= edge.max) {
if (lat == edge.lat1 && lat == edge.lat2 ||
(lat <= edge.lat1 && lat >= edge.lat2) != (lat >= edge.lat1 && lat <= edge.lat2)) {
if ((lon == edge.lon1 && lon == edge.lon2) ||
((lon <= edge.lon1 && lon >= edge.lon2) != (lon >= edge.lon1 && lon <= edge.lon2) &&
GeoUtils.orient(edge.lon1, edge.lat1, edge.lon2, edge.lat2, lon, lat) == 0)) {
// if its on the boundary return true
isOnEdge.set(true);
return true;
} else if (edge.lat1 > lat != edge.lat2 > lat) {
res = lon < (edge.lon2 - edge.lon1) * (lat - edge.lat1) / (edge.lat2 - edge.lat1) + edge.lon1;
}
}
if (edge.left != null) {
res ^= contains(edge.left, lat, lon, isOnEdge);
}
if (edge.right != null && lat >= edge.low) {
res ^= contains(edge.right, lat, lon, isOnEdge);
}
}
return isOnEdge.get() || res;
}
}