|
10 | 10 | import types |
11 | 11 |
|
12 | 12 | import geopandas as gpd |
| 13 | +import pandas as pd |
13 | 14 | import pytest |
14 | | -from shapely.geometry import Polygon |
| 15 | +from shapely.geometry import LineString, Point, Polygon |
15 | 16 |
|
16 | 17 | import cityImage.osm as osm |
17 | 18 | from cityImage.osm import ( |
@@ -156,3 +157,102 @@ def test_network_from_osm_validates_arguments(monkeypatch): |
156 | 157 | network_from_osm("X", download_method="bogus") |
157 | 158 | with pytest.raises(ValueError, match="distance is required"): |
158 | 159 | network_from_osm("X", download_method="distance_from_address") |
| 160 | + |
| 161 | + |
| 162 | +# --------------------------------------------------------------------------- graph conversion |
| 163 | + |
| 164 | + |
| 165 | +def _fake_network_ox(): |
| 166 | + """Fake OSMnx exposing a graph pipeline over a tiny 3-node / 2-edge network.""" |
| 167 | + nodes = gpd.GeoDataFrame( |
| 168 | + {"x": [0.0, 10.0, 20.0], "y": [0.0, 0.0, 10.0]}, |
| 169 | + geometry=[Point(0, 0), Point(10, 0), Point(20, 10)], |
| 170 | + index=[10, 20, 30], # OSM node ids |
| 171 | + crs=UTM, |
| 172 | + ) |
| 173 | + edge_idx = pd.MultiIndex.from_tuples([(10, 20, 0), (20, 30, 0)], names=["u", "v", "key"]) |
| 174 | + edges = gpd.GeoDataFrame( |
| 175 | + {"length": [10.0, 14.14], "highway": ["residential", "primary"], "name": ["A", "B"]}, |
| 176 | + geometry=[LineString([(0, 0), (10, 0)]), LineString([(10, 0), (20, 10)])], |
| 177 | + index=edge_idx, |
| 178 | + crs=UTM, |
| 179 | + ) |
| 180 | + graph = types.SimpleNamespace(graph={"crs": UTM}) |
| 181 | + |
| 182 | + def graph_to_gdfs(g, nodes=False, edges=False, **_): |
| 183 | + return nodes_frame.copy() if nodes else edges_frame.copy() |
| 184 | + |
| 185 | + nodes_frame, edges_frame = nodes, edges |
| 186 | + |
| 187 | + def graph_from(*_a, **_k): |
| 188 | + return graph |
| 189 | + |
| 190 | + return types.SimpleNamespace( |
| 191 | + graph_from_place=graph_from, |
| 192 | + graph_from_address=graph_from, |
| 193 | + graph_from_point=graph_from, |
| 194 | + graph_from_polygon=graph_from, |
| 195 | + project_graph=lambda g, to_crs=None: g, |
| 196 | + graph_to_gdfs=graph_to_gdfs, |
| 197 | + projection=types.SimpleNamespace(project_gdf=lambda gdf, **_: gdf), |
| 198 | + features_from_place=lambda q, tags=None: _building_features(tags), |
| 199 | + ) |
| 200 | + |
| 201 | + |
| 202 | +def test_network_from_osm_converts_graph_to_cityimage_schema(monkeypatch): |
| 203 | + monkeypatch.setattr(osm, "ox", _fake_network_ox()) |
| 204 | + nodes, edges = network_from_osm("Place", crs=UTM, dict_columns={"road_type": "highway"}) |
| 205 | + |
| 206 | + assert {"nodeID", "x", "y", "geometry"}.issubset(nodes.columns) |
| 207 | + assert {"u", "v", "edgeID", "length", "road_type"}.issubset(edges.columns) |
| 208 | + node_ids = set(nodes["nodeID"]) |
| 209 | + assert set(edges["u"]).issubset(node_ids) and set(edges["v"]).issubset(node_ids) |
| 210 | + assert edges["road_type"].tolist() == ["residential", "primary"] # dict_columns mapping applied |
| 211 | + |
| 212 | + |
| 213 | +@pytest.mark.parametrize( |
| 214 | + "download_method,query,distance", |
| 215 | + [ |
| 216 | + ("distance_from_address", "Addr", 500), |
| 217 | + ("distance_from_point", (0.0, 0.0), 500), |
| 218 | + ("polygon", Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]), None), |
| 219 | + ], |
| 220 | +) |
| 221 | +def test_network_from_osm_dispatches_each_graph_method( |
| 222 | + monkeypatch, download_method, query, distance |
| 223 | +): |
| 224 | + monkeypatch.setattr(osm, "ox", _fake_network_ox()) |
| 225 | + nodes, edges = network_from_osm( |
| 226 | + query, crs=UTM, download_method=download_method, distance=distance |
| 227 | + ) |
| 228 | + assert not nodes.empty and not edges.empty |
| 229 | + |
| 230 | + |
| 231 | +def test_network_from_osm_dict_columns_missing_column_raises(monkeypatch): |
| 232 | + monkeypatch.setattr(osm, "ox", _fake_network_ox()) |
| 233 | + with pytest.raises(ValueError, match="missing column"): |
| 234 | + network_from_osm("Place", crs=UTM, dict_columns={"road_type": "does_not_exist"}) |
| 235 | + |
| 236 | + |
| 237 | +def test_buildings_from_osm_projects_when_crs_is_none(monkeypatch): |
| 238 | + monkeypatch.setattr(osm, "ox", _fake_network_ox()) |
| 239 | + buildings = buildings_from_osm( |
| 240 | + "Place", crs=None, min_area=200 |
| 241 | + ) # triggers projection.project_gdf |
| 242 | + assert "buildingID" in buildings.columns and len(buildings) == 1 |
| 243 | + |
| 244 | + |
| 245 | +def test_buildings_from_osm_drops_below_min_area(monkeypatch): |
| 246 | + def _two_buildings(tags=None, **_): |
| 247 | + return gpd.GeoDataFrame( |
| 248 | + {"building": ["yes", "yes"]}, |
| 249 | + geometry=[ |
| 250 | + Polygon([(0, 0), (20, 0), (20, 20), (0, 20)]), # 400 m^2 -> kept |
| 251 | + Polygon([(100, 100), (101, 100), (101, 101), (100, 101)]), # 1 m^2 -> dropped |
| 252 | + ], |
| 253 | + crs=UTM, |
| 254 | + ) |
| 255 | + |
| 256 | + monkeypatch.setattr(osm, "ox", _fake_ox(_two_buildings)) |
| 257 | + buildings = buildings_from_osm("Place", crs=UTM, min_area=200) |
| 258 | + assert len(buildings) == 1 # the 1 m^2 footprint is filtered out |
0 commit comments