def clip_lines_by_polygon(lines, polygon, closed=True, check_input=True): """Clip multiple lines by polygon Input line: Sequence of polylines: [[p0, p1, ...], [q0, q1, ...], ...] where pi and qi are point coordinates (x, y). Output polygon: list of vertices of polygon or the corresponding numpy array closed: (optional) determine whether points on boundary should be regarded as belonging to the polygon (closed = True) or not (closed = False) - False is not recommended here check_input: Allows faster execution if set to False Outputs inside_line_segments: Clipped line segments that are inside polygon outside_line_segments: Clipped line segments that are outside polygon This is a wrapper around clip_line_by_polygon """ if check_input: # Input checks msg = 'Keyword argument "closed" must be boolean' if not isinstance(closed, bool): raise RuntimeError(msg) for i in range(len(lines)): try: lines[i] = ensure_numeric(lines[i], numpy.float) except Exception, e: msg = ('Line could not be converted to numeric array: %s' % str(e)) raise Exception(msg) try: polygon = ensure_numeric(polygon, numpy.float) except Exception, e: msg = ('Polygon could not be converted to numeric array: %s' % str(e)) raise Exception(msg)
def intersection(line0, line1, rtol=1.0e-12, atol=1.0e-12): """Returns intersecting point between two line segments. However, if parallel lines coincide partly (i.e. share a common segment), the line segment where lines coincide is returned Inputs: line0, line1: Each defined by two end points as in: [[x0, y0], [x1, y1]] A line can also be a 2x2 numpy array with each row corresponding to a point. rtol, atol: Tolerances passed onto numpy.allclose Output: status, value - where status and value is interpreted as follows: status == 0: no intersection, value set to None. status == 1: intersection point found and returned in value as [x,y]. status == 2: Collinear overlapping lines found. Value takes the form [[x0,y0], [x1,y1]] which is the segment common to both lines. status == 3: Collinear non-overlapping lines. Value set to None. status == 4: Lines are parallel. Value set to None. """ line0 = ensure_numeric(line0, numpy.float) line1 = ensure_numeric(line1, numpy.float) x0, y0 = line0[0, :] x1, y1 = line0[1, :] x2, y2 = line1[0, :] x3, y3 = line1[1, :] denom = (y3 - y2) * (x1 - x0) - (x3 - x2) * (y1 - y0) u0 = (x3 - x2) * (y0 - y2) - (y3 - y2) * (x0 - x2) u1 = (x2 - x0) * (y1 - y0) - (y2 - y0) * (x1 - x0) if numpy.allclose(denom, 0.0, rtol=rtol, atol=atol): # Lines are parallel - check if they are collinear if numpy.allclose([u0, u1], 0.0, rtol=rtol, atol=atol): # We now know that the lines are collinear state = (point_on_line([x0, y0], line1, rtol=rtol, atol=atol), point_on_line([x1, y1], line1, rtol=rtol, atol=atol), point_on_line([x2, y2], line0, rtol=rtol, atol=atol), point_on_line([x3, y3], line0, rtol=rtol, atol=atol)) return collinearmap[state]([x0, y0], [x1, y1], [x2, y2], [x3, y3]) else: # Lines are parallel but aren't collinear return 4, None # FIXME (Ole): Add distance here instead of None else: # Lines are not parallel, check if they intersect u0 = u0 / denom u1 = u1 / denom x = x0 + u0 * (x1 - x0) y = y0 + u0 * (y1 - y0) # Sanity check - can be removed to speed up if needed if not numpy.allclose(x, x2 + u1 * (x3 - x2), rtol=rtol, atol=atol): raise Exception if not numpy.allclose(y, y2 + u1 * (y3 - y2), rtol=rtol, atol=atol): raise Exception # Check if point found lies within given line segments if 0.0 <= u0 <= 1.0 and 0.0 <= u1 <= 1.0: # We have intersection return 1, numpy.array([x, y]) else: # No intersection return 0, None
def clip_line_by_polygon(line, polygon, closed=True, check_input=True): """Clip line segments by polygon Input line: Sequence of line nodes: [[x0, y0], [x1, y1], ...] or the equivalent Nx2 numpy array polygon: list of vertices of polygon or the corresponding numpy array closed: (optional) determine whether points on boundary should be regarded as belonging to the polygon (closed = True) or not (closed = False) - False is not recommended here check_input: Allows faster execution if set to False Outputs inside_lines: Clipped lines that are inside polygon outside_lines: Clipped lines that are outside polygon Both outputs take the form of lists of Nx2 line arrays Example: U = [[0,0], [1,0], [1,1], [0,1]] # Unit square # Simple horizontal fully intersecting line line = [[-1, 0.5], [2, 0.5]] inside_line_segments, outside_line_segments = \ clip_line_by_polygon(line, polygon) print numpy.allclose(inside_line_segments, [[[0, 0.5], [1, 0.5]]]) print numpy.allclose(outside_line_segments, [[[-1, 0.5], [0, 0.5]], [[1, 0.5], [2, 0.5]]]) Remarks: The assumptions listed in separate_points_by_polygon apply Output line segments are listed as separate lines i.e. not joined """ if check_input: # Input checks msg = 'Keyword argument "closed" must be boolean' if not isinstance(closed, bool): raise RuntimeError(msg) try: line = ensure_numeric(line, numpy.float) except Exception, e: msg = ('Line could not be converted to numeric array: %s' % str(e)) raise Exception(msg) msg = 'Line segment array must be a 2d array of vertices' if not len(line.shape) == 2: raise RuntimeError(msg) msg = 'Line array must have two columns' if not line.shape[1] == 2: raise RuntimeError(msg) try: polygon = ensure_numeric(polygon, numpy.float) except Exception, e: msg = ('Polygon could not be converted to numeric array: %s' % str(e)) raise Exception(msg)
def point_on_line(points, line, rtol=1.0e-5, atol=1.0e-8): """Determine if a point is on a line segment Input points: Coordinates of either * one point given by sequence [x, y] * multiple points given by list of points or Nx2 array line: Endpoint coordinates [[x0, y0], [x1, y1]] or the equivalent 2x2 numeric array with each row corresponding to a point. rtol: Relative error for how close a point must be to be accepted atol: Absolute error for how close a point must be to be accepted Output True or False Notes Line can be degenerate and function still works to discern coinciding points from non-coinciding. Tolerances rtol and atol are used with numpy.allclose() """ # Prepare input data points = ensure_numeric(points) line = ensure_numeric(line) if len(points.shape) == 1: # One point only - make into 1 x 2 array points = points[numpy.newaxis, :] one_point = True else: one_point = False msg = 'Argument points must be either [x, y] or an Nx2 array of points' if len(points.shape) != 2: raise Exception(msg) if not points.shape[0] > 0: raise Exception(msg) if points.shape[1] != 2: raise Exception(msg) N = points.shape[0] # Number of points x = points[:, 0] y = points[:, 1] x0, y0 = line[0] x1, y1 = line[1] # Vector from beginning of line to point a0 = x - x0 a1 = y - y0 # It's normal vector a_normal0 = a1 a_normal1 = -a0 # Vector parallel to line b0 = x1 - x0 b1 = y1 - y0 # Dot product nominator = abs(a_normal0 * b0 + a_normal1 * b1) denominator = b0 * b0 + b1 * b1 # Determine if point vector is parallel to line up to a tolerance is_parallel = numpy.zeros(N, dtype=numpy.bool) # All False is_parallel[nominator <= atol + rtol * denominator] = True # Determine for points parallel to line if they are within end points a0p = a0[is_parallel] a1p = a1[is_parallel] len_a = numpy.sqrt(a0p * a0p + a1p * a1p) len_b = numpy.sqrt(b0 * b0 + b1 * b1) cross = a0p * b0 + a1p * b1 # Initialise result to all False result = numpy.zeros(N, dtype=numpy.bool) # Result is True only if a0 * b0 + a1 * b1 >= 0 and len_a <= len_b result[is_parallel] = (cross >= 0) * (len_a <= len_b) # Return either boolean scalar or boolean vector if one_point: return result[0] else: return result
def separate_points_by_polygon(points, polygon, closed=True, check_input=True, numpy_version=True): """Determine whether points are inside or outside a polygon Input: points - Tuple of (x, y) coordinates, or list of tuples polygon - list of vertices of polygon closed - (optional) determine whether points on boundary should be regarded as belonging to the polygon (closed = True) or not (closed = False) check_input: Allows faster execution if set to False Outputs: indices: array of same length as points with indices of points falling inside the polygon listed from the beginning and indices of points falling outside listed from the end. count: count of points falling inside the polygon The indices of points inside are obtained as indices[:count] The indices of points outside are obtained as indices[count:] Examples: U = [[0,0], [1,0], [1,1], [0,1]] # Unit square separate_points_by_polygon( [[0.5, 0.5], [1, -0.5], [0.3, 0.2]], U) will return the indices [0, 2, 1] and count == 2 as only the first and the last point are inside the unit square Remarks: The vertices may be listed clockwise or counterclockwise and the first point may optionally be repeated. Polygons do not need to be convex. Polygons can have holes in them and points inside a hole is regarded as being outside the polygon. Algorithm is based on work by Darel Finley, http://www.alienryderflex.com/polygon/ """ if check_input: # Input checks msg = 'Keyword argument "closed" must be boolean' if not isinstance(closed, bool): raise Exception(msg) try: points = ensure_numeric(points, numpy.float) except Exception, e: msg = ('Points could not be converted to numeric array: %s' % str(e)) raise Exception(msg) try: polygon = ensure_numeric(polygon, numpy.float) except Exception, e: msg = ('Polygon could not be converted to numeric array: %s' % str(e)) raise Exception(msg)