def cut_line_at_points(line, cut_points, tolerance=1e-6): """Cut a pygeos line geometry at points. If there are no interior points, the original line will be returned. Parameters ---------- line : pygeos Linestring cut_points : list-like of pygeos Points will be projected onto the line; those interior to the line will be used to cut the line in to new segments. tolerance : float, optional (default: 1e-6) minimum distance from endpoints to consider the points interior to the line. Returns ------- MultiLineStrings (or LineString, if unchanged) """ if not pg.get_type_id(line) == 1: raise ValueError("line is not a single linestring") vertices = pg.get_point(line, range(pg.get_num_points(line))) offsets = pg.line_locate_point(line, vertices) cut_offsets = pg.line_locate_point(line, cut_points) # only keep those that are interior to the line and ignore those very close # to endpoints or beyond endpoints cut_offsets = cut_offsets[(cut_offsets > tolerance) & (cut_offsets < offsets[-1] - tolerance)] if len(cut_offsets) == 0: # nothing to cut, return original return line # get coordinates of new vertices from the cut points (interpolated onto the line) cut_offsets.sort() # add in the last coordinate of the line cut_offsets = np.append(cut_offsets, offsets[-1]) # TODO: convert this to a pygos ufunc coords = pg.get_coordinates(line) cut_coords = pg.get_coordinates( pg.line_interpolate_point(line, cut_offsets)) lines = [] orig_ix = 0 for cut_ix in range(len(cut_offsets)): offset = cut_offsets[cut_ix] segment = [] if cut_ix > 0: segment = [cut_coords[cut_ix - 1]] while offsets[orig_ix] < offset: segment.append(coords[orig_ix]) orig_ix += 1 segment.append(cut_coords[cut_ix]) lines.append(pg.linestrings(segment)) return pg.multilinestrings(lines)
def transform_geometry(self, geom, rs, max_points=5): """Transforms a geometry embedding new points. In case geom is (multi)line or (multi)polygon, it adds points collinear to their neighbours, so that an equivalent geometry is generated. The number of extra points depends on the number of vertices in the geometry. Arguments: geom (pygeos.Geometry): Geometry rs (numpy.RandomState): Random State max_points (int): Maximum value of extra points. Returns: (pygeos.Geometry) Raises: ValueError: When geometry type is not supported. """ type_ = pg.get_type_id(geom) if type_ == 1 or type_ == 3: # LINESTRING or POLYGON vertices = pg.get_coordinates(geom) size = min(max_points, math.ceil(len(vertices) / 6)) vert_ids = rs.randint(1, len(vertices), size) vert_ids.sort() new = [] for idx in vert_ids: xa, ya = vertices[idx - 1] xb, yb = vertices[idx] if xa == xb: x = xa y = self._random_float(rs, ya, yb) else: x = self._random_float(rs, xa, xb) y = (yb - ya) * (x - xa) / (xb - xa) + ya x = _round(x, [xa, xb]) y = _round(y, [ya, yb]) new.append((idx, [x, y])) offset = 0 extended = [] for idx, entry in new: extended.extend(vertices[offset:idx]) extended.append(entry) offset = idx extended.extend(vertices[offset:]) extended = np.array(extended) result = pg.linestrings(extended) if type_ == 1 else pg.polygons( extended) elif type_ == 5 or type_ == 6: # MULTILINESTRING or MULTIPOLYGON parts = pg.get_parts(geom) part_idx = rs.randint(0, len(parts)) parts[part_idx] = self.transform_geometry(parts[part_idx], rs) result = pg.multilinestrings( parts) if type_ == 5 else pg.multipolygons(parts) else: raise ValueError( 'geom should be linestring, polygon, multilinestring, or multipolygon.' ) return result
def test_pickle(geom): if pygeos.get_type_id(geom) == 2: # Linearrings get converted to linestrings expected = pygeos.linestrings(pygeos.get_coordinates(geom)) else: expected = geom pickled = pickle.dumps(geom) assert pygeos.equals_exact(pickle.loads(pickled), expected)
def _get_extrapolated_line(coords, tolerance, point=False): """ Creates a pygeos line extrapoled in p1->p2 direction. """ p1 = coords[:2] p2 = coords[2:] a = p2 # defining new point based on the vector between existing points if p1[0] >= p2[0] and p1[1] >= p2[1]: b = ( p2[0] - tolerance * math.cos( math.atan( math.fabs(p1[1] - p2[1] + 0.000001) / math.fabs(p1[0] - p2[0] + 0.000001))), p2[1] - tolerance * math.sin( math.atan( math.fabs(p1[1] - p2[1] + 0.000001) / math.fabs(p1[0] - p2[0] + 0.000001))), ) elif p1[0] <= p2[0] and p1[1] >= p2[1]: b = ( p2[0] + tolerance * math.cos( math.atan( math.fabs(p1[1] - p2[1] + 0.000001) / math.fabs(p1[0] - p2[0] + 0.000001))), p2[1] - tolerance * math.sin( math.atan( math.fabs(p1[1] - p2[1] + 0.000001) / math.fabs(p1[0] - p2[0] + 0.000001))), ) elif p1[0] <= p2[0] and p1[1] <= p2[1]: b = ( p2[0] + tolerance * math.cos( math.atan( math.fabs(p1[1] - p2[1] + 0.000001) / math.fabs(p1[0] - p2[0] + 0.000001))), p2[1] + tolerance * math.sin( math.atan( math.fabs(p1[1] - p2[1] + 0.000001) / math.fabs(p1[0] - p2[0] + 0.000001))), ) else: b = ( p2[0] - tolerance * math.cos( math.atan( math.fabs(p1[1] - p2[1] + 0.000001) / math.fabs(p1[0] - p2[0] + 0.000001))), p2[1] + tolerance * math.sin( math.atan( math.fabs(p1[1] - p2[1] + 0.000001) / math.fabs(p1[0] - p2[0] + 0.000001))), ) if point: return b return pygeos.linestrings([a, b])
def split_edges_at_nodes_pyg(network, tolerance=1e-9): """Split network edges where they intersect node geometries """ #already initiate the spatial index, so we dont have to do that every time sindex = pygeos.STRtree(network.nodes['geometry']) grab_all_edges = [] for edge in tqdm(network.edges.itertuples(index=False), desc="split", total=len(network.edges)): hits = nodes_intersecting_pyg(edge.geometry, network.nodes['geometry'], sindex, tolerance=1e-9) if len(hits) < 3: grab_all_edges.append([[edge.osm_id], [edge.geometry], [edge.highway]]) continue # get points and geometry as list of coordinates split_points = pygeos.coordinates.get_coordinates( pygeos.snap(hits, edge.geometry, tolerance=1e-9)) coor_geom = pygeos.coordinates.get_coordinates(edge.geometry) # potentially split to multiple edges split_locs = np.argwhere(np.isin(coor_geom, split_points).all(axis=1))[:, 0] split_locs = list(zip(split_locs.tolist(), split_locs.tolist()[1:])) new_edges = [ coor_geom[split_loc[0]:split_loc[1] + 1] for split_loc in split_locs ] grab_all_edges.append( [[edge.osm_id] * len(new_edges), [pygeos.linestrings(edge) for edge in new_edges], [edge.infra_type] * len(new_edges)]) # combine all new edges edges = pd.DataFrame([ item for sublist in [list(zip(x[0], x[1], x[2])) for x in grab_all_edges] for item in sublist ], columns=['osm_id', 'geometry', 'infra_type']) # return new network with split edges return Network(nodes=network.nodes, edges=edges)
def pg_lines_wgs84(): size = 1000 line_length = 10 # number of vertices # generate some fields in the data frame f = random.sample(size) * 360 - 180 i = random.randint(-32767, 32767, size=size) ui = random.randint(0, 65535, size=size).astype("uint64") df = DataFrame(data={"f": f, "i": i, "ui": ui, "labels": i.astype("str")}) df["geometry"] = df.apply( lambda x: pg.linestrings(np.array(generate_lon_lat(line_length)).T), axis=1) return df
def cut_line_at_points(coords, cut_offsets): """Cut a pygeos line geometry at points. The points must be interior to the line. Parameters ---------- coords : ndarray of shape (2,n) line coordinates cut_points : list-like of offsets projected onto the line Returns ------- MultiLineStrings """ new_coords, line_ix = split_coords(coords, cut_offsets) return pg.multilinestrings(pg.linestrings(new_coords, indices=line_ix))
def occult(lines: LineCollection, tolerance: float) -> LineCollection: """ Remove occulted lines. The order of the geometries in 'lines' matters, see example below. 'tolerance' controls the distance tolerance between the first and last points of a geometry to consider it closed. Examples: $ vpype line 0 0 5 5 rect 2 2 1 1 occult show # line is occulted by rect $ vpype rect 2 2 1 1 line 0 0 5 5 occult show # line is NOT occulted by rect, as the line is drawn after the rectangle. """ line_arr = np.array( [pygeos.linestrings(list(zip(line.real, line.imag))) for line in lines] ) for i, line in enumerate(line_arr): coords = pygeos.get_coordinates(line) if math.hypot(coords[-1, 0] - coords[0, 0], coords[-1, 1] - coords[0, 1]) < tolerance: tree = pygeos.STRtree(line_arr[:i]) p = pygeos.polygons(coords) geom_idx = tree.query(p, predicate="intersects") line_arr[geom_idx] = pygeos.set_operations.difference(line_arr[geom_idx], p) new_lines = LineCollection() for geom in line_arr: for i in range(pygeos.get_num_geometries(geom)): coords = pygeos.get_coordinates(pygeos.get_geometry(geom, i)) new_lines.append(coords[:, 0] + coords[:, 1] * 1j) return new_lines
def test_project(): line = pygeos.linestrings([[0, 0], [1, 1], [2, 2]]) points = pygeos.points([1, 3], [0, 3]) actual = pygeos.project(line, points) expected = [0.5 * 2**0.5, 2 * 2**0.5] np.testing.assert_allclose(actual, expected)
def test_simplify(): line = pygeos.linestrings([[0, 0], [0.1, 1], [0, 2]]) actual = pygeos.simplify(line, [0, 1.0]) assert pygeos.get_num_points(actual).tolist() == [3, 2]
def test_snap(): line = pygeos.linestrings([[0, 0], [1, 0], [2, 0]]) points = pygeos.points([0, 1], [1, 0.1]) actual = pygeos.snap(points, line, 0.5) expected = pygeos.points([0, 1], [1, 0]) assert pygeos.equals(actual, expected).all()
def test_linestrings_invalid_shape(shape): with pytest.raises(ValueError): pygeos.linestrings(np.ones(shape))
def test_buffer_single_sided(): # buffer a line on one side line = pygeos.linestrings([[0, 0], [10, 0]]) actual = pygeos.buffer(line, 0.1, cap_style="square", single_sided=True) assert pygeos.area(actual) == pytest.approx(0.1 * 10, abs=0.01)
def test_linestrings_from_xy_broadcast(): x = [0, 1] # the same X coordinates for both linestrings y = [2, 3], [4, 5] # each linestring has a different set of Y coordinates actual = pygeos.linestrings(x, y) assert str(actual[0]) == "LINESTRING (0 2, 1 3)" assert str(actual[1]) == "LINESTRING (0 4, 1 5)"
def test_linestrings_from_xyz(): actual = pygeos.linestrings([0, 1], [2, 3], 0) assert str(actual) == "LINESTRING Z (0 2 0, 1 3 0)"
import pygeos import numpy as np from .common import empty_point from .common import empty_line_string from .common import point from .common import line_string from .common import linear_ring from .common import multi_line_string pygeos.linestrings([(0, 0), (1, 0), (1, 1)]) def test_line_interpolate_point_geom_array(): actual = pygeos.line_interpolate_point([line_string, linear_ring], -1) assert pygeos.equals(actual[0], pygeos.Geometry("POINT (1 0)")) assert pygeos.equals(actual[1], pygeos.Geometry("POINT (0 1)")) def test_line_interpolate_point_float_array(): actual = pygeos.line_interpolate_point(line_string, [0.2, 1.5, -0.2]) assert pygeos.equals(actual[0], pygeos.Geometry("POINT (0.2 0)")) assert pygeos.equals(actual[1], pygeos.Geometry("POINT (1 0.5)")) assert pygeos.equals(actual[2], pygeos.Geometry("POINT (1 0.8)")) def test_line_interpolate_point_empty(): assert pygeos.equals(pygeos.line_interpolate_point(empty_line_string, 0.2), empty_point)
def test_linestrings_from_xy(): actual = pygeos.linestrings([0, 1], [2, 3]) assert str(actual) == "LINESTRING (0 2, 1 3)"
def test_haussdorf_distance_densify(): # example from GEOS docs a = pygeos.linestrings([[0, 0], [100, 0], [10, 100], [10, 100]]) b = pygeos.linestrings([[0, 100], [0, 10], [80, 10]]) actual = pygeos.hausdorff_distance(a, b, densify=0.001) assert actual == pytest.approx(47.8, abs=0.1)
def test_shared_paths_non_linestring(): g1 = pygeos.linestrings([(0, 0), (1, 0), (1, 1)]) g2 = pygeos.points(0, 1) with pytest.raises(pygeos.GEOSException): pygeos.shared_paths(g1, g2)
def __init__(self, left, right, heights=None, distance=10, tick_length=50, verbose=True): self.left = left self.right = right self.distance = distance self.tick_length = tick_length pygeos_lines = left.geometry.values.data list_points = np.empty((0, 2)) ids = [] end_markers = [] lengths = pygeos.length(pygeos_lines) for ix, (line, length) in enumerate(zip(pygeos_lines, lengths)): pts = pygeos.line_interpolate_point( line, np.linspace(0, length, num=int((length) // distance))) list_points = np.append(list_points, pygeos.get_coordinates(pts), axis=0) if len(pts) > 1: ids += [ix] * len(pts) * 2 markers = [True] + ([False] * (len(pts) - 2)) + [True] end_markers += markers elif len(pts) == 1: end_markers += [True] ids += [ix] * 2 ticks = [] for num, (pt, end) in enumerate(zip(list_points, end_markers), 1): if end: ticks.append([pt, pt]) ticks.append([pt, pt]) else: angle = self._getAngle(pt, list_points[num]) line_end_1 = self._getPoint1(pt, angle, tick_length / 2) angle = self._getAngle(line_end_1, pt) line_end_2 = self._getPoint2(line_end_1, angle, tick_length) ticks.append([line_end_1, pt]) ticks.append([line_end_2, pt]) ticks = pygeos.linestrings(ticks) inp, res = right.sindex.query_bulk(ticks, predicate="intersects") intersections = pygeos.intersection(ticks[inp], right.geometry.values.data[res]) distances = pygeos.distance(intersections, pygeos.points(list_points[inp // 2])) inp_uni, inp_cts = np.unique(inp, return_counts=True) splitter = np.cumsum(inp_cts)[:-1] dist_per_res = np.split(distances, splitter) inp_per_res = np.split(res, splitter) min_distances = [] min_inds = [] for dis, ind in zip(dist_per_res, inp_per_res): min_distances.append(np.min(dis)) min_inds.append(ind[np.argmin(dis)]) dists = np.zeros((len(ticks), )) dists[:] = np.nan dists[inp_uni] = min_distances if heights is not None: if isinstance(heights, str): heights = self.heights = right[heights] elif not isinstance(heights, pd.Series): heights = self.heights = pd.Series(heights) blgs = np.zeros((len(ticks), )) blgs[:] = None blgs[inp_uni] = min_inds do_heights = True else: do_heights = False ids = np.array(ids) widths = [] openness = [] deviations = [] heights_list = [] heights_deviations_list = [] for i in range(len(left)): f = ids == i s = dists[f] lefts = s[::2] rights = s[1::2] left_mean = np.nanmean( lefts) if ~np.isnan(lefts).all() else tick_length / 2 right_mean = (np.nanmean(rights) if ~np.isnan(rights).all() else tick_length / 2) widths.append(np.mean([left_mean, right_mean]) * 2) openness.append(np.isnan(s).sum() / (f).sum()) deviations.append(np.nanstd(s)) if do_heights: b = blgs[f] h = heights.iloc[b[~np.isnan(b)]] heights_list.append(h.mean()) heights_deviations_list.append(h.std()) self.w = pd.Series(widths, index=left.index) self.wd = pd.Series(deviations, index=left.index).fillna( 0) # fill for empty intersections self.o = pd.Series(openness, index=left.index).fillna(1) if do_heights: self.h = pd.Series(heights_list, index=left.index).fillna( 0) # fill for empty intersections self.hd = pd.Series(heights_deviations_list, index=left.index).fillna( 0) # fill for empty intersections self.p = self.h / self.w.replace(0, np.nan) # replace to avoid np.inf
def line_tree(): x = np.arange(10) y = np.arange(10) offset = 1 geoms = pygeos.linestrings(np.array([[x, x + offset], [y, y + offset]]).T) yield pygeos.STRtree(geoms)
def find_dam_face_from_waterbody(waterbody, drain_pt): total_area = pg.area(waterbody) ring = pg.get_exterior_ring(pg.normalize(waterbody)) total_length = pg.length(ring) num_pts = pg.get_num_points(ring) - 1 # drop closing coordinate vertices = pg.get_point(ring, range(num_pts)) ### Extract line segments that are no more than 1/3 coordinates of polygon # starting from the vertex nearest the drain # note: lower numbers are to the right tree = pg.STRtree(vertices) ix = tree.nearest(drain_pt)[1][0] side_width = min(num_pts // 3, MAX_SIDE_PTS) left_ix = ix + side_width right_ix = ix - side_width # extract these as a left-to-write line; pts = vertices[max(right_ix, 0):min(num_pts, left_ix)][::-1] if left_ix >= num_pts: pts = np.append(vertices[0:left_ix - num_pts][::-1], pts) if right_ix < 0: pts = np.append(pts, vertices[num_pts + right_ix:num_pts][::-1]) coords = pg.get_coordinates(pts) if len(coords) > 2: # first run a simplification process to extract the major shape and bends # then run the straight line algorithm simp_coords, simp_ix = simplify_vw( coords, min(MAX_SIMPLIFY_AREA, total_area / 100)) if len(simp_coords) > 2: keep_coords, ix = extract_straight_segments( simp_coords, max_angle=MAX_STRAIGHT_ANGLE, loops=5) keep_ix = simp_ix.take(ix) else: keep_coords = simp_coords keep_ix = simp_ix else: keep_coords = coords keep_ix = np.arange(len(coords)) ### Calculate the length of each run and drop any that are not sufficiently long lengths = segment_length(keep_coords) ix = (lengths >= MIN_DAM_WIDTH) & (lengths / total_length < MAX_WIDTH_RATIO) pairs = np.dstack([keep_ix[:-1][ix], keep_ix[1:][ix]])[0] # since ranges are ragged, we have to do this in a loop instead of vectorized segments = [] for start, end in pairs: segments.append(pg.linestrings(coords[start:end + 1])) segments = np.array(segments) # only keep the segments that are close to the drain segments = segments[ pg.intersects(segments, pg.buffer(drain_pt, MAX_DRAIN_DIST)), ] if not len(segments): return segments # only keep those where the drain is interior to the line pos = pg.line_locate_point(segments, drain_pt) lengths = pg.length(segments) ix = (pos >= MIN_INTERIOR_DIST) & (pos <= (lengths - MIN_INTERIOR_DIST)) return segments[ix]
actual = pygeos.distance(*point_polygon_testdata) expected = [2 * 2**0.5, 2**0.5, 0, 0, 0, 2**0.5] np.testing.assert_allclose(actual, expected) def test_distance_missing(): actual = pygeos.distance(point, None) assert np.isnan(actual) @pytest.mark.parametrize( "geom,expected", [ (point, [2, 3, 2, 3]), ([point, multi_point], [[2, 3, 2, 3], [0, 0, 1, 2]]), (pygeos.linestrings([[0, 0], [0, 1]]), [0, 0, 0, 1]), (pygeos.linestrings([[0, 0], [1, 0]]), [0, 0, 1, 0]), (multi_point, [0, 0, 1, 2]), (multi_polygon, [0, 0, 2.2, 2.2]), (geometry_collection, [49, -1, 52, 2]), (empty, [np.nan, np.nan, np.nan, np.nan]), (None, [np.nan, np.nan, np.nan, np.nan]), ], ) def test_bounds(geom, expected): assert_array_equal(pygeos.bounds(geom), expected) @pytest.mark.parametrize( "geom,shape", [
def test_set_nan(): # As NaN != NaN, you can have multiple "NaN" points in a set # set([float("nan"), float("nan")]) also returns a set with 2 elements a = set(pygeos.linestrings([[[np.nan, np.nan], [np.nan, np.nan]]] * 10)) assert len(a) == 10 # different objects: NaN != NaN
def test_haussdorf_distance(): # example from GEOS docs a = pygeos.linestrings([[0, 0], [100, 0], [10, 100], [10, 100]]) b = pygeos.linestrings([[0, 100], [0, 10], [80, 10]]) actual = pygeos.hausdorff_distance(a, b) assert actual == pytest.approx(22.360679775, abs=1e-7)
def test_linestrings(coordinates, indices, expected): actual = pygeos.linestrings(coordinates, indices=indices) assert_geometries_equal(actual, expected)
def test_shared_paths_linestring(): g1 = pygeos.linestrings([(0, 0), (1, 0), (1, 1)]) g2 = pygeos.linestrings([(0, 0), (1, 0)]) actual1 = pygeos.shared_paths(g1, g2) assert pygeos.equals(pygeos.get_geometry(actual1, 0), g2)
def test_linestrings_invalid(): # attempt to construct linestrings with 1 coordinate with pytest.raises(pygeos.GEOSException): pygeos.linestrings([[1, 1], [2, 2]], indices=[0, 1])
import numpy as np import pygeos point_polygon_testdata = ( pygeos.points(np.arange(6), np.arange(6)), pygeos.box(2, 2, 4, 4), ) point = pygeos.points(2, 3) line_string = pygeos.linestrings([(0, 0), (1, 0), (1, 1)]) linear_ring = pygeos.linearrings([(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)]) polygon = pygeos.polygons([(0, 0), (2, 0), (2, 2), (0, 2), (0, 0)]) multi_point = pygeos.multipoints([(0, 0), (1, 2)]) multi_line_string = pygeos.multilinestrings([[(0, 0), (1, 2)]]) multi_polygon = pygeos.multipolygons([ [(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)], [(2.1, 2.1), (2.2, 2.1), (2.2, 2.2), (2.1, 2.2), (2.1, 2.1)], ]) geometry_collection = pygeos.geometrycollections( [pygeos.points(51, -1), pygeos.linestrings([(52, -1), (49, 2)])]) point_z = pygeos.points(1.0, 1.0, 1.0) polygon_with_hole = pygeos.Geometry( "POLYGON((0 0, 0 10, 10 10, 10 0, 0 0), (2 2, 2 4, 4 4, 4 2, 2 2))") all_types = ( point, line_string, linear_ring, polygon, multi_point, multi_line_string,
def test_linestrings_from_coords(): actual = pygeos.linestrings([[[0, 0], [1, 1]], [[0, 0], [2, 2]]]) assert str(actual[0]) == "LINESTRING (0 0, 1 1)" assert str(actual[1]) == "LINESTRING (0 0, 2 2)"