def interpolate(data, distance, normalized=False): if compat.USE_PYGEOS: try: return pygeos.line_interpolate_point(data, distance, normalized=normalized) except TypeError: # support for pygeos<0.9 return pygeos.line_interpolate_point(data, distance, normalize=normalized) else: out = np.empty(len(data), dtype=object) if isinstance(distance, np.ndarray): if len(distance) != len(data): raise ValueError( "Length of distance sequence does not match " "length of the GeoSeries" ) with compat.ignore_shapely2_warnings(): out[:] = [ geom.interpolate(dist, normalized=normalized) for geom, dist in zip(data, distance) ] return out with compat.ignore_shapely2_warnings(): out[:] = [ geom.interpolate(distance, normalized=normalized) for geom in data ] return out
def test_line_interpolate_point_empty(geom, normalized): # These geometries segfault in some versions of GEOS (in 3.8.0, still # some of them segfault). Instead, we patched this to return POINT EMPTY. # This matches GEOS 3.8.0 behavior on simple empty geometries. assert pygeos.equals( pygeos.line_interpolate_point(geom, 0.2, normalized=normalized), empty_point)
def test_line_interpolate_point_geom_array_normalized(): actual = pygeos.line_interpolate_point( [line_string, linear_ring, multi_line_string], 1, normalized=True ) assert pygeos.equals(actual[0], pygeos.Geometry("POINT (1 1)")) assert pygeos.equals(actual[1], pygeos.Geometry("POINT (0 0)")) assert pygeos.equals(actual[2], pygeos.Geometry("POINT (1 2)"))
def _dense_point_array(self, geoms, distance, index): """ geoms - array of pygeos lines """ # interpolate lines to represent them as points for Voronoi points = np.empty((0, 2)) ids = [] if pygeos.get_type_id(geoms[0]) not in [1, 2, 5]: lines = pygeos.boundary(geoms) else: lines = geoms lengths = pygeos.length(lines) for ix, line, length in zip(index, lines, lengths): if length > distance: # some polygons might have collapsed pts = pygeos.line_interpolate_point( line, np.linspace(0.1, length - 0.1, num=int((length - 0.1) // distance)), ) # .1 offset to keep a gap between two segments points = np.append(points, pygeos.get_coordinates(pts), axis=0) ids += [ix] * len(pts) return points, ids
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 test_line_interpolate_point_geom_array(): actual = pygeos.line_interpolate_point( [line_string, linear_ring, multi_line_string], -1) assert pygeos.equals(actual[0], pygeos.Geometry("POINT (1 0)")) assert pygeos.equals(actual[1], pygeos.Geometry("POINT (0 1)")) assert pygeos.equals_exact(actual[2], pygeos.Geometry("POINT (0.5528 1.1056)"), tolerance=0.001)
def interpolate(data, distance, normalized=False): if compat.USE_PYGEOS: return pygeos.line_interpolate_point(data, distance, normalize=normalized) else: out = np.empty(len(data), dtype=object) if isinstance(distance, np.ndarray): if len(distance) != len(data): raise ValueError( "Length of distance sequence does not match " "length of the GeoSeries" ) out[:] = [ geom.interpolate(dist, normalized=normalized) for geom, dist in zip(data, distance) ] return out out[:] = [geom.interpolate(distance, normalized=normalized) for geom in data] return out
def street_profile(streets, buildings, distance=3, tick_length=50): pygeos_lines = streets.geometry.values.data list_points = np.empty((0, 2)) ids = [] 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)) ) # .1 offset to keep a gap between two segments list_points = np.append(list_points, pygeos.get_coordinates(pts), axis=0) ids += [ix] * len(pts) * 2 ticks = [] for num, pt in enumerate(list_points, 1): # start chainage 0 if num == 1: angle = _getAngle(pt, list_points[num]) line_end_1 = _getPoint1(pt, angle, tick_length / 2) angle = _getAngle(line_end_1, pt) line_end_2 = _getPoint2(line_end_1, angle, tick_length) ticks.append([line_end_1, pt]) ticks.append([line_end_2, pt]) # everything in between if num < len(list_points) - 1: angle = _getAngle(pt, list_points[num]) line_end_1 = _getPoint1( list_points[num], angle, tick_length / 2 ) angle = _getAngle(line_end_1, list_points[num]) line_end_2 = _getPoint2(line_end_1, angle, tick_length) ticks.append([line_end_1, list_points[num]]) ticks.append([line_end_2, list_points[num]]) # end chainage if num == len(list_points): angle = _getAngle(list_points[num - 2], pt) line_end_1 = _getPoint1(pt, angle, tick_length / 2) angle = _getAngle(line_end_1, pt) line_end_2 = _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 = pygeos.STRtree(ticks).query_bulk(buildings.geometry.values.data, predicate='intersects') intersections = pygeos.intersection(ticks[res], buildings.geometry.values.data[inp]) distances = pygeos.distance(intersections, pygeos.points(list_points[res // 2])) dists = np.zeros((len(ticks),)) dists[:] = np.nan dists[res] = distances ids = np.array(ids) widths = [] openness = [] deviations = [] for i in range(len(streets)): 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)) return (widths, deviations, openness)
def test_line_interpolate_point_nan(): assert pygeos.line_interpolate_point(line_string, np.nan) is None
def test_line_interpolate_point_none(): assert pygeos.line_interpolate_point(None, 0.2) is None
def test_line_interpolate_point_empty(): assert pygeos.equals( pygeos.line_interpolate_point(empty_line_string, 0.2), empty_point )
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_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 __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 test_line_interpolate_point_invalid_type(geom, normalized): with pytest.raises(TypeError): assert pygeos.line_interpolate_point(geom, 0.2, normalized=normalized)
def snap_to_flowlines(df, to_snap): """Snap to nearest flowline, within tolerance Updates df with snapping results, and returns to_snap as set of dams still needing to be snapped after this operation. Parameters ---------- df : GeoDataFrame master dataset, this is where all snapping gets recorded to_snap : DataFrame data frame containing pygeos geometries to snap ("geometry") and snapping tolerance ("snap_tolerance") Returns ------- tuple of (GeoDataFrame, DataFrame) (df, to_snap) """ for region, HUC2s in list(REGION_GROUPS.items()): region_start = time() print("\n----- {} ------\n".format(region)) print("Reading flowlines...") flowlines = from_geofeather( nhd_dir / "clean" / region / "flowlines.feather" ).set_index("lineID") in_region = to_snap.loc[to_snap.HUC2.isin(HUC2s)] print( "Selected {:,} barriers in region to snap against {:,} flowlines".format( len(in_region), len(flowlines) ) ) if len(in_region) == 0: print("No barriers in region to snap") continue print("Finding nearest flowlines...") # TODO: can use near instead of nearest, and persist list of near lineIDs per barrier # so that we can construct subnetworks with just those lines = nearest( in_region.geometry, flowlines.geometry, in_region.snap_tolerance ) lines = lines.join(in_region.geometry).join( flowlines.geometry.rename("line"), on="lineID", ) # project the point to the line, # find out its distance on the line, # then interpolate its new coordinates lines["geometry"] = pg.line_interpolate_point( lines.line, pg.line_locate_point(lines.line, lines.geometry) ) ix = lines.index df.loc[ix, "snapped"] = True df.loc[ix, "geometry"] = lines.geometry df.loc[ix, "snap_dist"] = lines.distance df.loc[ix, "snap_ref_id"] = lines.lineID df.loc[ix, "lineID"] = lines.lineID df.loc[ix, "snap_log"] = ndarray_append_strings( "snapped: within ", to_snap.loc[ix].snap_tolerance, "m tolerance of flowline", ) to_snap = to_snap.loc[~to_snap.index.isin(ix)].copy() print( "{:,} barriers snapped in region in {:.2f}s".format( len(ix), time() - region_start ) ) # TODO: flag those that joined to loops return df, to_snap
def snap_to_flowlines(df, to_snap): """Snap to nearest flowline, within tolerance Updates df with snapping results, and returns to_snap as set of dams still needing to be snapped after this operation. If dams are within SNAP_ENDPOINT_TOLERANCE of the endpoints of the line, they will be snapped to the endpoint instead of closest point on line. Parameters ---------- df : GeoDataFrame master dataset, this is where all snapping gets recorded to_snap : DataFrame data frame containing pygeos geometries to snap ("geometry") and snapping tolerance ("snap_tolerance") Returns ------- tuple of (GeoDataFrame, DataFrame) (df, to_snap) """ print("=================\nSnapping to flowlines...") for huc2 in sorted(to_snap.HUC2.unique()): region_start = time() print(f"\n----- {huc2} ------") in_huc2 = to_snap.loc[to_snap.HUC2 == huc2].copy() flowlines = gp.read_feather( nhd_dir / "clean" / huc2 / "flowlines.feather", columns=["geometry", "lineID"], ).set_index("lineID") print( f"HUC {huc2} selected {len(in_huc2):,} barriers in region to snap against {len(flowlines):,} flowlines" ) lines = nearest( pd.Series(in_huc2.geometry.values.data, index=in_huc2.index), pd.Series(flowlines.geometry.values.data, index=flowlines.index), in_huc2.snap_tolerance.values, ) lines = lines.join(in_huc2.geometry).join( flowlines.geometry.rename("line"), on="lineID", ) # project the point to the line, # find out its distance on the line, lines["line_pos"] = pg.line_locate_point(lines.line.values.data, lines.geometry.values.data) # if within tolerance of start point, snap to start ix = lines["line_pos"] <= SNAP_ENDPOINT_TOLERANCE lines.loc[ix, "line_pos"] = 0 # if within tolerance of endpoint, snap to end end = pg.length(lines.line.values.data) ix = lines["line_pos"] >= end - SNAP_ENDPOINT_TOLERANCE lines.loc[ix, "line_pos"] = end[ix] # then interpolate its new coordinates lines["geometry"] = pg.line_interpolate_point(lines.line.values.data, lines["line_pos"]) ix = lines.index df.loc[ix, "snapped"] = True df.loc[ix, "geometry"] = lines.geometry df.loc[ix, "snap_dist"] = lines.distance df.loc[ix, "snap_ref_id"] = lines.lineID df.loc[ix, "lineID"] = lines.lineID df.loc[ix, "snap_log"] = ndarray_append_strings( "snapped: within ", to_snap.loc[ix].snap_tolerance, "m tolerance of flowline", ) to_snap = to_snap.loc[~to_snap.index.isin(ix)].copy() print("{:,} barriers snapped in region in {:.2f}s".format( len(ix), time() - region_start)) # TODO: flag those that joined to loops return df, to_snap