# -*- coding:utf-8 -*- # ##### BEGIN LGPL LICENSE BLOCK ##### # GEOS - Geometry Engine Open Source # http://geos.osgeo.org # # Copyright (C) 2011 Sandro Santilli <strk@kbt.io> # Copyright (C) 2005 2006 Refractions Research Inc. # Copyright (C) 2001-2002 Vivid Solutions Inc. # Copyright (C) 1995 Olivier Devillers <Olivier.Devillers@sophia.inria.fr> # # This is free software you can redistribute and/or modify it under # the terms of the GNU Lesser General Public Licence as published # by the Free Software Foundation. # See the COPYING file for more information. # # ##### END LGPL LICENSE BLOCK ##### # <pep8 compliant> # ---------------------------------------------------------- # Partial port (version 3.7.0) by: Stephen Leger (s-leger) # # ---------------------------------------------------------- import logging logger = logging.getLogger("pygeos.algorithms") from math import floor, isfinite, sqrt, pi, atan2 from .shared import ( quicksort, GeomTypeId, Location, Envelope, Quadrant, Coordinate, CoordinateSequence, LinearComponentExtracter, CoordinateFilter ) from .index_bintree import ( Bintree, Interval ) from .index_intervaltree import ( SortedPackedIntervalRTree ) # index EPSILON_SINGLE = 1e-5 EPSILON = 1e-12 EPSILON_SQUARE = EPSILON * EPSILON USE_FUZZY_LINE_INTERSECTOR = False class ItemVisitor(): """ * A visitor for items in an index. """ def visitItem(self) -> None: raise NotImplementedError() # index/chain class MonotoneChain(): """ * Monotone Chains are a way of partitioning the segments of a linestring to * allow for fast searching of intersections. * * They have the following properties: * * - the segments within a monotone chain never intersect each other * - the envelope of any contiguous subset of the segments in a monotone * chain is equal to the envelope of the endpoints of the subset. * * Property 1 means that there is no need to test pairs of segments from * within the same monotone chain for intersection. * Property 2 allows an efficient binary search to be used to find the * intersection points of two monotone chains. * * For many types of real-world data, these properties eliminate * a large number of segment comparisons, producing substantial speed gains. * * One of the goals of this implementation of MonotoneChains is to be * as space and time efficient as possible. One design choice that aids this * is that a MonotoneChain is based on a subarray of a list of points. * This means that new arrays of points (potentially very large) do not * have to be allocated. * * MonotoneChains support the following kinds of queries: * * - Envelope select: determine all the segments in the chain which * intersect a given envelope * - Overlap: determine all the pairs of segments in two chains whose * envelopes overlap * * This implementation of MonotoneChains uses the concept of internal iterators * to return the resultsets for the above queries. * This has time and space advantages, since it * is not necessary to build lists of instantiated objects to represent the segments * returned by the query. * However, it does mean that the queries are not thread-safe. """ def __init__(self, coords, start, end, context): self.coords = coords self._env = None self.context = context self.start = start self.end = end self.id = -1 @property def envelope(self): if self._env is None: self._env = Envelope( self.coords[self.start], self.coords[self.end]) return self._env def getLineSegment(self, index: int, ls) -> None: """ * Set given LineSegment with points of the segment starting * at the given index. """ ls.p0 = self.coords[index] ls.p1 = self.coords[index + 1] def select(self, searchEnv, mcs) -> None: self.computeSelect(searchEnv, self.start, self.end, mcs) def _computeSelect(self, searchEnv, start0: int, end0: int, mcs) -> None: p0 = self.coords[start0] p1 = self.coords[end0] mcs.tempEnv1.__init__(p0, p1) # terminating condition for the recursion if end0 - start0 == 1: mcs.select(self, start0) return # nothing to do if the envelopes don't overlap if not searchEnv.intersects(mcs.tempEnv1): return # the chains overlap,so split each in half and iterate (binary search) mid = int((start0 + end0) / 2) if start0 < mid: self._computeSelect(searchEnv, start0, mid, mcs) if mid < end0: self._computeSelect(searchEnv, mid, end0, mcs) def computeOverlaps(self, mc, mco) -> None: self._computeOverlaps(self.start, self.end, mc, mc.start, mc.end, mco) def _computeOverlaps(self, start0: int, end0: int, mc, start1: int, end1: int, mco) -> None: # terminating condition for the recursion if end0 - start0 == 1 and end1 - start1 == 1: mco.overlap(self, start0, mc, start1) return p1 = self.coords[start0] p2 = self.coords[end0] q1 = mc.coords[start1] q2 = mc.coords[end1] # nothing to do if the envelopes of these chains don't overlap # MonotoneChainOverlapAction mco.tempEnv1.__init__(p1, p2) mco.tempEnv2.__init__(q1, q2) if not mco.tempEnv1.intersects(mco.tempEnv2): return mid0 = int((start0 + end0) / 2) mid1 = int((start1 + end1) / 2) if start0 < mid0: if start1 < mid1: self._computeOverlaps(start0, mid0, mc, start1, mid1, mco) if mid1 < end1: self._computeOverlaps(start0, mid0, mc, mid1, end1, mco) if mid0 < end0: if start1 < mid1: self._computeOverlaps(mid0, end0, mc, start1, mid1, mco) if mid1 < end1: self._computeOverlaps(mid0, end0, mc, mid1, end1, mco) def __str__(self): return "MonotonChain {} {}-{}".format(self.id, self.start, self.end) class MonotoneChainBuilder(): """ * Constructs {MonotoneChain}s * for sequences of {Coordinate}s. """ @staticmethod def getChains(coords, context=None, mcList=[]) -> list: """ * Fill the provided vector with newly-allocated MonotoneChain objects * for the given CoordinateSequence. """ startIndex = [] MonotoneChainBuilder.getChainStartIndices(coords, startIndex) n = len(startIndex) if n > 0: for i in range(n - 1): mc = MonotoneChain(coords, startIndex[i], startIndex[i + 1], context) mcList.append(mc) logger.debug("MonotoneChainBuilder.getChains(%s) for coords:%s\n%s", len(mcList), coords, "\n".join([str(mc) for mc in mcList])) return mcList @staticmethod def getChainStartIndices(coords, startIndexList: list) -> None: """ * Fill the given vector with start/end indexes of the monotone chains * for the given CoordinateSequence. * The last entry in the array points to the end point of the point * array, * for use as a sentinel. """ start = 0 startIndexList.append(start) n = len(coords) - 1 while (True): last = MonotoneChainBuilder._findChainEnd(coords, start) startIndexList.append(last) start = last if start >= n: break @staticmethod def _findChainEnd(coords, start: int) -> int: """ * Finds the index of the last point in a monotone chain * starting at a given point. * Any repeated points (0-length segments) will be included * in the monotone chain returned. * * @return the index of the last point in the monotone chain * starting at start. * * NOTE: aborts if 'start' is >= pts.getSize() """ safeStart = start npts = len(coords) # skip any zero-length segments at the start of the sequence # (since they cannot be used to establish a quadrant) while (safeStart < npts - 1 and coords[safeStart] == coords[safeStart + 1]): safeStart += 1 # check if there are NO non-zero-length segments if safeStart >= npts - 1: return npts - 1 # determine overall quadrant for chain chainQuad = Quadrant.from_coords(coords[safeStart], coords[safeStart + 1]) last = start + 1 while last < npts: if coords[last - 1] != coords[last]: quad = Quadrant.from_coords(coords[last - 1], coords[last]) # logger.debug("MonotoneChainBuilder._findChainEnd() Quadrants: %s %s", chainQuad, quad) if quad != chainQuad: break last += 1 return last - 1 class MonotoneChainSelectAction(): def __init__(self): # geom.LineSegment self._selectedSegment = LineSegment() # these envelopes are used during the MonotoneChain search process self.tempEnv1 = Envelope() def select(self, mc, start: int) -> None: mc.getLineSegment(start, self._selectedSegment) self._select(self._selectedSegment) def _select(self, seg) -> None: """ * This is a convenience function which can be overridden * to obtain the actual line segment which is selected """ pass class MonotoneChainOverlapAction(): """ * The action for the internal iterator for performing * overlap queries on a MonotoneChain """ def __init__(self): # geom.LineSegment self.overlapSeg1 = LineSegment() self.overlapSeg2 = LineSegment() # these envelopes are used during the MonotoneChain search process self.tempEnv1 = Envelope() self.tempEnv2 = Envelope() def overlap(self, mc1, start1: int, mc2, start2: int) -> None: """ * This function can be overridden if the original chains are needed * * @param start1 the index of the start of the overlapping segment * from mc1 * @param start2 the index of the start of the overlapping segment * from mc2 """ mc1.getLineSegment(start1, self.overlapSeg1) mc2.getLineSegment(start2, self.overlapSeg2) self._overlap(self.overlapSeg1, self.overlapSeg2) def _overlap(self, seg1, seg2) -> None: """ * This is a convenience function which can be overridden to * obtain the actual line segments which overlap * @param seg1 * @param seg2 """ pass class MonotoneChainIndexer(): def getChainStartIndices(self, coords, startIndexList): start = 0 startIndexList.append(start) while True: last = self._findChainEnd(coords, start) startIndexList.append(last) start = last if start >= len(coords) - 1: break def _findChainEnd(self, coords, start): """ * @return the index of the last point in the monotone chain """ chainQuad = Quadrant.from_coords(coords[start], coords[start + 1]) last = start + 1 while last < len(coords): quad = Quadrant.from_coords(coords[last - 1], coords[last]) if quad != chainQuad: break last += 1 return last - 1 class LineSegment(): """ * A line segment """ def __init__(self, x0=None, y0=None, x1=None, y1=None): if x0 is None: x0, y0, x1, y1 = 0, 0, 0, 0 elif y0 is None: y1 = x0.p1.y x1 = x0.p1.x y0 = x0.p0.y x0 = x0.p0.x elif x1 is None: y1 = y0.y x1 = y0.x y0 = x0.y x0 = x0.x self.p0 = Coordinate(x0, y0) self.p1 = Coordinate(x1, y1) def setCoordinates(self, p0, p1): self.p0.x = p0.x self.p0.y = p0.y self.p1.x = p1.x self.p1.y = p1.y @property def length(self): return CGAlgorithms.distancePointPoint(self.p0, self.p1) def distance(self, other) -> float: if type(other).__name__ == 'LineSegment': return CGAlgorithms.distanceLineLine(self.p0, self.p1, other.p0, other.p1) else: return CGAlgorithms.distancePointLine(other, self.p0, self.p1) def projectionFactor(self, p) -> float: if p == self.p0: return 0.0 if p == self.p1: return 1.0 # Otherwise, use comp.graphics.algorithms Frequently Asked Questions method """ (1) AC dot AB r = --------- ||AB||^2 r has the following meaning: r=0 P = A r=1 P = B r<0 P is on the backward extension of AB r>1 P is on the forward extension of AB 0<r<1 P is interiors to AB """ p0, p1 = self.p0, self.p1 dx = p1.x - p0.x dy = p1.y - p0.y len2 = dx * dx + dy * dy r = ((p.x - p0.x) * dx + (p.y - p0.y) * dy) / len2 return r def orientationIndex(self, seg) -> int: orient0 = CGAlgorithms.orientationIndex(self.p0, self.p1, seg.p0) orient1 = CGAlgorithms.orientationIndex(self.p0, self.p1, seg.p1) # this handles the case where the points are L or collinear if orient0 >= 0 and orient1 >= 0: return max(orient0, orient1) # this handles the case where the points are R or collinear if orient0 <= 0 and orient1 <= 0: return max(orient0, orient1) # points lie on opposite sides ==> indeterminate orientation return 0 def pointAlongOffset(self, t: float, offsetDistance: float, res) -> None: dx = self.p1.x - self.p0.x dy = self.p1.y - self.p0.y # the point on the segment line segx = self.p0.x + t * dx segy = self.p0.y + t * dy length = sqrt(dx * dx + dy * dy) ux = 0.0 uy = 0.0 if offsetDistance != 0.0: if length <= 0.0: raise ValueError("Cannot compute offset from zero-length line segment") # u is the vector that is the length of the offset, # in the direction of the segment ux = offsetDistance * dx / length uy = offsetDistance * dy / length # the offset point is the seg point plus the offset # vector rotated 90 degrees CCW res.x = segx - uy res.y = segy + ux def __str__(self): return "{} - {}".format(self.p0, self.p1) class UniqueCoordinateArrayFilter(CoordinateFilter): """ * A CoordinateFilter that fills a vector of Coordinate const pointers. * The set of coordinates contains no duplicate points. """ def __init__(self, target): self._unique = {} self.pts = target def filter_ro(self, coord): if self._unique.get(coord) is None: self._unique[coord] = coord self.pts.append(coord) class ReallyLessThen(): def __init__(self, origin): self.origin = origin def compare(self, p1, p2) -> bool: return self.polarCompare(self.origin, p1, p2) == -1 def polarCompare(self, o, p, q) -> int: dxp = p.x - o.x dyp = p.y - o.y dxq = q.x - o.x dyq = q.y - o.y orient = CGAlgorithms.computeOrientation(o, p, q) if orient == CGAlgorithms.COUNTERCLOCKWISE: return 1 elif orient == CGAlgorithms.CLOCKWISE: return -1 op = dxp * dxp + dyp * dyp oq = dxq * dxq + dyq * dyq if op < oq: return -1 elif op > oq: return 1 return 0 # algorithms class Angle(): """ * Utility functions for working with angles. * Unless otherwise noted, methods in this class express angles in radians. """ PI_TIMES_2 = 2.0 * pi PI_OVER_2 = pi / 2.0 PI_OVER_4 = pi / 4.0 @staticmethod def angle(p0, p1=None) -> float: if p1 is None: x = p0.x y = p0.y else: x = p1.x - p0.x y = p1.y - p0.y return atan2(y, x) @staticmethod def angleBetween(p0, p1, p2) -> float: """ * Returns the angle between two vectors. * * The computed angle will be in the range (-Pi, Pi]. * A positive result corresponds to a counterclockwise rotation * from v1 to v2; * a negative result corresponds to a clockwise rotation. * @param tip1 the tip of v1 * @param tail the tail of each vector * @param tip2 the tip of v2 * @return the angle between v1 and v2, relative to v1 """ a1 = Angle.angle(p1, p0) a2 = Angle.angle(p1, p2) return Angle.diff(a1, a2) @staticmethod def angleBetweenOriented(p0, p1, p2) -> float: """ * Returns the oriented smallest angle between two vectors. * * The computed angle will be in the range (-Pi, Pi]. * A positive result corresponds to a counterclockwise rotation * from v1 to v2; * a negative result corresponds to a clockwise rotation. * @param tip1 the tip of v1 * @param tail the tail of each vector * @param tip2 the tip of v2 * @return the angle between v1 and v2, relative to v1 """ a1 = Angle.angle(p1, p0) a2 = Angle.angle(p1, p2) delta = a2 - a1 if delta <= -pi: return delta + Angle.PI_TIMES_2 if delta > pi: return delta - Angle.PI_TIMES_2 return delta @staticmethod def normalize(angle: float) -> float: """ * Computes the normalized value of an angle, which is the * equivalent angle in the range ( -Pi, Pi ]. * @param angle the angle to normalize * @return an equivalent angle in the range (-Pi, Pi] """ while angle > pi: angle -= Angle.PI_TIMES_2 while angle <= -pi: angle += Angle.PI_TIMES_2 return angle @staticmethod def normalizePositive(angle: float) -> float: """ * Computes the normalized value of an angle, which is the * equivalent angle in the range ( -Pi, Pi ]. * @param angle the angle to normalize * @return an equivalent angle in the range (-Pi, Pi] """ if angle < 0.0: while angle < 0.0: angle += Angle.PI_TIMES_2 if angle >= Angle.PI_TIMES_2: angle = 0.0 else: while angle >= Angle.PI_TIMES_2: angle -= Angle.PI_TIMES_2 if angle < 0.0: angle = 0.0 return angle @staticmethod def diff(ang1: float, ang2: float) -> float: if ang1 < ang2: delta = ang2 - ang1 else: delta = ang1 - ang2 if delta > pi: delta = Angle.PI_TIMES_2 - delta return delta class ConvexHull(): """ * Computes the convex hull of a Geometry. * * The convex hull is the smallest convex Geometry that contains all the * points in the input Geometry. * * Uses the Graham Scan algorithm. """ def __init__(self, geom): self._factory = geom._factory self.inputCoords = [] self.extractCoordinates(geom) def toCoordinateSequence(self, coords): """ * Create a CoordinateSequence from the Coordinate.ConstVect * This is needed to construct the geometries. * Here coordinate copies happen """ csf = self._factory.coordinateSequenceFactory return csf.create(coords) def extractCoordinates(self, geom) -> None: filter = UniqueCoordinateArrayFilter(self.inputCoords) geom.apply_ro(filter) def computeOctPts(self, src, tgt) -> None: # Initialize all slots with first input coordinate tgt.clear() tgt.extend([src[0] for i in range(8)]) for coord in src: if coord.x < tgt[0].x: tgt[0] = coord if coord.x - coord.y < tgt[1].x - tgt[1].y: tgt[1] = coord if coord.y > tgt[2].y: tgt[2] = coord if coord.x + coord.y > tgt[3].x + tgt[3].y: tgt[3] = coord if coord.x > tgt[4].x: tgt[4] = coord if coord.x - coord.y > tgt[5].x - tgt[5].y: tgt[5] = coord if coord.y < tgt[6].y: tgt[6] = coord if coord.x + coord.y < tgt[7].x + tgt[7].y: tgt[7] = coord def computeOctRing(self, src, tgt) -> bool: self.computeOctPts(src, tgt) # Remove consecutive equal Coordinates tmp = [co for i, co in enumerate(tgt) if i == 0 or tgt[i - 1] is not co] tgt.clear() tgt.extend(tmp) logger.debug("ConvexHull.computeOctRing() %s", [str(co) for co in tgt]) if len(tgt) < 3: return False # close ring tgt.append(tgt[0]) return True def reduce(self, coords) -> None: """ * Uses a heuristic to reduce the number of points scanned * to compute the hull. * The heuristic is to find a polygon guaranteed to * be in (or on) the hull, and eliminate all points inside it. * A quadrilateral defined by the extremal points * in the four orthogonal directions * can be used, but even more inclusive is * to use an octilateral defined by the points in the * 8 cardinal directions. * * Note that even if the method used to determine the polygon * vertices is not 100% robust, this does not affect the * robustness of the convex hull. * * To satisfy the requirements of the Graham Scan algorithm, * the resulting array has at least 3 entries. * * @param pts The vector of const Coordinate pointers * to be reduced (to at least 3 elements) * * WARNING: the parameter will be modified """ polyPts = [] if not self.computeOctRing(coords, polyPts): # unable to compute interiors polygon for some reason return # add points defining polygon reducedSet = [polyPts[0], polyPts[-1]] """ * Add all unique points not in the interiors poly. * CGAlgorithms.isPointInRing is not defined for points * actually on the ring, but this doesn't matter since * the points of the interiors polygon are forced to be * in the reduced set. """ for p in coords: if not CGAlgorithms.isPointInRing(p, polyPts): reducedSet.append(p) self.inputCoords.clear() self.inputCoords.extend(reducedSet) if len(self.inputCoords) < 3: self.padArray3(self.inputCoords) def padArray3(self, coords) -> None: for i in range(len(coords), 3): coords.append(coords[0]) def preSort(self, coords) -> None: # find the lowest point in the set. If two or more points have # the same minimum y coordinate choose the one with the minimum x. # This focal point is put in array location pts[0]. for i, p1 in enumerate(coords): p0 = coords[0] if p1.y < p0.y or (p1.y == p0.y and p1.x < p0.x): coords[0], coords[i] = p1, p0 logger.debug("ConvexHull.preSort() before radial sort: %s", [str(co) for co in coords]) # sort the points radially around the focal point. rls = ReallyLessThen(coords[0]) quicksort(coords, rls.compare) logger.debug("ConvexHull.preSort() after radial sort: %s", [str(co) for co in coords]) def grahamScan(self, coords, res) -> None: res.extend(coords[0:3]) for i in range(3, len(coords)): p = res.pop() while (not len(res) == 0) and CGAlgorithms.computeOrientation(res[-1], p, coords[i]) > 0: p = res.pop() res.append(p) res.append(coords[i]) res.append(coords[0]) logger.debug("ConvexHull.grahamScan() %s", [str(co) for co in res]) def lineOrPolygon(self, coords): """ * @param vertices the vertices of a linear ring, * which may or may not be * flattened (i.e. vertices collinear) * * @return a 2-vertex LineString if the vertices are * collinear; otherwise, a Polygon with unnecessary * (collinear) vertices removed """ cleaned = [] self.cleanRing(coords, cleaned) if len(cleaned) == 3: cleaned = cleaned[0:2] cs = self.toCoordinateSequence(cleaned) return self._factory.createLineString(cs) cs = self.toCoordinateSequence(cleaned) lr = self._factory.createLinearRing(cs) return self._factory.createPolygon(lr, None) def cleanRing(self, coords, cleaned) -> None: """ * Write in 'cleaned' a version of 'input' with collinear * vertexes removed. """ npts = len(coords) logger.debug("ConvexHull.cleanRing() before: %s %s", npts, [str(co) for co in coords]) last = coords[-1] prev = None for i in range(npts - 1): curr = coords[i] next = coords[i + 1] # skip consecutive equal coords if curr == next: continue if prev is not None and self.isBetween(prev, curr, next): continue cleaned.append(curr) prev = curr cleaned.append(last) logger.debug("ConvexHull.cleanRing() after: %s %s", len(cleaned), [str(co) for co in cleaned]) def isBetween(self, c1, c2, c3) -> bool: """ * @return whether the three coordinates are collinear * and c2 lies between c1 and c3 inclusive """ if CGAlgorithms.computeOrientation(c1, c2, c3) != 0: return False if c1.x != c3.x: if c1.x <= c2.x <= c3.x: return True if c3.x <= c2.x <= c1.x: return True if c1.y != c3.y: if c1.y <= c2.y <= c3.y: return True if c3.y <= c2.y <= c1.y: return True return False def getConvexHull(self): """ * Returns a Geometry that represents the convex hull of * the input geometry. * The returned geometry contains the minimal number of points * needed to represent the convex hull. * In particular, no more than two consecutive points * will be collinear. * * @return if the convex hull contains 3 or more points, * a Polygon; 2 points, a LineString; * 1 point, a Point; 0 points, an empty GeometryCollection. """ nInputPts = len(self.inputCoords) logger.debug("ConvexHull.getConvexHull() %s", [str(co) for co in self.inputCoords]) if nInputPts == 0: return self._factory.createEmptyGeometry() if nInputPts == 1: # Return a point return self._factory.createPoint(self.inputCoords[0]) if nInputPts == 2: # return a LineString cs = self.toCoordinateSequence(self.inputCoords) return self._factory.createLineString(cs) # use heuristic to reduce points if large if nInputPts > 50: self.reduce(self.inputCoords) # Sort points for Graham scan self.preSort(self.inputCoords) # Use Graham scan to find convex hull cHs = [] self.grahamScan(self.inputCoords, cHs) return self.lineOrPolygon(cHs) class PointLocator(): """ * Computes the topological relationship (Location) * of a single point to a Geometry. * * The algorithm obeys the SFS boundaryDetermination rule to correctly determine * whether the point lies on the boundary or not. * * Notes: * - instances of this class are not reentrant. * - LinearRing objects do not enclose any area * points inside the ring are still in the EXTERIOR of the ring. """ def __init__(self): # true if the point lies in or on any Geometry element self._isIn = False # the number of sub-elements whose boundaries the point lies in self.numBoundaries = 0 def locate(self, coord, geom): """ * Computes the topological relationship (Location) of a single point * to a Geometry. * It handles both single-element * and multi-element Geometries. * The algorithm for multi-part Geometries * takes into account the boundaryDetermination rule. * * @return the Location of the point relative to the input Geometry """ if geom.is_empty: return Location.EXTERIOR self._isIn = False self._numBoundaries = 0 self._computeLocation(coord, geom) if self._numBoundaries % 2 == 1: return Location.BOUNDARY if self._numBoundaries > 0 or self._isIn: return Location.INTERIOR return Location.EXTERIOR def intersects(self, coord, geom): """ * Convenience method to test a point for intersection with * a Geometry * * @param p the coordinate to test * @param geom the Geometry to test * @return true if the point is in the interiors or boundary of the Geometry """ return self.locate(coord, geom) != Location.EXTERIOR def _locateInPoint(self, coord, geom): if geom.coord == coord: return Location.INTERIOR return Location.EXTERIOR def _locateInLineString(self, coord, geom): coords = geom.coords if not geom.isClosed: if coord == coords[0] or coord == coords[-1]: return Location.BOUNDARY if CGAlgorithms.isOnLine(coord, coords): return Location.INTERIOR return Location.EXTERIOR def _locateInLinearRing(self, coord, ring): coords = ring.coords if CGAlgorithms.isOnLine(coord, coords): return Location.BOUNDARY if CGAlgorithms.isPointInRing(coord, coords): return Location.INTERIOR return Location.EXTERIOR def _locateInPolygon(self, coord, geom): exterior = geom.exterior exteriorLoc = self._locateInLinearRing(coord, exterior) if exteriorLoc == Location.EXTERIOR: return Location.EXTERIOR if exteriorLoc == Location.BOUNDARY: return Location.BOUNDARY for hole in geom.interiors: holeLoc = self._locateInLinearRing(coord, hole) if holeLoc == Location.INTERIOR: return Location.EXTERIOR if holeLoc == Location.BOUNDARY: return Location.BOUNDARY return Location.INTERIOR def _computeLocation(self, coord, geom): type_id = geom.type_id if type_id == GeomTypeId.GEOS_POINT: loc = self._locateInPoint(coord, geom) self._updateLocationInfo(loc) elif type_id == GeomTypeId.GEOS_LINESTRING: loc = self._locateInLineString(coord, geom) self._updateLocationInfo(loc) elif type_id == GeomTypeId.GEOS_LINEARRING: loc = self._locateInLinearRing(coord, geom) self._updateLocationInfo(loc) elif type_id == GeomTypeId.GEOS_POLYGON: loc = self._locateInPolygon(coord, geom) self._updateLocationInfo(loc) elif type_id in [ GeomTypeId.GEOS_MULTIPOINT, GeomTypeId.GEOS_MULTILINESTRING, GeomTypeId.GEOS_MULTIPOLYGON, GeomTypeId.GEOS_GEOMETRYCOLLECTION ]: for g in geom.geoms: self._computeLocation(coord, g) def _updateLocationInfo(self, loc): if loc == Location.INTERIOR: self._isIn = True if loc == Location.BOUNDARY: self._numBoundaries += 1 class HCoordinate(): @staticmethod def intersection(p1, p2, q1, q2, ret): px = p1.y - p2.y py = p2.x - p1.x pw = p1.x * p2.y - p2.x * p1.y qx = q1.y - q2.y qy = q2.x - q1.x qw = q1.x * q2.y - q2.x * q1.y x = py * qw - qy * pw y = qx * pw - px * qw w = px * qy - qx * py xInt = x / w yInt = y / w if not isfinite(xInt) or not isfinite(yInt): raise ArithmeticError() ret.x, ret.y = xInt, yInt class RobustDeterminant(): """ * RobustDeterminant implements an algorithm to compute the * sign of a 2x2 determinant for double precision values robustly. * It is a direct translation of code developed by Olivier Devillers. * * The original code carries the following copyright notice: * * Author : Olivier Devillers * Olivier.Devillers@sophia.inria.fr * http://www-sop.inria.fr/prisme/logiciel/determinant.html * * Olivier Devillers has allowed the code to be distributed under * the LGPL (2012-02-16) saying "It is ok for LGPL distribution." """ @staticmethod def signOfDet2x2(x1: float, y1: float, x2: float, y2: float) -> int: """ returns -1 if the determinant is negative, returns 1 if the determinant is positive, retunrs 0 if the determinant is null. """ sign = 1 # testing null entries if x1 == 0.0 or y2 == 0.0: if y1 == 0.0 or x2 == 0.0: return 0 elif y1 > 0: if x2 > 0: return -sign else: return sign else: if x2 > 0: return sign else: return -sign if y1 == 0.0 or x2 == 0.0: if y2 > 0: if x1 > 0: return sign else: return -sign else: if x1 > 0: return -sign else: return sign # making y coordinates positive and permuting the entries # so that y2 is the biggest one if 0.0 < y1: if 0.0 < y2: if y1 > y2: sign = -sign x1, x2 = x2, x1 y1, y2 = y2, y1 else: if y1 <= -y2: sign = -sign x2 = -x2 y2 = -y2 else: x1, x2 = -x2, x1 y1, y2 = -y2, y1 else: if 0.0 < y2: if -y1 <= y2: sign = -sign x1 = -x1 y1 = -y1 else: x1, x2 = x2, -x1 y1, y2 = y2, -y1 else: if y1 >= y2: x1, x2 = -x1, -x2 y1, y2 = -y1, -y2 else: sign = -sign x1, x2 = -x2, -x1 y1, y2 = -y2, -y1 # making x coordinates positive # if |x2|<|x1| one can conclude if 0.0 < x1: if 0.0 < x2: if x1 > x2: return sign else: return sign else: if 0.0 < x2: return -sign else: if x1 >= x2: sign = -sign x1 = -x1 x2 = -x2 else: return -sign # all entries strictly positive x1 <= x2 and y1 <= y2 while (True): k = floor(x2 / x1) x2 = x2 - k * x1 y2 = y2 - k * y1 # testing if R (new U2) is in U1 rectangle if y2 < 0.0: return -sign if y2 > y1: return sign # finding R' if x1 > x2 + x2: if y1 < y2 + y2: return sign else: if y1 > y2 + y2: return -sign else: x2 = x1 - x2 y2 = y1 - y2 sign = -sign if y2 == 0.0: if x2 == 0.0: return 0 else: return -sign if x2 == 0.0: return sign # exchange 1 and 2 role. k = floor(x1 / x2) x1 = x1 - k * x2 y1 = y1 - k * y2 # testing if R (new U1) is in U2 rectangle if y1 < 0.0: return sign if y1 > y2: return -sign # finding R' if x2 > x1 + x1: if y2 < y1 + y1: return -sign else: if y2 > y1 + y1: return sign else: x1 = x2 - x1 y1 = y2 - y1 sign = -sign if y1 == 0.0: if x1 == 0.0: return 0 else: return sign if x1 == 0.0: return -sign class RayCrossingCounter(): """ * Counts the number of segments crossed by a horizontal ray extending to the right * from a given point, in an incremental fashion. * * This can be used to determine whether a point lies in a {Polygonal} geometry. * The class determines the situation where the point lies exactly on a segment. * When being used for Point-In-Polygon determination, this case allows short-circuiting * the evaluation. * * This class handles polygonal geometries with any number of exteriors and interiors. * The orientation of the exterior and hole rings is unimportant. * In order to compute a correct location for a given polygonal geometry, * it is essential that <b>all</b> segments are counted which * <ul> * <li>touch the ray * <li>lie in in any ring which may contain the point * </ul> * The only exception is when the point-on-segment situation is detected, in which * case no further processing is required. * The implication of the above rule is that segments * which can be a priori determined to <i>not</i> touch the ray * (i.e. by a test of their bounding box or Y-extent) * do not need to be counted. This allows for optimization by indexing. * * @author Martin Davis """ def __init__(self, point): self._point = point self._crossingCount = 0 self._isPointOnSegment = False @staticmethod def locatePointInRing(p, ring): """ * Determines the {Location} of a point in a ring. * This method is an exemplar of how to use this class. * * @param p the point to test * @param ring an array of Coordinates forming a ring * @return the location of the point in the ring """ rcc = RayCrossingCounter(p) for i in range(1, len(ring)): p1 = ring[i - 1] p2 = ring[i] rcc.countSegment(p1, p2) if rcc._isPointOnSegment: return rcc.location return rcc.location @staticmethod def orientationIndex(p1, p2, q) -> int: """ * Returns the index of the direction of the point q * relative to a vector specified by p1-p2. * * @param p1 the origin point of the vector * @param p2 the final point of the vector * @param q the point to compute the direction to * * @return 1 if q is counter-clockwise (left) from p1-p2 * @return -1 if q is clockwise (right) from p1-p2 * @return 0 if q is collinear with p1-p2 """ dx1 = p2.x - p1.x dy1 = p2.y - p1.y dx2 = q.x - p2.x dy2 = q.y - p2.y return RobustDeterminant.signOfDet2x2(dx1, dy1, dx2, dy2) def countSegment(self, p1, p2): """ * Counts a segment * * @param p1 an endpoint of the segment * @param p2 another endpoint of the segment """ # For each segment, check if it crosses # a horizontal ray running from the test point in # the positive x direction. point = self._point # check if the segment is strictly to the left of the test point if p1.x < point.x and p2.x < point.x: return # check if the point is equal to the current ring vertex if p2 == point: self._isPointOnSegment = True return # For horizontal segments, check if the point is on the segment. # Otherwise, horizontal segments are not counted. if p1.y == point.y and p2.y == point.y: if p1.x > p2.x: minx, maxx = p2.x, p1.x else: minx, maxx = p1.x, p2.x if maxx >= point.x >= minx: self._isPointOnSegment = True return # Evaluate all non-horizontal segments which cross a horizontal ray # to the right of the test pt. # To avoid double-counting shared vertices, we use the convention that # - an upward edge includes its starting endpoint, and excludes its # final endpoint # - a downward edge excludes its starting endpoint, and includes its # final endpoint if (p1.y > point.y and p2.y <= point.y) or (p2.y > point.y and p1.y <= point.y): # For an upward edge, orientationIndex will be positive when p1->p2 # crosses ray. Conversely, downward edges should have negative sign. sign = RayCrossingCounter.orientationIndex(p1, p2, point) if sign == 0: self._isPointOnSegment = True return if p2.y < p1.y: sign = -sign # The segment crosses the ray if the sign is strictly positive. if sign > 0: self._crossingCount += 1 @property def location(self): """ * Gets the {Location} of the point relative to * the ring, polygon * or multipolygon from which the processed segments were provided. * <p> * This method only determines the correct location * if <b>all</b> relevant segments have been processed. * * @return the Location of the point """ if self._isPointOnSegment: return Location.BOUNDARY # The point is in the interiors of the ring if the number # of X-crossings is odd. if (self._crossingCount % 2) == 1: return Location.INTERIOR return Location.EXTERIOR def isPointInPolygon(self): """ * Tests whether the point lies in or on * the ring, polygon * or multipolygon from which the processed segments were provided. * <p> * This method only determines the correct location * if <b>all</b> relevant segments must have been processed. * * @return true if the point lies in or on the supplied polygon """ return self.location != Location.EXTERIOR class CGAlgorithms(): # shared CLOCKWISE = -1 COLLINEAR = 0 COUNTERCLOCKWISE = 1 RIGHT = -1 LEFT = 0 STRAIGHT = 1 @staticmethod def isOnLine(p, coords): if len(coords) == 0: return False p0 = coords[0] for i in range(1, len(coords)): p1 = coords[i] if LineIntersector._hasIntersection(p, p0, p1): return True p0 = p1 return False @staticmethod def orientationIndex(p1, p2, q): """ * Returns the index of the direction of the point q * relative to a vector specified by p1-p2. * * @param p1 the origin point of the vector * @param p2 the final point of the vector * @param q the point to compute the direction to * * @return 1 if q is counter-clockwise (left) from p1-p2 * @return -1 if q is clockwise (right) from p1-p2 * @return 0 if q is collinear with p1-p2 """ return RayCrossingCounter.orientationIndex(p1, p2, q) @staticmethod def computeOrientation(p1, p2, q): """ * Computes the orientation of a point q to the directed line * segment p1-p2. * * The orientation of a point relative to a directed line * segment indicates which way you turn to get to q after * travelling from p1 to p2. * * @return 1 if q is counter-clockwise from p1-p2 * @return -1 if q is clockwise from p1-p2 * @return 0 if q is collinear with p1-p2 """ return RayCrossingCounter.orientationIndex(p1, p2, q) @staticmethod def isCCW(ring): """ * Computes whether a ring defined by an array of Coordinate is * oriented counter-clockwise. * * - The list of points is assumed to have the first and last * points equal. * - This will handle coordinate lists which contain repeated points. * * This algorithm is <b>only</b> guaranteed to work with valid rings. * If the ring is invalid (e.g. self-crosses or touches), * the computed result <b>may</b> not be correct. * * @param ring an array of coordinates forming a ring * @return true if the ring is oriented counter-clockwise. """ # of points without closing endpoint nCoords = len(ring) - 1 if nCoords < 3: raise Exception("Ring has fewer than 3 points, so orientation cannot be determined") # find highest point # Coordinate hiPt = ring[0] hiIndex = 0 for i in range(1, nCoords): p = ring[i] if p.y > hiPt.y: hiPt = p hiIndex = i # find distinct point before highest point iPrev = hiIndex - 1 while ring[iPrev] == hiPt and iPrev != hiIndex: iPrev = iPrev - 1 if iPrev < 0: iPrev = nCoords # find distinct point after highest point iNext = (hiIndex + 1) % nCoords while ring[iNext] == hiPt and iNext != hiIndex: iNext = (iNext + 1) % nCoords # Coordinate prev = ring[iPrev] next = ring[iNext] """ * This check catches cases where the ring contains an A-B-A * configuration of points. * This can happen if the ring does not contain 3 distinct points * (including the case where the input array has fewer than 4 elements), * or it contains coincident line segments. """ if prev is hiPt or next is hiPt or prev is next: return False disc = CGAlgorithms.orientationIndex(prev, hiPt, next) """ * If disc is exactly 0, lines are collinear. * There are two possible cases: * (1) the lines lie along the x axis in opposite directions * (2) the lines lie on top of one another * * (1) is handled by checking if next is left of prev ==> CCW * (2) should never happen, so we're going to ignore it! * (Might want to assert this) """ isCCW = False if disc == 0: # poly is CCW if prev x is right of next x isCCW = prev.x > next.x else: # if area is positive, points are ordered CCW isCCW = disc > 0 return isCCW @staticmethod def isPointInRing(p, ring): """ * Tests whether a point lies inside a ring. * * The ring may be oriented in either direction. * A point lying exactly on the ring boundary is considered * to be inside the ring. * * This algorithm does not first check the * point against the envelope of the ring. * * @param p point to check for ring inclusion * @param ring is assumed to have first point identical to last point * @return true if p is inside ring * * @see locatePointInRing """ return RayCrossingCounter.locatePointInRing(p, ring) != Location.EXTERIOR @staticmethod def locatePointInRing(p, ring): """ * Determines whether a point lies in the interiors, * on the boundary, or in the exterior of a ring. * * The ring may be oriented in either direction. * * This method does <i>not</i> first check the point against * the envelope of the ring. * * @param p point to check for ring inclusion * @param ring an array of coordinates representing the ring * (which must have first point identical to last point) * @return the {Location} of p relative to the ring """ return RayCrossingCounter.locatePointInRing(p, ring) @staticmethod def signedArea(ring): npts = len(ring) if npts < 3: return 0.0 cx, cy = ring[0].x, ring[0].y nx, ny = ring[1].x, ring[1].y x0 = cx nx -= x0 sum = 0.0 for i in range(1, npts): py, cx, cy = cy, nx, ny nx, ny = ring[i].x, ring[i].y nx -= x0 sum += cx * (ny - py) return -sum / 2.0 @staticmethod def length(coords): if len(coords) < 2: return 0.0 length = 0.0 p0 = coords[0] x0, y0 = p0.x, p0.y for i in range(1, len(coords)): p = coords[i] x1, y1 = p.x, p.y dx, dy = x1 - x0, y1 - y0 length += sqrt(dx * dx + dy * dy) x0, y0 = x1, y1 return length @staticmethod def distancePointPoint(p0, p1): dx = p1.x - p0.x dy = p1.y - p0.y return sqrt(dx * dx + dy * dy) @staticmethod def distancePointLine(p, A, B): # if start==end, then use pt distance if A == B: return CGAlgorithms.distancePointPoint(p, A) """ otherwise use comp.graphics.algorithms Frequently Asked Questions method (1) AC dot AB r = --------- ||AB||^2 r has the following meaning: r=0 P = A r=1 P = B r<0 P is on the backward extension of AB r>1 P is on the forward extension of AB 0<r<1 P is interiors to AB """ bax = B.x - A.x bay = B.y - A.y pax = p.x - A.x pay = p.y - A.y d = (bax * bax + bay * bay) r = (pax * bax + pay * bay) / d if r <= 0.0: return CGAlgorithms.distancePointPoint(p, A) if r >= 1.0: return CGAlgorithms.distancePointPoint(p, B) """ (2) (Ay-Cy)(Bx-Ax)-(Ax-Cx)(By-Ay) s = ----------------------------- L^2 Then the distance from C to P = |s|*L. """ s = (pax * bay - pay * bax) / d return abs(s) * sqrt(d) @staticmethod def distanceLineLine(A, B, C, D): # check for zero-length segments if A == B: return CGAlgorithms.distancePointLine(A, C, D) if C == D: return CGAlgorithms.distancePointLine(D, A, B) # AB and CD are line segments """ from comp.graphics.algo Solving the above for r and s yields (Ay-Cy)(Dx-Cx)-(Ax-Cx)(Dy-Cy) r = ----------------------------- (eqn 1) (Bx-Ax)(Dy-Cy)-(By-Ay)(Dx-Cx) (Ay-Cy)(Bx-Ax)-(Ax-Cx)(By-Ay) s = ----------------------------- (eqn 2) (Bx-Ax)(Dy-Cy)-(By-Ay)(Dx-Cx) Let P be the position vector of the intersection point, then P=A+r(B-A) or Px=Ax+r(Bx-Ax) Py=Ay+r(By-Ay) By examining the values of r & s, you can also determine some other limiting conditions: If 0<=r<=1 & 0<=s<=1, intersection exists r<0 or r>1 or s<0 or s>1 line segments do not intersect If the denominator in eqn 1 is zero, AB & CD are parallel If the numerator in eqn 1 is also zero, AB & CD are collinear. """ r_top = (A.y - C.y) * (D.x - C.x) - (A.x - C.x) * (D.y - C.y) r_bot = (B.x - A.x) * (D.y - C.y) - (B.y - A.y) * (D.x - C.x) s_top = (A.y - C.y) * (B.x - A.x) - (A.x - C.x) * (B.y - A.y) s_bot = (B.x - A.x) * (D.y - C.y) - (B.y - A.y) * (D.x - C.x) if r_bot == 0 or s_bot == 0: return min([CGAlgorithms.distancePointLine(A, C, D), CGAlgorithms.distancePointLine(B, C, D), CGAlgorithms.distancePointLine(C, A, B), CGAlgorithms.distancePointLine(D, A, B)]) s = s_top / s_bot r = r_top / r_bot if r < 0 or r > 1 or s < 0 or s > 1: # no intersection return min([CGAlgorithms.distancePointLine(A, C, D), CGAlgorithms.distancePointLine(B, C, D), CGAlgorithms.distancePointLine(C, A, B), CGAlgorithms.distancePointLine(D, A, B)]) else: # intersection exists return 0.0 class Mod2BoundaryNodeRule(): """ * A {BoundaryNodeRule} specifies that points are in the * boundary of a lineal geometry iff * the point lies on the boundary of an odd number * of components. * Under this rule {LinearRing}s and closed * {LineString}s have an empty boundary. * <p> * This is the rule specified by the <i>OGC SFS</i>, * and is the default rule used in JTS. * * @author Martin Davis * @version 1.7 """ def isInBoundary(self, boundaryCount): return boundaryCount % 2 == 1 class EndPointBoundaryNodeRule(): """ * A {@link BoundaryNodeRule} which specifies that any points * which are endpoints * of lineal components are in the boundary of the * parent geometry. * This corresponds to the "intuitive" topological definition * of boundary. * Under this rule {@link LinearRing}s have a non-empty boundary * (the common endpoint of the underlying LineString). * <p> * This rule is useful when dealing with linear networks. * For example, it can be used to check * whether linear networks are correctly noded. * The usual network topology constraint is that linear segments may * touch only at endpoints. * In the case of a segment touching a closed segment (ring) at one * point, * the Mod2 rule cannot distinguish between the permitted case of * touching at the * node point and the invalid case of touching at some other interiors * (non-node) point. * The EndPoint rule does distinguish between these cases, * so is more appropriate for use. * * @author Martin Davis * @version 1.7 """ def isInBoundary(self, boundaryCount): return boundaryCount > 0 class BoundaryNodeRule(): mod2Rule = Mod2BoundaryNodeRule() endPointRule = EndPointBoundaryNodeRule() @staticmethod def getBoundaryEndPoint(): return BoundaryNodeRule.endPointRule @staticmethod def getBoundaryRuleMod2(): return BoundaryNodeRule.mod2Rule @staticmethod def getBoundaryOGCSFS(): return BoundaryNodeRule.getBoundaryRuleMod2() class PointInRing(): """ """ class MCPointInRing(PointInRing): """ """ def __init__(self, newRing): self._ring = newRing self._interval = Interval() self.coords = None self._tree = None self._crossings = 0 def isInside(self, coord): """ """ self._crossings = 0 # test all segments intersected by ray from pt in positive x direction rayEnv = Envelope(-1e64, 1e64, coord.y, coord.y) self._interval.mini = coord.y self._interval.maxi = coord.y segs = self._tree.query(self._interval) mcSelecter = MCSelecter(coord, self) for mc in segs: # MonotoneChain self.testMonotoneChain(rayEnv, mcSelecter, mc) return (self._crossings % 2) == 1 def testLineSegment(self, coord, seg): """ """ p1 = seg.p0 p2 = seg.p1 x1 = p1.x - coord.x y1 = p1.y - coord.y x2 = p2.x - coord.x y2 = p2.y - coord.y if (y1 > 0 and y2 <= 0) or (y2 > 0 and y1 <= 0): # segment straddles x axis, so compute intersection. xInt = RobustDeterminant.signOfDet2x2(x1, y1, x2, y2) / (y2 - y1) # crosses ray if strictly positive intersection. if 0.0 < xInt: self._crossings += 1 def buildIndex(self): self._tree = Bintree() coords = CoordinateSequence.removeRepeatedPoints(self._ring.coords) mcList = MonotoneChainBuilder.getChains(coords) for mc in mcList: mcEnv = mc.envelope self._interval.mini = mcEnv.miny self._interval.maxi = mcEnv.maxy self._tree.insert(self._interval, mc) def testMonotoneChain(self, rayEnv, mcSelecter, mc): """ """ mc.select(rayEnv, mcSelecter) class LineIntersector(): """ * A LineIntersector is an algorithm that can both test whether * two line segments intersect and compute the intersection point * if they do. * * The intersection point may be computed in a precise or non-precise manner. * Computing it precisely involves rounding it to an integer. (This assumes * that the input coordinates have been made precise by scaling them to * an integer grid.) """ # Indicates that line segments do not intersect NO_INTERSECTION = 0 # Indicates that line segments intersect in a single point POINT_INTERSECTION = 1 # Indicates that line segments intersect in a line segment COLLINEAR_INTERSECTION = 2 def __init__(self, initialPrecisionModel=None): # PrecisionModel self._precisionModel = initialPrecisionModel self.intersections = 0 # Coordinate self._inputLines = [] self._isProper = False # Coordinate self.intersectionPts = [None, None] # int The indexes of the endpoints of the intersection lines, in order along # the corresponding line self._intLineIndex = [[0, 0], [0, 0]] @property def isInteriorIntersection(self): """ * Tests whether either intersection point is an interiors point of * one of the input segments. * * @return true if either intersection point is in * the interiors of one of the input segments """ if self._isInteriorIntersection(0): return True if self._isInteriorIntersection(1): return True return False def _isInteriorIntersection(self, inputLineIndex): """ * Tests whether either intersection point is an interiors point * of the specified input segment. * * @return true if either intersection point is in * the interiors of the input segment """ for i in range(self.intersections): if not ((self.intersectionPts[i] == self._inputLines[inputLineIndex][0]) or (self.intersectionPts[i] == self._inputLines[inputLineIndex][1])): return True return False def nearestEndpoint(self, p1, p2, q1, q2): """ * Finds the endpoint of the segments P and Q which * is closest to the other segment. * This is a reasonable surrogate for the true * intersection points in ill-conditioned cases * (e.g. where two segments are nearly coincident, * or where the endpoint of one segment lies almost on the other segment). * <p> * This replaces the older CentralEndpoint heuristic, * which chose the wrong endpoint in some cases * where the segments had very distinct slopes * and one endpoint lay almost on the other segment. * * @param p1 an endpoint of segment P * @param p2 an endpoint of segment P * @param q1 an endpoint of segment Q * @param q2 an endpoint of segment Q * @return the nearest endpoint to the other segment """ nearestPt = p1 minDist = CGAlgorithms.distancePointLine(p1, q1, q2) dist = CGAlgorithms.distancePointLine(p2, q1, q2) if dist < minDist: minDist = dist nearestPt = p2 dist = CGAlgorithms.distancePointLine(q1, p1, p2) if dist < minDist: minDist = dist nearestPt = q1 dist = CGAlgorithms.distancePointLine(q2, p1, p2) if dist < minDist: nearestPt = q2 return nearestPt @staticmethod def computeEdgeDistance(p, p0, p1): """ * Computes the "edge distance" of an intersection point p in an edge. * * The edge distance is a metric of the point along the edge. * The metric used is a robust and easy to compute metric function. * It is <b>not</b> equivalent to the usual Euclidean metric. * It relies on the fact that either the x or the y ordinates of the * points in the edge are unique, depending on whether the edge is longer in * the horizontal or vertical direction. * * NOTE: This function may produce incorrect distances * for inputs where p is not precisely on p1-p2 * (E.g. p = (139,9) p1 = (139,10), p2 = (280,1) produces distanct * 0.0, which is incorrect. * * My hypothesis is that the function is safe to use for points which are the * result of <b>rounding</b> points which lie on the line, * but not safe to use for <b>truncated</b> points. """ dx = abs(p1.x - p0.x) dy = abs(p1.y - p0.y) # sentinel value dist = -1.0 if p == p0: dist = 0.0 elif p == p1: if dx > dy: dist = dx else: dist = dy else: pdx = abs(p.x - p0.x) pdy = abs(p.y - p0.y) if dx > dy: dist = pdx else: dist = pdy # hack to ensure that non-endpoints always have a non-zero distance if dist == 0.0 and not (p == p0): dist = max(pdx, pdy) assert(dist > 0.0 or p == p0), "Bad distance calculation" return dist def computeIntersection(self, p, p1, p2): """ * Compute the intersection of a point p and the line p1-p2. * * This function computes the boolean value of the hasIntersection test. * The actual value of the intersection (if there is one) * is equal to the value of p. """ self._isProper = False if Envelope.static_intersects(p1, p2, p): if (CGAlgorithms.orientationIndex(p1, p2, p) == 0 and CGAlgorithms.orientationIndex(p2, p1, p) == 0): self._isProper = True if (p == p1) or (p == p2): self._isProper = False self.intersectionPts[0] = p self.intersections = LineIntersector.POINT_INTERSECTION return self.intersections = LineIntersector.NO_INTERSECTION @staticmethod def _hasIntersection(p, p1, p2): # Same as above but doent's compute intersection point. Faster. if Envelope.static_intersects(p1, p2, p): if (CGAlgorithms.orientationIndex(p1, p2, p) == 0 and CGAlgorithms.orientationIndex(p2, p1, p) == 0): return True return False def computeLinesIntersection(self, p1, p2, q1, q2): # Computes the intersection of the lines p1-p2 and p3-p4 self._inputLines = [[p1, p2], [q1, q2]] # logger.debug("computeLinesIntersection %s", self) if USE_FUZZY_LINE_INTERSECTOR: self.intersections = self._fuzzy_computeIntersect(p1, p2, q1, q2) else: self.intersections = self._computeIntersect(p1, p2, q1, q2) def _fuzzy_computeIntersect(self, p1, p2, q1, q2): """ point_sur_segment return p: point d'intersection u: param t de l'intersection sur le segment courant v: param t de l'intersection sur le segment segment d: perpendicular distance of segment.p """ self._isProper = False if Envelope.static_intersects(p1, p2, q1, q2): # vector seg 1 vx = p2.x - p1.x vy = p2.y - p1.y # cross seg 2 qx = q2.y - q1.y qy = q1.x - q2.x # dot product d = qx * vx + qy * vy # vector delta p1 q1 dx = q1.x - p1.x dy = q1.y - p1.y # logger.debug("d:%s", d) # almost parallel, check for real proximity of points if d != 0 and abs(d) < 0.001: dx2 = q2.x - p2.x dy2 = q2.y - p2.y l = sqrt(vx * vx + vy * vy) dist0 = abs(vx * dy - vy * dx) / l if dist0 < EPSILON_SINGLE: dist1 = abs(vy * dx2 - vx * dy2) / l if dist1 < EPSILON_SINGLE: vqx = q2.x - q1.x vqy = q2.y - q1.y l = sqrt(vqx * vqx + vqy * vqy) dist2 = abs(vqy * dx - vqx * dy) / l dist3 = abs(vqx * dy2 - vqy * dx2) / l logger.debug("Almost parallel d0:%s d1:%s d2:%s d3:%s", dist0, dist1, dist2, dist3) if (dist2 < EPSILON_SINGLE and dist3 < EPSILON_SINGLE): d = 0 if d == 0: return self._computeCollinearIntersection(p1, p2, q1, q2) """ # check for distance between segments l = sqrt(vx * vx + vy * vy) d = abs(vx * dy - vy * dx) / l if d < EPSILON_SINGLE: # logger.debug("Parallel") if vx > vy: if p1.x > p2.x: p1, p2 = p2, p1 if q1.x > q2.x: q1, q2 = q2, q1 pq1 = p1.x <= q1.x <= p2.x pq2 = p1.x <= q2.x <= p2.x qp1 = q1.x <= p1.x <= q2.x qp2 = q1.x <= p2.x <= q2.x else: if p1.y > p2.y: p1, p2 = p2, p1 if q1.y > q2.y: q1, q2 = q2, q1 pq1 = p1.y <= q1.y <= p2.y pq2 = p1.y <= q2.y <= p2.y qp1 = q1.y <= p1.y <= q2.y qp2 = q1.y <= p2.y <= q2.y if pq1 and pq2: # q1 and q2 between p1 and p2 self.intersectionPts[0] = q1 self.intersectionPts[1] = q2 logger.debug("COLLINEAR_INTERSECTION q1:%s q2:%s", q1, q2) return LineIntersector.COLLINEAR_INTERSECTION if qp1 and qp2: self.intersectionPts[0] = p1 self.intersectionPts[1] = p2 logger.debug("COLLINEAR_INTERSECTION p1:%s p2:%s", p1, p2) return LineIntersector.COLLINEAR_INTERSECTION if pq1 and qp1: self.intersectionPts[0] = q1 self.intersectionPts[1] = p1 if (q1 == p1) and not pq2 and not qp2: logger.debug("POINT_INTERSECTION q1 == p1:%s", q1) return LineIntersector.POINT_INTERSECTION else: logger.debug("COLLINEAR_INTERSECTION q1:%s p1:%s", q1, p1) return LineIntersector.COLLINEAR_INTERSECTION if pq1 and qp2: self.intersectionPts[0] = q1 self.intersectionPts[1] = p2 if (q1 == p2) and not pq2 and not qp1: logger.debug("POINT_INTERSECTION q1 == p2:%s", q1) return LineIntersector.POINT_INTERSECTION else: logger.debug("COLLINEAR_INTERSECTION q1:%s p2:%s", q1, p2) return LineIntersector.COLLINEAR_INTERSECTION if pq2 and qp1: self.intersectionPts[0] = q2 self.intersectionPts[1] = p1 if (q2 == p1) and not pq1 and not qp2: logger.debug("POINT_INTERSECTION q2 == p1:%s", q2) return LineIntersector.POINT_INTERSECTION else: logger.debug("COLLINEAR_INTERSECTION q2:%s p1:%s", q2, p1) return LineIntersector.COLLINEAR_INTERSECTION if pq2 and qp2: self.intersectionPts[0] = q2 self.intersectionPts[1] = p2 if (q2 == p2) and not pq1 and not qp1: logger.debug("POINT_INTERSECTION q2 == p2:%s", q2) return LineIntersector.POINT_INTERSECTION else: logger.debug("COLLINEAR_INTERSECTION q2:%s p2:%s", q2, p2) return LineIntersector.COLLINEAR_INTERSECTION else: return LineIntersector.NO_INTERSECTION """ # logger.debug("Point intersection") # cross seg 1 py = p1.x - p2.x px = p2.y - p1.y u = (qx * dx + qy * dy) / d v = (px * dx + py * dy) / d if 0 <= u <= 1 and 0 <= v <= 1: # check for distance between end points and intersection pt = p1 + Coordinate(vx, vy) * u if u < 0.5: # near segment 1 start point # distance p1 intersection if p1.distance(pt) < EPSILON: self.u = 0 self.intersectionPts[0] = p1 logger.debug("POINT_INTERSECTION p1:%s", p1) return LineIntersector.POINT_INTERSECTION elif u > 0.5: # near segment 1 end point if p2.distance(pt) < EPSILON: self.u = 1 self.intersectionPts[0] = p2 logger.debug("POINT_INTERSECTION p2:%s", p2) return LineIntersector.POINT_INTERSECTION if v < 0.5: # near segment 1 start point # distance p1 intersection if q1.distance(pt) < EPSILON: self.v = 0 self.intersectionPts[0] = q1 logger.debug("POINT_INTERSECTION q1:%s", q1) return LineIntersector.POINT_INTERSECTION elif v > 0.5: # near segment 1 end point if q2.distance(pt) < EPSILON: self.v = 1 self.intersectionPts[0] = q2 logger.debug("POINT_INTERSECTION q2:%s", q2) return LineIntersector.POINT_INTERSECTION self._isProper = True logger.debug("POINT_INTERSECTION pt:%s", pt) self.intersectionPts[0] = pt return LineIntersector.POINT_INTERSECTION logger.debug("NO_INTERSECTION") return LineIntersector.NO_INTERSECTION @property def hasIntersection(self): return self.intersections != LineIntersector.NO_INTERSECTION @property def isProper(self): """ * Tests whether an intersection is proper. * * The intersection between two line segments is considered proper if * they intersect in a single point in the interiors of both segments * (e.g. the intersection is a single point and is not equal to any of the * endpoints). * * The intersection between a point and a line segment is considered proper * if the point lies in the interiors of the segment (e.g. is not equal to * either of the endpoints). * * @return true if the intersection is proper """ return self.hasIntersection and self._isProper @property def isCollinear(self): return self.intersections == LineIntersector.COLLINEAR_INTERSECTION @property def isEndpoint(self): return self.hasIntersection and not self._isProper def getIntersection(self, intIndex): return self.intersectionPts[intIndex] @staticmethod def isSameSignAndNonZero(a, b): if a == 0 or b == 0: return False return (a < 0 and b < 0) or (a > 0 and b > 0) def computeIntLineIndex(self): self._computeIntLineIndex(0) self._computeIntLineIndex(1) def isIntersection(self, pt): """ * Test whether a point is a intersection point of two line segments. * * Note that if the intersection is a line segment, this method only tests for * equality with the endpoints of the intersection segment. * It does <b>not</b> return true if * the input point is internal to the intersection segment. * * @return true if the input point is one of the intersection points. """ for i in range(self.intersections): if self.intersectionPts[i] == pt: return True return False def getIntersectionAlongSegment(self, segmentIndex, intIndex): """ * Computes the intIndex'th intersection point in the direction of * a specified input line segment * * @param segmentIndex is 0 or 1 * @param intIndex is 0 or 1 * * @return the intIndex'th intersection point in the direction of the * specified input line segment """ self.computeIntLineIndex() return self.intersectionPts[self._intLineIndex[segmentIndex][intIndex]] def getIndexAlongSegment(self, segmentIndex, intIndex): """ * Computes the index of the intIndex'th intersection point in the direction of * a specified input line segment * * @param segmentIndex is 0 or 1 * @param intIndex is 0 or 1 * * @return the index of the intersection point along the segment (0 or 1) """ self.computeIntLineIndex() return self._intLineIndex[segmentIndex][intIndex] def getEdgeDistance(self, segmentIndex, intIndex): """ * Computes the "edge distance" of an intersection point along the specified * input line segment. * * @param segmentIndex is 0 or 1 * @param intIndex is 0 or 1 * * @return the edge distance of the intersection point """ # logger.debug("getEdgeDistance %s", self) return self.computeEdgeDistance( self.intersectionPts[intIndex], self._inputLines[segmentIndex][0], self._inputLines[segmentIndex][1] ) def _intersectionWithNormalization(self, p1, p2, q1, q2, ret): # Make new Coordinates n1 = Coordinate(p1.x, p1.y) n2 = Coordinate(p2.x, p2.y) n3 = Coordinate(q1.x, q1.y) n4 = Coordinate(q2.x, q2.y) normPt = Coordinate(0, 0) self._normalizeToEnvCentre(n1, n2, n3, n4, normPt) self._safeHCoordinateIntersection(n1, n2, n3, n4, ret) ret.x += normPt.x ret.y += normPt.y def _computeIntLineIndex(self, segmentIndex): dist0 = self.getEdgeDistance(segmentIndex, 0) dist1 = self.getEdgeDistance(segmentIndex, 1) if dist0 > dist1: self._intLineIndex[segmentIndex][0] = 0 self._intLineIndex[segmentIndex][1] = 1 else: self._intLineIndex[segmentIndex][0] = 1 self._intLineIndex[segmentIndex][1] = 0 def _computeIntersect(self, p1, p2, q1, q2): self._isProper = False # logger.debug("LineIntersector.computeIntersect(p1:%s, p2:%s, q1:%s, q2:%s)", p1, p2, q1, q2) # logger.debug("LineIntersector.computeIntersect(%s)", self) if not Envelope.static_intersects(p1, p2, q1, q2): logger.debug("NO_INTERSECTION env p1:%s p2:%s q1:%s q2:%s", p1, p2, q1, q2) return LineIntersector.NO_INTERSECTION # for each endpoint, compute which side of the other segment it lies # if both endpoints lie on the same side of the other segment, # the segments do not intersect pq1 = CGAlgorithms.orientationIndex(p1, p2, q1) pq2 = CGAlgorithms.orientationIndex(p1, p2, q2) if (pq1 > 0 and pq2 > 0) or (pq1 < 0 and pq2 < 0): logger.debug("NO_INTERSECTION p side p1:%s p2:%s q1:%s q2:%s", p1, p2, q1, q2) return LineIntersector.NO_INTERSECTION qp1 = CGAlgorithms.orientationIndex(q1, q2, p1) qp2 = CGAlgorithms.orientationIndex(q1, q2, p2) if (qp1 > 0 and qp2 > 0) or (qp1 < 0 and qp2 < 0): logger.debug("NO_INTERSECTION q side p1:%s p2:%s q1:%s q2:%s", p1, p2, q1, q2) return LineIntersector.NO_INTERSECTION collinear = pq1 == 0 and pq2 == 0 and qp1 == 0 and qp2 == 0 if collinear: return self._computeCollinearIntersection(p1, p2, q1, q2) """ * At this point we know that there is a single intersection point * (since the lines are not collinear). * Check if the intersection is an endpoint. * If it is, copy the endpoint as * the intersection point. Copying the point rather than * computing it ensures the point has the exact value, * which is important for robustness. It is sufficient to * simply check for an endpoint which is on the other line, * since at this point we know that the inputLines must * intersect. """ if pq1 == 0 or pq2 == 0 or qp1 == 0 or qp2 == 0: """ * Check for two equal endpoints. * This is done explicitly rather than by the orientation tests * below in order to improve robustness. * * (A example where the orientation tests fail * to be consistent is: * * LINESTRING ( 19.850257749638203 46.29709338043669, * 20.31970698357233 46.76654261437082 ) * and * LINESTRING ( -48.51001596420236 -22.063180333403878, * 19.850257749638203 46.29709338043669 ) * * which used to produce the result: * (20.31970698357233, 46.76654261437082, NaN) """ if p1 == q1 or p1 == q2: self.intersectionPts[0] = p1 logger.debug("POINT_INTERSECTION p1:%s", p1) elif p2 == q1 or p2 == q2: self.intersectionPts[0] = p2 logger.debug("POINT_INTERSECTION p2:%s", p2) elif pq1 == 0: # Now check to see if any endpoint lies on the interiors of the other segment self.intersectionPts[0] = q1 logger.debug("POINT_INTERSECTION q1:%s", q1) elif pq2 == 0: self.intersectionPts[0] = q2 logger.debug("POINT_INTERSECTION q2:%s", q2) elif qp1 == 0: self.intersectionPts[0] = p1 logger.debug("POINT_INTERSECTION p1:%s", p1) elif qp2 == 0: self.intersectionPts[0] = p2 logger.debug("POINT_INTERSECTION p2:%s", p2) else: self._isProper = True self.intersectionPts[0] = Coordinate(0, 0) self._intersection(p1, p2, q1, q2, self.intersectionPts[0]) logger.debug("POINT_INTERSECTION pt:%s", self.intersectionPts[0]) return LineIntersector.POINT_INTERSECTION def _computeCollinearIntersection(self, p1, p2, q1, q2): # logger.debug("LineIntersector._computeCollinearIntersection()") pq1 = Envelope.static_intersects(p1, p2, q1) pq2 = Envelope.static_intersects(p1, p2, q2) qp1 = Envelope.static_intersects(q1, q2, p1) qp2 = Envelope.static_intersects(q1, q2, p2) if pq1 and pq2: # q1 and q2 between p1 and p2 self.intersectionPts[0] = q1.clone() self.intersectionPts[1] = q2.clone() logger.debug("COLLINEAR_INTERSECTION q1:%s q2:%s between p1:%s p2:%s", q1, q2, p1, p2) return LineIntersector.COLLINEAR_INTERSECTION if qp1 and qp2: self.intersectionPts[0] = p1.clone() self.intersectionPts[1] = p2.clone() logger.debug("COLLINEAR_INTERSECTION p1:%s p2:%s between q1:%s q2:%s", p1, p2, q1, q2) return LineIntersector.COLLINEAR_INTERSECTION if pq1 and qp1: self.intersectionPts[0] = q1.clone() self.intersectionPts[1] = p1.clone() if (q1 == p1) and not pq2 and not qp2: logger.debug("POINT_INTERSECTION q1 == p1:%s", q1) return LineIntersector.POINT_INTERSECTION else: logger.debug("COLLINEAR_INTERSECTION q1:%s p1:%s", q1, p1) return LineIntersector.COLLINEAR_INTERSECTION if pq1 and qp2: self.intersectionPts[0] = q1.clone() self.intersectionPts[1] = p2.clone() if (q1 == p2) and not pq2 and not qp1: logger.debug("POINT_INTERSECTION q1 == p2:%s", q1) return LineIntersector.POINT_INTERSECTION else: logger.debug("COLLINEAR_INTERSECTION q1:%s p2:%s", q1, p2) return LineIntersector.COLLINEAR_INTERSECTION if pq2 and qp1: self.intersectionPts[0] = q2.clone() self.intersectionPts[1] = p1.clone() if (q2 == p1) and not pq1 and not qp2: logger.debug("POINT_INTERSECTION q2 == p1:%s", q2) return LineIntersector.POINT_INTERSECTION else: logger.debug("COLLINEAR_INTERSECTION q2:%s p1:%s", q2, p1) return LineIntersector.COLLINEAR_INTERSECTION if pq2 and qp2: self.intersectionPts[0] = q2.clone() self.intersectionPts[1] = p2.clone() if (q2 == p2) and not pq1 and not qp1: logger.debug("POINT_INTERSECTION q2 == p2:%s", q2) return LineIntersector.POINT_INTERSECTION else: logger.debug("COLLINEAR_INTERSECTION q2:%s p2:%s", q2, p2) return LineIntersector.COLLINEAR_INTERSECTION logger.debug("NO_INTERSECTION collinear p1:%s p2:%s q1:%s q2:%s", p1, p2, q1, q2) return LineIntersector.NO_INTERSECTION def _intersection(self, p1, p2, q1, q2, intPtOut): """ * This method computes the actual value of the intersection point. * * To obtain the maximum precision from the intersection calculation, * the coordinates are normalized by subtracting the minimum * ordinate values (in absolute value). This has the effect of * removing common significant digits from the calculation to * maintain more bits of precision. """ self._intersectionWithNormalization(p1, p2, q1, q2, intPtOut) """ * Due to rounding it can happen that the computed intersection is * outside the envelopes of the input segments. Clearly this * is inconsistent. * This code checks this condition and forces a more reasonable answer * * MD - May 4 2005 - This is still a problem. Here is a failure case: * * LINESTRING (2089426.5233462777 1180182.3877339689, * 2085646.6891757075 1195618.7333999649) * LINESTRING (1889281.8148903656 1997547.0560044837, * 2259977.3672235999 483675.17050843034) * int point = (2097408.2633752143,1144595.8008114607) """ if not self._isInSegmentEnvelopes(intPtOut): intPtOut = self.nearestEndpoint(p1, p2, q1, q2) def _smallestInAbsValue(self, x1, x2, x3, x4): """ """ x = x1 xabs = abs(x) if abs(x2) < xabs: x = x2 xabs = abs(x2) if abs(x3) < xabs: x = x3 xabs = abs(x3) if abs(x4) < xabs: x = x4 return x def _isInSegmentEnvelopes(self, intPt): """ * Test whether a point lies in the envelopes of both input segments. * A correctly computed intersection point should return true * for this test. * Since this test is for debugging purposes only, no attempt is * made to optimize the envelope test. * * @return true if the input point lies within both * input segment envelopes """ p0, p1 = self._inputLines[0] env0 = Envelope(p0, p1) p0, p1 = self._inputLines[0] env1 = Envelope(p0, p1) return env0.contains(intPt) and env1.contains(intPt) def _normalizeToEnvCentre(self, p1, p2, q1, q2, normPt): """ * Normalize the supplied coordinates to * so that the midpoint of their intersection envelope * lies at the origin. * * @param n00 * @param n01 * @param n10 * @param n11 * @param normPt """ minx0, maxx0 = p1.x, p2.x if minx0 > maxx0: minx0, maxx0 = maxx0, minx0 miny0, maxy0 = p1.y, p2.y if miny0 > maxy0: miny0, maxy0 = maxy0, miny0 minx1, maxx1 = q1.x, q2.x if minx1 > maxx1: minx1, maxx1 = maxx1, minx1 miny1, maxy1 = q1.y, q2.y if miny1 > maxy1: miny1, maxy1 = maxy1, miny1 if minx0 > minx1: minx = minx0 else: minx = minx1 if miny0 > miny1: miny = miny0 else: miny = miny1 if maxx0 < maxx1: maxx = maxx0 else: maxx = maxx1 if maxy0 < maxy1: maxy = maxy0 else: maxy = maxy1 midx = (minx + maxx) / 2.0 midy = (miny + maxy) / 2.0 normPt.x = midx normPt.y = midy p1.x -= midx p1.y -= midy p2.x -= midx p2.y -= midy q1.x -= midx q1.y -= midy q2.x -= midx q2.y -= midy def _safeHCoordinateIntersection(self, p1, p2, q1, q2, intPt): """ * Computes a segment intersection using homogeneous coordinates. * Round-off error can cause the raw computation to fail, * (usually due to the segments being approximately parallel). * If this happens, a reasonable approximation is computed instead. * * @param p1 a segment endpoint * @param p2 a segment endpoint * @param q1 a segment endpoint * @param q2 a segment endpoint * @param intPt the computed intersection point is stored there """ try: HCoordinate.intersection(p1, p2, q1, q2, intPt) except: coord = self.nearestEndpoint(p1, p2, q1, q2) intPt.x = coord.x intPt.y = coord.y pass def __str__(self): return "p1:{}_p2:{} q1:{}_q2:{} : isEndpoint:{} isProper:{} isCollinear:{}".format( self._inputLines[0][0], self._inputLines[0][1], self._inputLines[1][0], self._inputLines[1][1], self.isEndpoint, self.isProper, self.isCollinear ) # algorithms/locate class IntervalIndexedGeometry(): def __init__(self, geom): # index.intervalrtree.SortedPackedIntervalRTree self.index = SortedPackedIntervalRTree() self.init(geom) def init(self, geom) -> None: # LineString lines = [] LinearComponentExtracter.getLines(geom, lines) for line in lines: self.addLine(line.coords) def addLine(self, coords) -> None: for i in range(1, len(coords)): seg = LineSegment(coords[i - 1], coords[i]) min = seg.p0.y max = seg.p1.y if min > max: max, min = min, max self.index.insert(min, max, seg) def query(self, min: float, max: float, visitor) -> None: self.index.query(min, max, visitor) class SegmentVisitor(ItemVisitor): def __init__(self, counter): ItemVisitor.__init__(self) # algorithm.RayCrossingCounter self.counter = counter def visitItem(self, item) -> None: self.counter.countSegment(item.p0, item.p1) class IndexedPointInAreaLocator(): """ * Determines the location of {@link Coordinate}s relative to * a {@link Polygon} or {@link MultiPolygon} geometry, using indexing for efficiency. * * This algorithm is suitable for use in cases where * many points will be tested against a given area. * * @author Martin Davis * """ def __init__(self, geom): """ * Creates a new locator for a given {@link Geometry} * @param g the Geometry to locate in """ # Geometry self.geom = geom if geom.type_id not in [ GeomTypeId.GEOS_POLYGON, GeomTypeId.GEOS_MULTIPOLYGON ]: raise ValueError("Argument must be Polygonal") # IntervalIndexedGeometry self.index = None self.buildIndex(geom) def buildIndex(self, geom) -> None: self.index = IntervalIndexedGeometry(geom) def locate(self, coord) -> int: """ * Determines the {@link Location} of a point in an areal {@link Geometry}. * * @param p the point to test * @return the location of the point in the geometry """ rcc = RayCrossingCounter(coord) visitor = SegmentVisitor(rcc) self.index.query(coord.y, coord.y, visitor) return rcc.location class SimplePointInAreaLocator(): @staticmethod def locate(p, geom): if geom.is_empty: return Location.EXTERIOR if SimplePointInAreaLocator.containsPoint(p, geom): return Location.INTERIOR return Location.EXTERIOR @staticmethod def containsPoint(p, geom): type_id = geom.type_id if type_id == GeomTypeId.GEOS_POLYGON: return SimplePointInAreaLocator.containsPointInPolygon(p, geom) elif (type_id == GeomTypeId.GEOS_GEOMETRYCOLLECTION or type_id == GeomTypeId.GEOS_MULTIPOLYGON): for g in geom.geoms: if SimplePointInAreaLocator.containsPoint(p, g): return True return False @staticmethod def containsPointInPolygon(p, geom): if geom.is_empty: return False exterior = geom.exterior coords = exterior.coords if not CGAlgorithms.isPointInRing(p, coords): return False for hole in geom.interiors: coords = hole.coords if CGAlgorithms.isPointInRing(p, coords): return False return True # geomgraph/index class EdgeSetIntersector(): """ * Interface """ def __init__(self): pass def computeSelfIntersections(self, edges, si, testAllSegments): """ * Computes all self-intersections between edges in a set of edges, * allowing client to choose whether self-intersections are computed. * * @param edges a list of edges to test for intersections * @param si the SegmentIntersector to use * @param testAllSegments true if self-intersections are to be tested as well """ raise NotImplementedError() def computeIntersections(self, edges0, edges1, si): """ * Computes all mutual intersections between two sets of edges """ raise NotImplementedError() class SegmentIntersector(): """ """ def __init__(self, newLi, newIncludeProper, newRecordIsolated): """ * These variables keep track of what types of intersections were * found during ALL edges that have been intersected. """ self.hasIntersection = False self.hasProper = False self.hasProperInterior = False self.isDone = False self.isDoneWhenProperInt = False # the proper intersection point found # Coordinate self.properIntersectionPoint = None # LineIntersector self._li = newLi # bool self._includeProper = newIncludeProper self._recordIsolated = newRecordIsolated # int self.numIntersections = 0 # Elements are externally owned # Node self._bdyNodes = [None, None] # testing only self.numTests = 0 def setBoundaryNodes(self, bdyNodes0, bdyNodes1): self._bdyNodes[0] = bdyNodes0 self._bdyNodes[1] = bdyNodes1 def addIntersections(self, e0, segIndex0, e1, segIndex1): """ * This method is called by clients of the EdgeIntersector class to test * for and add intersections for two segments of the edges being intersected. * Note that clients (such as MonotoneChainEdges) may choose not to intersect * certain pairs of segments for efficiency reasons. * @param e0: Edge 1 first edge * @param e1: Edge 2 other edge * @param segIndex0 index of first vertex of segment of Edge 1 * @param segIndex1 index of first vertex of segment of Edge 2 """ if e0 == e1 and segIndex0 == segIndex1: return self.numTests += 1 # CoordinateSequence c0 = e0.coords # Coordinate p1 = c0[segIndex0] p2 = c0[segIndex0 + 1] # CoordinateSequence c1 = e1.coords # Coordinate q1 = c1[segIndex1] q2 = c1[segIndex1 + 1] # LineIntersector _li = self._li _li.computeLinesIntersection(p1, p2, q1, q2) """ * Always record any non-proper intersections. * If includeProper is true, record any proper intersections as well. """ if _li.hasIntersection: if self._recordIsolated: e0.isIsolated = False e1.isIsolated = False self.numIntersections += 1 # If the segments are adjacent they have at least one trivial # intersection, the shared endpoint. # Don't bother adding it if it is the # only intersection. if not self.isTrivialIntersection(e0, segIndex0, e1, segIndex1): self.hasIntersection = True if self._includeProper or not _li.isProper: logger.debug("SegmentIntersector.addIntersections(): (icludeProper: %s || !li.isProper: %s)", self._includeProper, not _li.isProper) e0.addIntersections(_li, segIndex0, 0) e1.addIntersections(_li, segIndex1, 1) if _li.isProper: self.properIntersectionPoint = _li.getIntersection(0) self.hasProper = True logger.debug("SegmentIntersector.addIntersections(): properIntersectionPoint: %s", self.properIntersectionPoint) if self.isDoneWhenProperInt: self.isDone = True if not self.isBoundaryPoint(_li, self._bdyNodes): self.hasProperInterior = True def isAdjacentSegments(self, i1, i2): return abs(i1 - i2) == 1 def isTrivialIntersection(self, e0, segIndex0, e1, segIndex1): """ * A trivial intersection is an apparent self-intersection which in fact * is simply the point shared by adjacent line segments. * Note that closed edges require a special check for the point * shared by the beginning and end segments. * @param e0: Edge 1 first edge * @param e1: Edge 2 other edge * @param segIndex0 index of first vertex of segment of Edge 1 * @param segIndex1 index of first vertex of segment of Edge 2 """ if e0 == e1: # only one intersection detected if self._li.intersections == 1: # is between consecutive segments if self.isAdjacentSegments(segIndex0, segIndex1): return True # is between last and first segment of closed shape if e0.isClosed: maxSegIndex = len(e0.coords) - 1 if ((segIndex0 == 0 and segIndex1 == maxSegIndex) or (segIndex1 == 0 and segIndex0 == maxSegIndex)): return True return False def _isBoundaryPoint(self, li, tstBdyNodes): if tstBdyNodes is None: return False for node in tstBdyNodes: if li.isIntersection(node.coord): return True return False def isBoundaryPoint(self, li, tstBdyNodes): if self._isBoundaryPoint(li, tstBdyNodes[0]): return True if self._isBoundaryPoint(li, tstBdyNodes[1]): return True return False class SweepLineEvent(): INSERT_EVENT = 1 DELETE_EVENT = 2 def __init__(self, edgeSet, x, insertEvent, newObj): self.edgeSet = edgeSet # SweepLineSegment self._obj = newObj self.x = x # SweepLineEvent INSERT_EVENT self._insertEvent = insertEvent self._deleteEventIndex = 0 if insertEvent is None: self.eventType = SweepLineEvent.INSERT_EVENT else: self.eventType = SweepLineEvent.DELETE_EVENT def compareTo(self, other): """ * ProjectionEvents are ordered first by their x-value, and then by their * eventType. * It is important that Insert events are sorted before Delete events, so that * items whose Insert and Delete events occur at the same x-value will be * correctly handled. """ if self.x < other.x: return -1 elif self.x > other.x: return 1 elif self.eventType < other.eventType: return -1 elif self.eventType > other.eventType: return 1 else: return 0 @property def isInsert(self): return self.eventType == SweepLineEvent.INSERT_EVENT @property def isDelete(self): return self.eventType == SweepLineEvent.DELETE_EVENT class SweepLineSegment(): def __init__(self, newEdge, newPtIndex): self.edge = newEdge self.coords = newEdge.coords self.ptIndex = newPtIndex @property def minx(self): x1 = self.coords[self.ptIndex].x x2 = self.coords[self.ptIndex + 1].x if x1 < x2: return x1 else: return x2 @property def maxx(self): x1 = self.coords[self.ptIndex].x x2 = self.coords[self.ptIndex + 1].x if x1 > x2: return x1 else: return x2 def computeIntersections(self, ss, si): si.addIntersections(self.edge, self.ptIndex, ss.edge, ss.ptIndex) class MonotoneChainEdge(): """ * MonotoneChains are a way of partitioning the segments of an edge to * allow for fast searching of intersections. * They have the following properties: * * - the segments within a monotone chain will never intersect each other * - the envelope of any contiguous subset of the segments in a monotone * chain is simply the envelope of the endpoints of the subset. * * Property 1 means that there is no need to test pairs of segments from * within the same monotone chain for intersection. * Property 2 allows binary search to be used to find the intersection * points of two monotone chains. * For many types of real-world data, these properties eliminate a large * number of segment comparisons, producing substantial speed gains. * @version 1.1 """ def __init__(self, newEdge): # Edge self._edge = newEdge # CoordinateSequence self.coords = newEdge.coords # int # the lists of start/end indexes of the monotone chains. # Includes the end point of the edge as a sentinel self._startIndex = [] # Envelope # these envelopes are created once and reused self._env1 = Envelope() self._env2 = Envelope() mcb = MonotoneChainIndexer() mcb.getChainStartIndices(self.coords, self._startIndex) logger.debug("MonotoneChainEdge.indices %s", ", ".join([str(i) for i in self._startIndex])) def getMinX(self, chainIndex): x1 = self.coords[self._startIndex[chainIndex]].x x2 = self.coords[self._startIndex[chainIndex + 1]].x if x1 < x2: return x1 else: return x2 def getMaxX(self, chainIndex): x1 = self.coords[self._startIndex[chainIndex]].x x2 = self.coords[self._startIndex[chainIndex + 1]].x if x1 > x2: return x1 else: return x2 def computeIntersects(self, mce, si): for i in range(len(self._startIndex) - 1): for j in range(len(mce._startIndex) - 1): self.computeIntersectsForChain(i, mce, j, si) def computeIntersectsForChain(self, chainIndex0: int, mce, chainIndex1: int, si): self._computeIntersectsForChain( self._startIndex[chainIndex0], self._startIndex[chainIndex0 + 1], mce, mce._startIndex[chainIndex1], mce._startIndex[chainIndex1 + 1], si) def _computeIntersectsForChain(self, start0: int, end0: int, mce, start1: int, end1: int, si) -> None: # terminating condition for the recursion if end0 - start0 == 1 and end1 - start1 == 1: # SegmentIntersector logger.debug("MonotoneChainEdge._computeIntersectsForChain indexes: %s / %s", start0, start1) si.addIntersections(self._edge, start0, mce._edge, start1) return p1 = self.coords[start0] p2 = self.coords[end0] q1 = mce.coords[start1] q2 = mce.coords[end1] self._env1.__init__(p1, p2) self._env2.__init__(q1, q2) if not self._env1.intersects(self._env2): logger.debug("MonotoneChainEdge._computeIntersectsForChain env dosent intersect") return mid0 = int((start0 + end0) / 2) mid1 = int((start1 + end1) / 2) logger.debug("MonotoneChainEdge._computeIntersectsForChain indexes: %s - %s - %s / %s - %s - %s", start0, mid0, end0, start1, mid1, end1) if start0 < mid0: if start1 < mid1: self._computeIntersectsForChain(start0, mid0, mce, start1, mid1, si) if mid1 < end1: self._computeIntersectsForChain(start0, mid0, mce, mid1, end1, si) if mid0 < end0: if start1 < mid1: self._computeIntersectsForChain(mid0, end0, mce, start1, mid1, si) if mid1 < end1: self._computeIntersectsForChain(mid0, end0, mce, mid1, end1, si) class SimpleMonotoneChain(): def __init__(self, mce, chainIndex): self.mce = mce self.chainIndex = chainIndex def computeIntersections(self, mc, si): self.mce.computeIntersectsForChain(self.chainIndex, mc.mce, mc.chainIndex, si) class SimpleMCSweepLineIntersector(EdgeSetIntersector): """ * Finds all intersections in one or two sets of edges, * using an x-axis sweepline algorithm in conjunction with Monotone Chains. * * While still O(n^2) in the worst case, this algorithm * drastically improves the average-case time. * The use of MonotoneChains as the items in the index * seems to offer an improvement in performance over a sweep-line alone. """ def __init__(self): # SweepLineEvent self.events = [] self.nOverlaps = 0 def computeSelfIntersections(self, edges, si, testAllSegments=False): """ * Computes all self-intersections between edges in a set of edges, * allowing client to choose whether self-intersections are computed. * * @param edges a list of edges to test for intersections * @param si the SegmentIntersector to use * @param testAllSegments true if self-intersections are to be tested as well """ if testAllSegments: self.addEdges(edges, None) else: for edge in edges: self._add(edge, edge) self._computeIntersections(si) def computeIntersections(self, edges0, edges1, si): """ * Computes all mutual intersections between two sets of edges """ self.addEdges(edges0, edges0) self.addEdges(edges1, edges1) self._computeIntersections(si) def _computeIntersections(self, si): self.nOverlaps = 0 self._prepareEvents() for i, ev in enumerate(self.events): if ev.isInsert: self._processOverlaps(i, ev._deleteEventIndex, ev, si) if si.isDone: break def _prepareEvents(self): """ * Because Delete Events have a link to their corresponding Insert event, * it is possible to compute exactly the range of events which must be * compared to a given Insert event object. """ self.events = sorted(self.events, key=lambda ev: (ev.x, ev.eventType)) for i, ev in enumerate(self.events): if ev.isDelete: ev._insertEvent._deleteEventIndex = i def addEdges(self, edges, edgeSet=None): for edge in edges: self._add(edge, edgeSet) """ def _add(self, edge, edgeSet): mce = edge.coords for i in range(len(mce) - 1): mc = SweepLineSegment(edge, i) insertEvent = SweepLineEvent(edgeSet, mc.minx, None, mc) self.events.append(insertEvent) self.events.append(SweepLineEvent(edgeSet, mc.maxx, insertEvent, mc)) """ def _add(self, edge, edgeSet): mce = edge.monotoneChainEdge for i in range(len(mce._startIndex) - 1): mc = SimpleMonotoneChain(mce, i) insertEvent = SweepLineEvent(edgeSet, mce.getMinX(i), None, mc) self.events.append(insertEvent) self.events.append(SweepLineEvent(edgeSet, mce.getMaxX(i), insertEvent, mc)) def _processOverlaps(self, start, end, ev0, si): # SimpleMonotoneChain mc0 = ev0._obj """ * Since we might need to test for self-intersections, * include current insert event object in list of event objects to test. * Last index can be skipped, because it must be a Delete event. """ for i in range(start, end): # SeeplineEvent ev1 = self.events[i] if ev1.isInsert: # SimpleMonotoneChain mc1 = ev1._obj if ev0.edgeSet is None or ev0.edgeSet != ev1.edgeSet: mc0.computeIntersections(mc1, si) self.nOverlaps += 1 # algorithm/MCPointInRing class MCSelecter(MonotoneChainSelectAction): def __init__(self, newCoord, mcp): # Coordinate self.coord = newCoord # MCPointInRing self._parent = mcp def select(self, seg): self._parent.testLineSegment(self.coord, seg)