From dac0f104e5ae1d396556e87b9df8bb8139b422da Mon Sep 17 00:00:00 2001 From: Gareth Simons Date: Mon, 2 Dec 2024 17:55:30 +0000 Subject: [PATCH 1/2] handles empty or zero length geoms --- pyproject.toml | 2 +- pysrc/cityseer/tools/io.py | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 09fe183c..ea35ca84 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cityseer" -version = '4.17.1' +version = '4.17.2' description = "Computational tools for network-based pedestrian-scale urban analysis" readme = "README.md" requires-python = ">=3.10, <3.14" diff --git a/pysrc/cityseer/tools/io.py b/pysrc/cityseer/tools/io.py index bdb6ec0b..b2d4f9f2 100644 --- a/pysrc/cityseer/tools/io.py +++ b/pysrc/cityseer/tools/io.py @@ -1069,7 +1069,8 @@ def network_structure_from_nx( f"Expecting LineString geometry but found {line_geom.geom_type} geom for edge " f"{start_node_key}-{end_node_key}." ) - # cannot have zero or negative length - division by zero + if line_geom.is_empty: + raise TypeError(f"Found empty geom for edge {start_node_key}-{end_node_key}.") line_len = line_geom.length if not np.isfinite(line_len) or line_len <= 0: raise ValueError( @@ -1419,6 +1420,14 @@ def _node_key(node_coords): for edge_idx, edge_row in tqdm(gdf_network.iterrows(), total=len(gdf_network), disable=config.QUIET_MODE): # generate start and ending nodes edge_geom = edge_row[geom_key] + # drop empty edges + if edge_geom.is_empty: + logger.warning(f"Dropping empty edge at row index {edge_idx}") + continue + # drop zero length edges + if edge_geom.length == 0: + logger.warning(f"Dropping zero length edge at row index {edge_idx}") + continue # round to 1cm - assumes 1m units if len(edge_geom.coords[0]) == 3: edge_geom = geometry.LineString( From 2ab23e6b4046826688a18f78101f74c255e1f770 Mon Sep 17 00:00:00 2001 From: Gareth Simons Date: Thu, 12 Dec 2024 14:58:51 +0000 Subject: [PATCH 2/2] handle osmnx insufficient response errors --- .vscode/settings.json | 4 +++ pyproject.toml | 2 +- pysrc/cityseer/tools/io.py | 66 +++++++++++++++++++++--------------- pysrc/cityseer/tools/mock.py | 2 +- pysrc/cityseer/tools/plot.py | 3 +- 5 files changed, 46 insertions(+), 31 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 4af5cab9..23a759d2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -36,4 +36,8 @@ ], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, + "files.readonlyInclude": { + "**/.cargo/registry/src/**/*.rs": true, + "**/lib/rustlib/src/rust/library/**/*.rs": true, + } } diff --git a/pyproject.toml b/pyproject.toml index ea35ca84..9e5ada4e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cityseer" -version = '4.17.2' +version = '4.17.3' description = "Computational tools for network-based pedestrian-scale urban analysis" readme = "README.md" requires-python = ">=3.10, <3.14" diff --git a/pysrc/cityseer/tools/io.py b/pysrc/cityseer/tools/io.py index b2d4f9f2..a231e9d0 100644 --- a/pysrc/cityseer/tools/io.py +++ b/pysrc/cityseer/tools/io.py @@ -19,6 +19,7 @@ import osmnx as ox import pandas as pd import requests +from osmnx._errors import InsufficientResponseError from pyproj import CRS, Transformer from shapely import geometry from shapely.strtree import STRtree @@ -259,37 +260,46 @@ def _auto_clean_network( # deduplicate by hierarchy G = graphs.nx_deduplicate_edges(G, dissolve_distance=20, max_ang_diff=20) # parks - parks_gdf = ox.features_from_polygon( - geom_wgs, - tags={ - "landuse": ["cemetery", "forest"], - "leisure": ["park", "garden", "sports_centre"], - }, - ) - park_area_gdf = _extract_gdf(parks_gdf) - park_area_gdf = park_area_gdf.to_crs(to_crs_code) + try: + parks_gdf = ox.features_from_polygon( + geom_wgs, + tags={ + "landuse": ["cemetery", "forest"], + "leisure": ["park", "garden", "sports_centre"], + }, + ) + park_area_gdf = _extract_gdf(parks_gdf) + park_area_gdf = park_area_gdf.to_crs(to_crs_code) + except InsufficientResponseError: + park_area_gdf = gpd.GeoDataFrame(columns=["geometry"], geometry="geometry", crs=to_crs_code) # type: ignore # plazas - plazas_gdf = ox.features_from_polygon( - geom_wgs, - tags={ - "highway": ["pedestrian"], - }, - ) - plaza_area_gdf = _extract_gdf(plazas_gdf) - plaza_area_gdf = plaza_area_gdf.to_crs(to_crs_code) + try: + plazas_gdf = ox.features_from_polygon( + geom_wgs, + tags={ + "highway": ["pedestrian"], + }, + ) + plaza_area_gdf = _extract_gdf(plazas_gdf) + plaza_area_gdf = plaza_area_gdf.to_crs(to_crs_code) + except InsufficientResponseError: + plaza_area_gdf = gpd.GeoDataFrame(columns=["geometry"], geometry="geometry", crs=to_crs_code) # type: ignore # parking - parking_gdf = ox.features_from_polygon( - geom_wgs, - tags={ - "amenity": ["parking"], - }, - ) - parking_area_gdf = _extract_gdf(parking_gdf) - parking_area_gdf = parking_area_gdf.to_crs(to_crs_code) + try: + parking_gdf = ox.features_from_polygon( + geom_wgs, + tags={ + "amenity": ["parking"], + }, + ) + parking_area_gdf = _extract_gdf(parking_gdf) + parking_area_gdf = parking_area_gdf.to_crs(to_crs_code) + except InsufficientResponseError: + parking_area_gdf = gpd.GeoDataFrame(columns=["geometry"], geometry="geometry", crs=to_crs_code) # type: ignore # use STR Tree for performance - parks_buff_str_tree = STRtree(park_area_gdf.buffer(5).geometry.to_list()) - plaza_str_tree = STRtree(plaza_area_gdf.geometry.to_list()) - parking_str_tree = STRtree(parking_area_gdf.geometry.to_list()) + parks_buff_str_tree = STRtree(park_area_gdf.buffer(5).geometry.to_list()) # type: ignore + plaza_str_tree = STRtree(plaza_area_gdf.geometry.to_list()) # type: ignore + parking_str_tree = STRtree(parking_area_gdf.geometry.to_list()) # type: ignore # iter edges to find edges for marking remove_edges = [] for start_node_key, end_node_key, edge_key, edge_data in tqdm( # type: ignore diff --git a/pysrc/cityseer/tools/mock.py b/pysrc/cityseer/tools/mock.py index b0304fd5..4d113265 100644 --- a/pysrc/cityseer/tools/mock.py +++ b/pysrc/cityseer/tools/mock.py @@ -463,4 +463,4 @@ def mock_species_data( counts[idx] = (data == uniq).sum() probs = counts / len(data) - yield counts.tolist(), probs.tolist() + yield counts.tolist(), probs.tolist() # type: ignore diff --git a/pysrc/cityseer/tools/plot.py b/pysrc/cityseer/tools/plot.py index e51b9a18..8945a1ab 100644 --- a/pysrc/cityseer/tools/plot.py +++ b/pysrc/cityseer/tools/plot.py @@ -173,7 +173,8 @@ def _plot_graph( _edge_width: int | float | None, ) -> None: if not len(_graph): - raise ValueError("Graph contains no nodes to plot.") + logger.warning("Graph contains no nodes to plot.") + return if not isinstance(_node_size, int) or _node_size < 1: raise ValueError("Node sizes should be a positive integer.") if _node_colour is not None: