-
Notifications
You must be signed in to change notification settings - Fork 6
Add inject_points for projecting external points onto the network #305
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
514d8d9
85b5c12
ee80cdb
23678cf
f58d568
56c0a49
a57c0b4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ | |
| consolidate_nodes, | ||
| fix_topology, | ||
| induce_nodes, | ||
| inject_points, | ||
| remove_interstitial_nodes, | ||
| split, | ||
| ) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -323,6 +323,10 @@ def induce_nodes(streets: gpd.GeoDataFrame, *, eps: float = 1e-4) -> gpd.GeoData | |
| ------- | ||
| geopandas.GeoDataFrame | ||
| Updated ``streets`` with (potentially) added nodes. | ||
|
|
||
| See Also | ||
| -------- | ||
| inject_points : add nodes from external Point features (POIs). | ||
| """ | ||
|
|
||
| sindex_kws = {"predicate": "dwithin", "distance": 1e-4} | ||
|
|
@@ -343,6 +347,78 @@ def induce_nodes(streets: gpd.GeoDataFrame, *, eps: float = 1e-4) -> gpd.GeoData | |
| return split(nodes_to_induce.geometry, streets, streets.crs, eps=eps) | ||
|
|
||
|
|
||
| def inject_points( | ||
| streets: gpd.GeoDataFrame, | ||
| points: gpd.GeoDataFrame, | ||
| *, | ||
| snap_radius: float | None = None, | ||
| eps: float = 1e-4, | ||
| ) -> gpd.GeoDataFrame: | ||
| """Project external points onto the nearest LineString and split there. | ||
|
|
||
| For every input point within ``snap_radius`` of any LineString in | ||
| ``streets``, the point is projected onto the nearest LineString and the | ||
| target line is split at that projected location. Points outside the | ||
| radius are ignored. When the CRS differs, ``points`` is reprojected to | ||
| match ``streets`` before processing. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| streets : geopandas.GeoDataFrame | ||
| LineString network. CRS must be set. | ||
| points : geopandas.GeoDataFrame | ||
| Point features to project onto ``streets``. CRS must be set; will | ||
| be reprojected to match ``streets`` if different. | ||
| snap_radius : float | None = None | ||
| Maximum projection distance, in ``streets`` CRS units. Points | ||
| beyond this distance from any LineString are dropped from the | ||
| result. ``None`` keeps every point regardless of distance. | ||
| eps : float = 1e-4 | ||
| Tolerance epsilon passed to :func:`split` for the actual snap. | ||
|
|
||
| Returns | ||
| ------- | ||
| geopandas.GeoDataFrame | ||
| ``streets`` with the relevant LineStrings split at projection | ||
| locations. | ||
|
|
||
| See Also | ||
| -------- | ||
| induce_nodes : symmetric case where the new node comes from a LineString endpoint. | ||
| split : underlying split-at-points primitive. | ||
| """ | ||
| if streets.crs is None or points.crs is None: | ||
| raise ValueError("Both streets and points must have a CRS set.") | ||
| if points.crs != streets.crs: | ||
| points = points.to_crs(streets.crs) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would do this here as we do in |
||
|
|
||
| line_geoms = streets.geometry.values | ||
| point_geoms = points.geometry.values | ||
| if len(point_geoms) == 0 or len(line_geoms) == 0: | ||
| return streets.copy() | ||
|
|
||
| # Nearest line per input point (sindex-accelerated) | ||
| input_idx, line_idx = streets.sindex.nearest(point_geoms, return_all=False) | ||
| distances = shapely.distance(line_geoms[line_idx], point_geoms[input_idx]) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| if snap_radius is not None: | ||
| keep = distances <= snap_radius | ||
| input_idx = input_idx[keep] | ||
| line_idx = line_idx[keep] | ||
|
|
||
| if len(input_idx) == 0: | ||
| return streets.copy() | ||
|
|
||
| # Project each accepted point onto its nearest LineString (foot of perpendicular) | ||
| accepted_lines = line_geoms[line_idx] | ||
| accepted_points = point_geoms[input_idx] | ||
| proj_dists = shapely.line_locate_point(accepted_lines, accepted_points) | ||
| projected = shapely.line_interpolate_point(accepted_lines, proj_dists) | ||
|
|
||
| projected_series = gpd.GeoSeries(projected, crs=streets.crs) | ||
| return split(projected_series, streets, streets.crs, eps=eps) | ||
|
|
||
|
|
||
| def _identify_degree_mismatch( | ||
| edges: gpd.GeoDataFrame, sindex_kws: dict | ||
| ) -> gpd.GeoSeries: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if
snap_radiusis the right name as we're technically not snapping. Maybe keep justradius?