Source code for cityImage.io
"""File IO bridge helpers for cityImage.
GeoPandas owns file reading. cityImage owns conversion from loaded raw
GeoDataFrames into cityImage schemas and downstream semantics.
"""
from __future__ import annotations
from collections.abc import Mapping, Sequence
from typing import Any
import geopandas as gpd
from .adapters import standardize_buildings_gdf
from .buildings import select_buildings_by_study_area
from .geometry import gdf_multipolygon_to_polygon
from .network import network_from_lines
from .schema import LAND_USES_RAW
def _normalise_crs(crs: Any) -> Any:
"""Accept integer EPSG codes as a convenience."""
return f"EPSG:{crs}" if isinstance(crs, int) else crs
def _polygonal_buildings(buildings: gpd.GeoDataFrame) -> gpd.GeoDataFrame:
"""Keep valid polygonal building geometries."""
return buildings[
buildings.geometry.notna()
& ~buildings.geometry.is_empty
& buildings.geometry.geom_type.isin(["Polygon", "MultiPolygon"])
].copy()
[docs]
def network_from_file(
input_path: str,
crs: Any,
*,
dict_columns: Mapping[str, str | None] | None = None,
other_columns: Sequence[str] | None = None,
) -> tuple[gpd.GeoDataFrame, gpd.GeoDataFrame]:
"""Load line geometries from file and convert them to cityImage network schema.
Parameters
----------
input_path : str
Path to a vector file readable by GeoPandas.
crs : Any
Target CRS for output nodes and edges.
dict_columns : Mapping[str, str | None], optional
Mapping from cityImage edge columns to source columns.
other_columns : Sequence[str], optional
Additional source columns to preserve on the output edges.
Returns
-------
tuple[geopandas.GeoDataFrame, geopandas.GeoDataFrame]
Nodes and edges in cityImage schema.
"""
crs = _normalise_crs(crs)
edges_raw = gpd.read_file(input_path)
return network_from_lines(
edges_raw,
crs,
dict_columns=dict_columns,
other_columns=other_columns or [],
)
[docs]
def buildings_from_file(
input_path: str,
crs: Any,
*,
case_study_area: Any = None,
distance_from_center: float | None = None,
height_field: str | None = None,
base_field: str | None = None,
land_uses_raw_field: str | None = None,
min_area: float = 200,
min_height: float = 5,
) -> gpd.GeoDataFrame:
"""Load building polygons from file and convert them to cityImage schema.
GeoPandas handles file reading and CRS conversion. cityImage standardises
identifiers, area, height/base defaults, and source/provenance land-use
columns.
"""
crs = _normalise_crs(crs)
buildings = gpd.read_file(input_path).to_crs(crs).copy()
buildings = _polygonal_buildings(buildings)
buildings["area"] = buildings.geometry.area
buildings = buildings[buildings["area"] >= min_area].copy()
if height_field is not None and height_field not in buildings.columns:
raise ValueError(f"height_field {height_field!r} not found in input file")
if base_field is not None and base_field not in buildings.columns:
raise ValueError(f"base_field {base_field!r} not found in input file")
if land_uses_raw_field is not None and land_uses_raw_field not in buildings.columns:
raise ValueError(f"land_uses_raw_field {land_uses_raw_field!r} not found in input file")
if height_field is not None:
buildings["height"] = buildings[height_field]
elif "height" not in buildings.columns:
buildings["height"] = min_height
if base_field is not None:
buildings["base"] = buildings[base_field]
elif "base" not in buildings.columns:
buildings["base"] = 0.0
land_uses_raw_column = land_uses_raw_field
if land_uses_raw_column is None and LAND_USES_RAW in buildings.columns:
land_uses_raw_column = LAND_USES_RAW
if "buildingID" not in buildings.columns:
buildings = buildings.reset_index(drop=True)
buildings["buildingID"] = buildings.index.astype(int)
buildings = gdf_multipolygon_to_polygon(buildings, columnID="buildingID")
buildings = standardize_buildings_gdf(
buildings,
building_id_column="buildingID",
land_uses_raw_column=land_uses_raw_column,
validate=False,
)
if case_study_area is not None:
buildings = select_buildings_by_study_area(
buildings,
method="polygon",
polygon=case_study_area,
)
elif distance_from_center not in (None, 0):
buildings = select_buildings_by_study_area(
buildings,
method="distance",
distance=float(distance_from_center),
)
return buildings.reset_index(drop=True)