/*
 * 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.

Loosely based on the algorithm described in http://www-ma2.upc.es/geoc/Schirra-pointPolygon.pdf.

@lucene.internal
/** * 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
/** 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.

See https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html for more information.

/** * 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.
/** 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
/** 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
/** 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
/** 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

See https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html for more information.

/** * 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; } }