Skip to content

Urban Layer

What is the Urban Layer's module?

The urban_layer module is responsible for the spatial canvases on which your datasets are displayed. These layers provide structure for urban insights, such as mapping taxi trips to busy intersections or analysing neighbourhood demographics.

Meanwhile, we recommend to look through the Example's Urban layer for a more hands-on introduction about the Urban layer module and its usage.

Documentation Under Alpha Construction

This documentation is in its early stages and still being developed. The API may therefore change, and some parts might be incomplete or inaccurate.

Use at your own risk, and please report anything that seems incorrect / outdated you find.

Open An Issue!

UrbanLayerBase

Bases: ABC

Abstract base class for all urban layers

This abstract class defines the interface that all urban layer implementations must follow. Urban layers represent spatial layers (dataset as GeoDataframe) used for geographical analysis and mapping within UrbanMapper, such as streets, regions, or custom layers.

Attributes:

Name Type Description
layer GeoDataFrame | None

The GeoDataFrame containing the spatial data.

mappings List[Dict[str, object]]

List of mapping configurations for relating this layer to datasets (bridging layer <-> dataset).

coordinate_reference_system str

The coordinate reference system used by this layer. Default: EPSG:4326.

has_mapped bool

Indicates whether this layer has been mapped to another dataset.

Examples:

>>> from urban_mapper import UrbanMapper
>>> mapper = UrbanMapper()
>>> streets = mapper.urban_layer.osmnx_streets().from_place("London, UK")
>>> # This is an abstract class; use concrete implementations like OSMNXStreets
Source code in src/urban_mapper/modules/urban_layer/abc_urban_layer.py
@beartype
class UrbanLayerBase(ABC):
    """Abstract base class for all urban layers

    This abstract class defines the interface that all urban layer implementations
    must follow. Urban layers represent spatial layers (dataset as `GeoDataframe`) used for geographical
    analysis and mapping within `UrbanMapper`, such as `streets`, `regions`, or custom layers.

    Attributes:
        layer (gpd.GeoDataFrame | None): The `GeoDataFrame` containing the spatial data.
        mappings (List[Dict[str, object]]): List of mapping configurations for relating this layer to datasets (bridging layer <-> dataset).
        coordinate_reference_system (str): The coordinate reference system used by this layer. Default: EPSG:4326.
        has_mapped (bool): Indicates whether this layer has been mapped to another dataset.

    Examples:
        >>> from urban_mapper import UrbanMapper
        >>> mapper = UrbanMapper()
        >>> streets = mapper.urban_layer.osmnx_streets().from_place("London, UK")
        >>> # This is an abstract class; use concrete implementations like OSMNXStreets
    """

    def __init__(self) -> None:
        self.layer: gpd.GeoDataFrame | None = None
        self.mappings: List[Dict[str, object]] = []
        self.coordinate_reference_system: str = DEFAULT_CRS
        self.has_mapped: bool = False
        self.data_id: str | None = None

    @abstractmethod
    def from_place(self, place_name: str, **kwargs) -> None:
        """Load an urban layer from a place name

        Creates and populates the layer attribute with spatial data from the specified place.

        !!! warning "Method Not Implemented"
            This method must be implemented by subclasses. It should contain the logic
            for geocoding the place name and retrieving the corresponding spatial data.

        Args:
            place_name: Name of the place to load (e.g., "New York City", "Paris, France").
            **kwargs: Additional implementation-specific parameters.
                Common parameters include:

                - [x] network_type: Type of network to retrieve (for street networks)
                - [x] tags: OSM tags to filter features (for OSM-based layers, like cities features)

        Raises:
            ValueError: If the place cannot be geocoded or data cannot be retrieved.
        """
        pass

    @abstractmethod
    def from_file(self, file_path: str | Path, **kwargs) -> None:
        """Load an urban layer from a local file.

        Creates and populates the layer attribute with spatial data from the specified file.

        !!! note "What is the difference between `from_file` and `from_place`?"
            The `from_file` method is used to load spatial data from a local file (e.g., shapefile, GeoJSON), if
            the urban layer is not available through `from_place`, it should be having a fallback to `from_file`.

        !!! warning "Method Not Implemented"
            This method must be implemented by subclasses. It should contain the logic
            for reading the file and loading the corresponding spatial data.

        Args:
            file_path: Path to the file containing the spatial data.
            **kwargs: Additional implementation-specific parameters.

        Raises:
            ValueError: If the file cannot be read or does not contain valid spatial data.
            FileNotFoundError: If the specified file does not exist.
        """
        pass

    @abstractmethod
    def _map_nearest_layer(
        self,
        data: gpd.GeoDataFrame,
        longitude_column: str,
        latitude_column: str,
        output_column: str = "nearest_element",
        _reset_layer_index: bool = True,
        **kwargs,
    ) -> Tuple[gpd.GeoDataFrame, gpd.GeoDataFrame]:
        """Map points to their nearest elements in this urban layer.

        Performs spatial joining of points to their nearest elements using either
        provided column names or predefined mappings.

        !!! "How does the mapping / spatial join works?"
            The map nearest layer is based on the type of urban layer,
            e.g `streets roads` will be mapping to the `nearest street`, while `streets sidewalks` will be mapping to
            the `nearest sidewalk`. Or in a nutshell, for any new urban layer, the mapping will be based on
            the type of urban layer.

        !!! warning "Method Not Implemented"
            This method must be implemented by subclasses. It should contain the logic
            for performing the spatial join and mapping the points to their nearest elements.

        Args:
            data (gpd.GeoDataFrame): `GeoDataFrame` with points to map.
            longitude_column (str | None, optional): Column name for longitude values.
            latitude_column (str | None, optional): Column name for latitude values.
            output_column (str | None, optional): Column name for mapping results.
            threshold_distance (float | None, optional): Maximum distance for mapping. I.e. if a record is N distance away from urban layer's component, that fits within the threshold distance, it will be mapped to the nearest component, otherwise will be skipped.
            **kwargs: Additional parameters for the implementation.

        Returns:
            Tuple[gpd.GeoDataFrame, gpd.GeoDataFrame]: Updated urban layer and mapped data.

        Raises:
            ValueError: If the layer is not loaded, columns are missing, or mappings are invalid.

        Examples:
            >>> mapper = UrbanMapper()
            >>> streets = mapper.urban_layer.osmnx_streets().from_place("Edinburgh, UK")
            >>> _, mapped = streets.map_nearest_layer(
            ...     data=taxi_trips,
            ...     longitude_column="pickup_lng",
            ...     latitude_column="pickup_lat",
            ...     output_column="nearest_street"
            ... )
        """
        pass

    @abstractmethod
    def get_layer(self) -> gpd.GeoDataFrame:
        """Get the `GeoDataFrame` representing this urban layer.

        !!! warning "Method Not Implemented"
            This method must be implemented by subclasses. It should contain the logic
            for returning the urban layer data.

        Returns:
            The `GeoDataFrame` containing the urban layer data.

        Raises:
            ValueError: If the layer has not been loaded yet.
        """
        pass

    @abstractmethod
    def get_layer_bounding_box(self) -> Tuple[float, float, float, float]:
        """Get the bounding box coordinates of this urban layer.

        !!! warning "Method Not Implemented"
            This method must be implemented by subclasses. It should contain the logic
            for calculating the bounding box.

        Returns:
            A tuple containing (`min_x`, `min_y`, `max_x`, `max_y`) coordinates of the bounding box.

        Raises:
            ValueError: If the layer has not been loaded yet.
        """
        pass

    @abstractmethod
    def static_render(self, **plot_kwargs) -> None:
        """Create a static visualisation of this urban layer.

        !!! warning "Method Not Implemented"
            This method must be implemented by subclasses. It should contain the logic
            for rendering the urban layer.

        Args:
            **plot_kwargs: Keyword arguments passed to the underlying plotting function.
                Common parameters include (obviously, this will depend on the implementation):

                - [x] figsize: Tuple specifying figure dimensions
                - [x] column: Column to use for coloring elements
                - [x] cmap: Colormap to use for visualization
                - [x] alpha: Transparency level
                - [x] title: Title for the visualization

        Raises:
            ValueError: If the layer has not been loaded yet.
        """
        pass

    @require_attributes_not_none(
        "layer",
        error_msg="Urban layer not built. Please call from_place() or from_file() first.",
    )
    def map_nearest_layer(
        self,
        data: Union[
            Dict[str, gpd.GeoDataFrame],
            gpd.GeoDataFrame,
        ],
        longitude_column: str | None = None,
        latitude_column: str | None = None,
        output_column: str | None = None,
        threshold_distance: float | None = None,
        **kwargs,
    ) -> Tuple[
        gpd.GeoDataFrame,
        Union[
            Dict[str, gpd.GeoDataFrame],
            gpd.GeoDataFrame,
        ],
    ]:
        """Map points to their nearest elements in this urban layer.

        This method is the public method calling internally the `_map_nearest_layer` method.
        This method performs **spatial joining** of points to their nearest elements in the urban layer.
        It either uses provided column names or processes all mappings defined for this layer.
        This means if `with_mapping(.)` from the `Urban Layer factory` is multiple time called, it'll process the
        spatial join (`_map_narest_layer(.)`) as many times as the mappings has objects.

        Args:
            data: one or more `GeoDataFrame` containing the points to map.
            longitude_column: Name of the column containing longitude values.
                If provided, overrides any predefined mappings.
            latitude_column: Name of the column containing latitude values.
                If provided, overrides any predefined mappings.
            output_column: Name of the column to store the mapping results.
                If provided, overrides any predefined mappings.
            threshold_distance: Maximum distance (in CRS units) to consider for nearest element.
                Points beyond this distance will not be mapped.
            **kwargs: Additional implementation-specific parameters passed to _map_nearest_layer.

        Returns:
            A tuple containing:
            - The updated urban layer `GeoDataFrame` (may be unchanged in some implementations)
            - The input data `GeoDataFrame`(s) with the output column(s) added containing mapping results

        Raises:
            ValueError: If the layer has not been loaded, if required columns are missing,
                if the layer has already been mapped, or if no mappings are defined.

        Examples:
            >>> streets = OSMNXStreets()
            >>> streets.from_place("Manhattan, New York")
            >>> # Using direct column mapping
            >>> _, mapped_data = streets.map_nearest_layer(
            ...     data=taxi_trips,
            ...     longitude_column="pickup_longitude",
            ...     latitude_column="pickup_latitude",
            ...     output_column="nearest_street"
            ... )
            >>> # πŸ‘†This essentially adds a new column `nearest_street` to the datataset `taxi_trips` with the nearest street for each of the data-points it is filled-with.
            >>> # Using predefined mappings
            >>> streets = OSMNXStreets()
            >>> streets.from_place("Manhattan, New York")
            >>> streets.mappings = [
            ...     {"longitude_column": "pickup_longitude",
            ...      "latitude_column": "pickup_latitude",
            ...      "output_column": "nearest_pickup_street"}
            ... ] # Much better with the `.with_mapping()` method from the `Urban Layer factory`.
            >>> _, mapped_data = streets.map_nearest_layer(data=taxi_trips)
        """
        if self.has_mapped:
            raise ValueError(
                "This layer has already been mapped. If you want to map again, create a new instance."
            )
        if longitude_column or latitude_column or output_column:
            if not (longitude_column and latitude_column and output_column):
                raise ValueError(
                    "When overriding mappings, longitude_column, latitude_column, and output_column "
                    "must all be specified."
                )
            mapping_kwargs = (
                {"threshold_distance": threshold_distance} if threshold_distance else {}
            )
            mapping_kwargs.update(kwargs)

            if isinstance(data, gpd.GeoDataFrame):
                result = self._map_nearest_layer(
                    data,
                    longitude_column,
                    latitude_column,
                    output_column,
                    **mapping_kwargs,
                )
            else:
                result = {}
                last_key = list(data.keys())[-1]

                for key, gdf in data.items():
                    result[key] = gdf

                    if self.data_id is None or self.data_id == key:
                        self.layer, mapped_data = self._map_nearest_layer(
                            gdf,
                            longitude_column,
                            latitude_column,
                            output_column,
                            _reset_layer_index=key == last_key,
                            **mapping_kwargs,
                        )
                        result[key] = mapped_data

                result = (self.layer, result)

            self.has_mapped = True
            return result

        if not self.mappings:
            raise ValueError(
                "No mappings defined. Use with_mapping() during layer creation."
            )

        mapped_data = data.copy()
        for mapping in self.mappings:
            lon_col = mapping.get("longitude_column", None)
            lat_col = mapping.get("latitude_column", None)
            out_col = mapping.get("output_column", None)
            if not (lon_col and lat_col and out_col):
                raise ValueError(
                    "Each mapping must specify longitude_column, latitude_column, and output_column."
                )

            mapping_kwargs = mapping.get("kwargs", {}).copy()
            if threshold_distance is not None:
                mapping_kwargs["threshold_distance"] = threshold_distance
            mapping_kwargs.update(kwargs)

            if None in [lon_col, lat_col, out_col]:
                raise ValueError(
                    "All of longitude_column, latitude_column, and output_column must be specified."
                )
            if self.mappings[-1]:
                logger.log(
                    "DEBUG_MID",
                    "INFO: Last mapping, resetting urban layer's index.",
                )
            if isinstance(mapped_data, gpd.GeoDataFrame):
                self.layer, temp_mapped = self._map_nearest_layer(
                    mapped_data,
                    lon_col,
                    lat_col,
                    out_col,
                    _reset_layer_index=(mapping == self.mappings[-1]),
                    **mapping_kwargs,
                )
                mapped_data[out_col] = temp_mapped[out_col]
            else:
                temp_mapped_data = {}
                last_key = list(mapped_data.keys())[-1]

                for key, gdf in mapped_data.items():
                    temp_mapped_data[key] = gdf

                    if self.data_id is None or self.data_id == key:
                        self.layer, temp_mapped = self._map_nearest_layer(
                            gdf,
                            lon_col,
                            lat_col,
                            out_col,
                            _reset_layer_index=(
                                mapping == self.mappings[-1] and key == last_key
                            ),
                            **mapping_kwargs,
                        )
                        gdf[out_col] = temp_mapped[out_col]
                        temp_mapped_data[key] = gdf

                mapped_data = temp_mapped_data

        self.has_mapped = True
        return self.layer, mapped_data

    @abstractmethod
    def preview(self, format: str = "ascii") -> Any:
        """Generate a preview of this urban layer.

        Creates a summary or visual representation of the urban layer for quick inspection.

        !!! warning "Method Not Implemented"
            This method must be implemented by subclasses. It should contain the logic
            for generating the preview based on the requested format.

            Supported Formats include:

            - [x] ascii: Text-based table format
            - [x] json: JSON-formatted data
            - [ ] html: one may see the HTML representation if necessary in the long term. Open an Issue!
            - [ ] dict: Python dictionary representation if necessary in the long term for re-use in the Python workflow. Open an Issue!
            - [ ] other: Any other formats of interest? Open an Issue!

        Args:
            format: The output format for the preview. Options include:

                - [x] "ascii": Text-based table format
                - [x] "json": JSON-formatted data

        Returns:
            A representation of the urban layer in the requested format.
            Return type varies based on the format parameter.

        Raises:
            ValueError: If the layer has not been loaded yet.
            ValueError: If an unsupported format is requested.
        """
        pass

from_place(place_name, **kwargs) abstractmethod

Load an urban layer from a place name

Creates and populates the layer attribute with spatial data from the specified place.

Method Not Implemented

This method must be implemented by subclasses. It should contain the logic for geocoding the place name and retrieving the corresponding spatial data.

Parameters:

Name Type Description Default
place_name str

Name of the place to load (e.g., "New York City", "Paris, France").

required
**kwargs

Additional implementation-specific parameters. Common parameters include:

  • network_type: Type of network to retrieve (for street networks)
  • tags: OSM tags to filter features (for OSM-based layers, like cities features)
{}

Raises:

Type Description
ValueError

If the place cannot be geocoded or data cannot be retrieved.

Source code in src/urban_mapper/modules/urban_layer/abc_urban_layer.py
@abstractmethod
def from_place(self, place_name: str, **kwargs) -> None:
    """Load an urban layer from a place name

    Creates and populates the layer attribute with spatial data from the specified place.

    !!! warning "Method Not Implemented"
        This method must be implemented by subclasses. It should contain the logic
        for geocoding the place name and retrieving the corresponding spatial data.

    Args:
        place_name: Name of the place to load (e.g., "New York City", "Paris, France").
        **kwargs: Additional implementation-specific parameters.
            Common parameters include:

            - [x] network_type: Type of network to retrieve (for street networks)
            - [x] tags: OSM tags to filter features (for OSM-based layers, like cities features)

    Raises:
        ValueError: If the place cannot be geocoded or data cannot be retrieved.
    """
    pass

from_file(file_path, **kwargs) abstractmethod

Load an urban layer from a local file.

Creates and populates the layer attribute with spatial data from the specified file.

What is the difference between from_file and from_place?

The from_file method is used to load spatial data from a local file (e.g., shapefile, GeoJSON), if the urban layer is not available through from_place, it should be having a fallback to from_file.

Method Not Implemented

This method must be implemented by subclasses. It should contain the logic for reading the file and loading the corresponding spatial data.

Parameters:

Name Type Description Default
file_path str | Path

Path to the file containing the spatial data.

required
**kwargs

Additional implementation-specific parameters.

{}

Raises:

Type Description
ValueError

If the file cannot be read or does not contain valid spatial data.

FileNotFoundError

If the specified file does not exist.

Source code in src/urban_mapper/modules/urban_layer/abc_urban_layer.py
@abstractmethod
def from_file(self, file_path: str | Path, **kwargs) -> None:
    """Load an urban layer from a local file.

    Creates and populates the layer attribute with spatial data from the specified file.

    !!! note "What is the difference between `from_file` and `from_place`?"
        The `from_file` method is used to load spatial data from a local file (e.g., shapefile, GeoJSON), if
        the urban layer is not available through `from_place`, it should be having a fallback to `from_file`.

    !!! warning "Method Not Implemented"
        This method must be implemented by subclasses. It should contain the logic
        for reading the file and loading the corresponding spatial data.

    Args:
        file_path: Path to the file containing the spatial data.
        **kwargs: Additional implementation-specific parameters.

    Raises:
        ValueError: If the file cannot be read or does not contain valid spatial data.
        FileNotFoundError: If the specified file does not exist.
    """
    pass

_map_nearest_layer(data, longitude_column, latitude_column, output_column='nearest_element', _reset_layer_index=True, **kwargs) abstractmethod

Map points to their nearest elements in this urban layer.

Performs spatial joining of points to their nearest elements using either provided column names or predefined mappings.

!!! "How does the mapping / spatial join works?" The map nearest layer is based on the type of urban layer, e.g streets roads will be mapping to the nearest street, while streets sidewalks will be mapping to the nearest sidewalk. Or in a nutshell, for any new urban layer, the mapping will be based on the type of urban layer.

Method Not Implemented

This method must be implemented by subclasses. It should contain the logic for performing the spatial join and mapping the points to their nearest elements.

Parameters:

Name Type Description Default
data GeoDataFrame

GeoDataFrame with points to map.

required
longitude_column str | None

Column name for longitude values.

required
latitude_column str | None

Column name for latitude values.

required
output_column str | None

Column name for mapping results.

'nearest_element'
threshold_distance float | None

Maximum distance for mapping. I.e. if a record is N distance away from urban layer's component, that fits within the threshold distance, it will be mapped to the nearest component, otherwise will be skipped.

required
**kwargs

Additional parameters for the implementation.

{}

Returns:

Type Description
Tuple[GeoDataFrame, GeoDataFrame]

Tuple[gpd.GeoDataFrame, gpd.GeoDataFrame]: Updated urban layer and mapped data.

Raises:

Type Description
ValueError

If the layer is not loaded, columns are missing, or mappings are invalid.

Examples:

>>> mapper = UrbanMapper()
>>> streets = mapper.urban_layer.osmnx_streets().from_place("Edinburgh, UK")
>>> _, mapped = streets.map_nearest_layer(
...     data=taxi_trips,
...     longitude_column="pickup_lng",
...     latitude_column="pickup_lat",
...     output_column="nearest_street"
... )
Source code in src/urban_mapper/modules/urban_layer/abc_urban_layer.py
@abstractmethod
def _map_nearest_layer(
    self,
    data: gpd.GeoDataFrame,
    longitude_column: str,
    latitude_column: str,
    output_column: str = "nearest_element",
    _reset_layer_index: bool = True,
    **kwargs,
) -> Tuple[gpd.GeoDataFrame, gpd.GeoDataFrame]:
    """Map points to their nearest elements in this urban layer.

    Performs spatial joining of points to their nearest elements using either
    provided column names or predefined mappings.

    !!! "How does the mapping / spatial join works?"
        The map nearest layer is based on the type of urban layer,
        e.g `streets roads` will be mapping to the `nearest street`, while `streets sidewalks` will be mapping to
        the `nearest sidewalk`. Or in a nutshell, for any new urban layer, the mapping will be based on
        the type of urban layer.

    !!! warning "Method Not Implemented"
        This method must be implemented by subclasses. It should contain the logic
        for performing the spatial join and mapping the points to their nearest elements.

    Args:
        data (gpd.GeoDataFrame): `GeoDataFrame` with points to map.
        longitude_column (str | None, optional): Column name for longitude values.
        latitude_column (str | None, optional): Column name for latitude values.
        output_column (str | None, optional): Column name for mapping results.
        threshold_distance (float | None, optional): Maximum distance for mapping. I.e. if a record is N distance away from urban layer's component, that fits within the threshold distance, it will be mapped to the nearest component, otherwise will be skipped.
        **kwargs: Additional parameters for the implementation.

    Returns:
        Tuple[gpd.GeoDataFrame, gpd.GeoDataFrame]: Updated urban layer and mapped data.

    Raises:
        ValueError: If the layer is not loaded, columns are missing, or mappings are invalid.

    Examples:
        >>> mapper = UrbanMapper()
        >>> streets = mapper.urban_layer.osmnx_streets().from_place("Edinburgh, UK")
        >>> _, mapped = streets.map_nearest_layer(
        ...     data=taxi_trips,
        ...     longitude_column="pickup_lng",
        ...     latitude_column="pickup_lat",
        ...     output_column="nearest_street"
        ... )
    """
    pass

map_nearest_layer(data, longitude_column=None, latitude_column=None, output_column=None, threshold_distance=None, **kwargs)

Map points to their nearest elements in this urban layer.

This method is the public method calling internally the _map_nearest_layer method. This method performs spatial joining of points to their nearest elements in the urban layer. It either uses provided column names or processes all mappings defined for this layer. This means if with_mapping(.) from the Urban Layer factory is multiple time called, it'll process the spatial join (_map_narest_layer(.)) as many times as the mappings has objects.

Parameters:

Name Type Description Default
data Union[Dict[str, GeoDataFrame], GeoDataFrame]

one or more GeoDataFrame containing the points to map.

required
longitude_column str | None

Name of the column containing longitude values. If provided, overrides any predefined mappings.

None
latitude_column str | None

Name of the column containing latitude values. If provided, overrides any predefined mappings.

None
output_column str | None

Name of the column to store the mapping results. If provided, overrides any predefined mappings.

None
threshold_distance float | None

Maximum distance (in CRS units) to consider for nearest element. Points beyond this distance will not be mapped.

None
**kwargs

Additional implementation-specific parameters passed to _map_nearest_layer.

{}

Returns:

Type Description
GeoDataFrame

A tuple containing:

Union[Dict[str, GeoDataFrame], GeoDataFrame]
  • The updated urban layer GeoDataFrame (may be unchanged in some implementations)
Tuple[GeoDataFrame, Union[Dict[str, GeoDataFrame], GeoDataFrame]]
  • The input data GeoDataFrame(s) with the output column(s) added containing mapping results

Raises:

Type Description
ValueError

If the layer has not been loaded, if required columns are missing, if the layer has already been mapped, or if no mappings are defined.

Examples:

>>> streets = OSMNXStreets()
>>> streets.from_place("Manhattan, New York")
>>> # Using direct column mapping
>>> _, mapped_data = streets.map_nearest_layer(
...     data=taxi_trips,
...     longitude_column="pickup_longitude",
...     latitude_column="pickup_latitude",
...     output_column="nearest_street"
... )
>>> # πŸ‘†This essentially adds a new column `nearest_street` to the datataset `taxi_trips` with the nearest street for each of the data-points it is filled-with.
>>> # Using predefined mappings
>>> streets = OSMNXStreets()
>>> streets.from_place("Manhattan, New York")
>>> streets.mappings = [
...     {"longitude_column": "pickup_longitude",
...      "latitude_column": "pickup_latitude",
...      "output_column": "nearest_pickup_street"}
... ] # Much better with the `.with_mapping()` method from the `Urban Layer factory`.
>>> _, mapped_data = streets.map_nearest_layer(data=taxi_trips)
Source code in src/urban_mapper/modules/urban_layer/abc_urban_layer.py
@require_attributes_not_none(
    "layer",
    error_msg="Urban layer not built. Please call from_place() or from_file() first.",
)
def map_nearest_layer(
    self,
    data: Union[
        Dict[str, gpd.GeoDataFrame],
        gpd.GeoDataFrame,
    ],
    longitude_column: str | None = None,
    latitude_column: str | None = None,
    output_column: str | None = None,
    threshold_distance: float | None = None,
    **kwargs,
) -> Tuple[
    gpd.GeoDataFrame,
    Union[
        Dict[str, gpd.GeoDataFrame],
        gpd.GeoDataFrame,
    ],
]:
    """Map points to their nearest elements in this urban layer.

    This method is the public method calling internally the `_map_nearest_layer` method.
    This method performs **spatial joining** of points to their nearest elements in the urban layer.
    It either uses provided column names or processes all mappings defined for this layer.
    This means if `with_mapping(.)` from the `Urban Layer factory` is multiple time called, it'll process the
    spatial join (`_map_narest_layer(.)`) as many times as the mappings has objects.

    Args:
        data: one or more `GeoDataFrame` containing the points to map.
        longitude_column: Name of the column containing longitude values.
            If provided, overrides any predefined mappings.
        latitude_column: Name of the column containing latitude values.
            If provided, overrides any predefined mappings.
        output_column: Name of the column to store the mapping results.
            If provided, overrides any predefined mappings.
        threshold_distance: Maximum distance (in CRS units) to consider for nearest element.
            Points beyond this distance will not be mapped.
        **kwargs: Additional implementation-specific parameters passed to _map_nearest_layer.

    Returns:
        A tuple containing:
        - The updated urban layer `GeoDataFrame` (may be unchanged in some implementations)
        - The input data `GeoDataFrame`(s) with the output column(s) added containing mapping results

    Raises:
        ValueError: If the layer has not been loaded, if required columns are missing,
            if the layer has already been mapped, or if no mappings are defined.

    Examples:
        >>> streets = OSMNXStreets()
        >>> streets.from_place("Manhattan, New York")
        >>> # Using direct column mapping
        >>> _, mapped_data = streets.map_nearest_layer(
        ...     data=taxi_trips,
        ...     longitude_column="pickup_longitude",
        ...     latitude_column="pickup_latitude",
        ...     output_column="nearest_street"
        ... )
        >>> # πŸ‘†This essentially adds a new column `nearest_street` to the datataset `taxi_trips` with the nearest street for each of the data-points it is filled-with.
        >>> # Using predefined mappings
        >>> streets = OSMNXStreets()
        >>> streets.from_place("Manhattan, New York")
        >>> streets.mappings = [
        ...     {"longitude_column": "pickup_longitude",
        ...      "latitude_column": "pickup_latitude",
        ...      "output_column": "nearest_pickup_street"}
        ... ] # Much better with the `.with_mapping()` method from the `Urban Layer factory`.
        >>> _, mapped_data = streets.map_nearest_layer(data=taxi_trips)
    """
    if self.has_mapped:
        raise ValueError(
            "This layer has already been mapped. If you want to map again, create a new instance."
        )
    if longitude_column or latitude_column or output_column:
        if not (longitude_column and latitude_column and output_column):
            raise ValueError(
                "When overriding mappings, longitude_column, latitude_column, and output_column "
                "must all be specified."
            )
        mapping_kwargs = (
            {"threshold_distance": threshold_distance} if threshold_distance else {}
        )
        mapping_kwargs.update(kwargs)

        if isinstance(data, gpd.GeoDataFrame):
            result = self._map_nearest_layer(
                data,
                longitude_column,
                latitude_column,
                output_column,
                **mapping_kwargs,
            )
        else:
            result = {}
            last_key = list(data.keys())[-1]

            for key, gdf in data.items():
                result[key] = gdf

                if self.data_id is None or self.data_id == key:
                    self.layer, mapped_data = self._map_nearest_layer(
                        gdf,
                        longitude_column,
                        latitude_column,
                        output_column,
                        _reset_layer_index=key == last_key,
                        **mapping_kwargs,
                    )
                    result[key] = mapped_data

            result = (self.layer, result)

        self.has_mapped = True
        return result

    if not self.mappings:
        raise ValueError(
            "No mappings defined. Use with_mapping() during layer creation."
        )

    mapped_data = data.copy()
    for mapping in self.mappings:
        lon_col = mapping.get("longitude_column", None)
        lat_col = mapping.get("latitude_column", None)
        out_col = mapping.get("output_column", None)
        if not (lon_col and lat_col and out_col):
            raise ValueError(
                "Each mapping must specify longitude_column, latitude_column, and output_column."
            )

        mapping_kwargs = mapping.get("kwargs", {}).copy()
        if threshold_distance is not None:
            mapping_kwargs["threshold_distance"] = threshold_distance
        mapping_kwargs.update(kwargs)

        if None in [lon_col, lat_col, out_col]:
            raise ValueError(
                "All of longitude_column, latitude_column, and output_column must be specified."
            )
        if self.mappings[-1]:
            logger.log(
                "DEBUG_MID",
                "INFO: Last mapping, resetting urban layer's index.",
            )
        if isinstance(mapped_data, gpd.GeoDataFrame):
            self.layer, temp_mapped = self._map_nearest_layer(
                mapped_data,
                lon_col,
                lat_col,
                out_col,
                _reset_layer_index=(mapping == self.mappings[-1]),
                **mapping_kwargs,
            )
            mapped_data[out_col] = temp_mapped[out_col]
        else:
            temp_mapped_data = {}
            last_key = list(mapped_data.keys())[-1]

            for key, gdf in mapped_data.items():
                temp_mapped_data[key] = gdf

                if self.data_id is None or self.data_id == key:
                    self.layer, temp_mapped = self._map_nearest_layer(
                        gdf,
                        lon_col,
                        lat_col,
                        out_col,
                        _reset_layer_index=(
                            mapping == self.mappings[-1] and key == last_key
                        ),
                        **mapping_kwargs,
                    )
                    gdf[out_col] = temp_mapped[out_col]
                    temp_mapped_data[key] = gdf

            mapped_data = temp_mapped_data

    self.has_mapped = True
    return self.layer, mapped_data

get_layer() abstractmethod

Get the GeoDataFrame representing this urban layer.

Method Not Implemented

This method must be implemented by subclasses. It should contain the logic for returning the urban layer data.

Returns:

Type Description
GeoDataFrame

The GeoDataFrame containing the urban layer data.

Raises:

Type Description
ValueError

If the layer has not been loaded yet.

Source code in src/urban_mapper/modules/urban_layer/abc_urban_layer.py
@abstractmethod
def get_layer(self) -> gpd.GeoDataFrame:
    """Get the `GeoDataFrame` representing this urban layer.

    !!! warning "Method Not Implemented"
        This method must be implemented by subclasses. It should contain the logic
        for returning the urban layer data.

    Returns:
        The `GeoDataFrame` containing the urban layer data.

    Raises:
        ValueError: If the layer has not been loaded yet.
    """
    pass

get_layer_bounding_box() abstractmethod

Get the bounding box coordinates of this urban layer.

Method Not Implemented

This method must be implemented by subclasses. It should contain the logic for calculating the bounding box.

Returns:

Type Description
Tuple[float, float, float, float]

A tuple containing (min_x, min_y, max_x, max_y) coordinates of the bounding box.

Raises:

Type Description
ValueError

If the layer has not been loaded yet.

Source code in src/urban_mapper/modules/urban_layer/abc_urban_layer.py
@abstractmethod
def get_layer_bounding_box(self) -> Tuple[float, float, float, float]:
    """Get the bounding box coordinates of this urban layer.

    !!! warning "Method Not Implemented"
        This method must be implemented by subclasses. It should contain the logic
        for calculating the bounding box.

    Returns:
        A tuple containing (`min_x`, `min_y`, `max_x`, `max_y`) coordinates of the bounding box.

    Raises:
        ValueError: If the layer has not been loaded yet.
    """
    pass

static_render(**plot_kwargs) abstractmethod

Create a static visualisation of this urban layer.

Method Not Implemented

This method must be implemented by subclasses. It should contain the logic for rendering the urban layer.

Parameters:

Name Type Description Default
**plot_kwargs

Keyword arguments passed to the underlying plotting function. Common parameters include (obviously, this will depend on the implementation):

  • figsize: Tuple specifying figure dimensions
  • column: Column to use for coloring elements
  • cmap: Colormap to use for visualization
  • alpha: Transparency level
  • title: Title for the visualization
{}

Raises:

Type Description
ValueError

If the layer has not been loaded yet.

Source code in src/urban_mapper/modules/urban_layer/abc_urban_layer.py
@abstractmethod
def static_render(self, **plot_kwargs) -> None:
    """Create a static visualisation of this urban layer.

    !!! warning "Method Not Implemented"
        This method must be implemented by subclasses. It should contain the logic
        for rendering the urban layer.

    Args:
        **plot_kwargs: Keyword arguments passed to the underlying plotting function.
            Common parameters include (obviously, this will depend on the implementation):

            - [x] figsize: Tuple specifying figure dimensions
            - [x] column: Column to use for coloring elements
            - [x] cmap: Colormap to use for visualization
            - [x] alpha: Transparency level
            - [x] title: Title for the visualization

    Raises:
        ValueError: If the layer has not been loaded yet.
    """
    pass

preview(format='ascii') abstractmethod

Generate a preview of this urban layer.

Creates a summary or visual representation of the urban layer for quick inspection.

Method Not Implemented

This method must be implemented by subclasses. It should contain the logic for generating the preview based on the requested format.

Supported Formats include:

  • ascii: Text-based table format
  • json: JSON-formatted data
  • html: one may see the HTML representation if necessary in the long term. Open an Issue!
  • dict: Python dictionary representation if necessary in the long term for re-use in the Python workflow. Open an Issue!
  • other: Any other formats of interest? Open an Issue!

Parameters:

Name Type Description Default
format str

The output format for the preview. Options include:

  • "ascii": Text-based table format
  • "json": JSON-formatted data
'ascii'

Returns:

Type Description
Any

A representation of the urban layer in the requested format.

Any

Return type varies based on the format parameter.

Raises:

Type Description
ValueError

If the layer has not been loaded yet.

ValueError

If an unsupported format is requested.

Source code in src/urban_mapper/modules/urban_layer/abc_urban_layer.py
@abstractmethod
def preview(self, format: str = "ascii") -> Any:
    """Generate a preview of this urban layer.

    Creates a summary or visual representation of the urban layer for quick inspection.

    !!! warning "Method Not Implemented"
        This method must be implemented by subclasses. It should contain the logic
        for generating the preview based on the requested format.

        Supported Formats include:

        - [x] ascii: Text-based table format
        - [x] json: JSON-formatted data
        - [ ] html: one may see the HTML representation if necessary in the long term. Open an Issue!
        - [ ] dict: Python dictionary representation if necessary in the long term for re-use in the Python workflow. Open an Issue!
        - [ ] other: Any other formats of interest? Open an Issue!

    Args:
        format: The output format for the preview. Options include:

            - [x] "ascii": Text-based table format
            - [x] "json": JSON-formatted data

    Returns:
        A representation of the urban layer in the requested format.
        Return type varies based on the format parameter.

    Raises:
        ValueError: If the layer has not been loaded yet.
        ValueError: If an unsupported format is requested.
    """
    pass

OSMNXStreets

Bases: UrbanLayerBase

Urban layer implementation for OpenStreetMap street networks.

This class offers a straightforward interface for loading and manipulating street networks from OpenStreetMap using OSMnx. It adheres to the UrbanLayerBase interface, ensuring compatibility with UrbanMapper components such as filters, enrichers, and pipelines.

When to Use?

Employ this class when you need to integrate street network data into your urban analysis workflows. It’s particularly handy for:

  • Accidents on roads
  • Traffic analysis
  • Urban planning
  • Road-Based Infrastructure development

Attributes:

Name Type Description
network StreetNetwork | None

The underlying StreetNetwork object managing OSMnx operations.

layer GeoDataFrame | None

The GeoDataFrame holding the street network edges (set after loading).

Examples:

>>> from urban_mapper import UrbanMapper
>>> mapper = UrbanMapper()
>>> # Load streets for a specific place
>>> streets = mapper.urban_layer.streets_roads().from_place("Edinburgh, Scotland")
>>> # Load streets within a bounding box
>>> bbox_streets = mapper.urban_layer.streets_roads().from_bbox(
...     (-3.21, 55.94, -3.17, 55.96)  # left, bottom, right, top
... )
Source code in src/urban_mapper/modules/urban_layer/urban_layers/osmnx_streets.py
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
@beartype
class OSMNXStreets(UrbanLayerBase):
    """Urban layer implementation for `OpenStreetMap` `street networks`.

    This class offers a straightforward interface for loading and manipulating
    `street networks` from `OpenStreetMap` using `OSMnx`. It adheres to the `UrbanLayerBase
    interface`, ensuring compatibility with UrbanMapper components such as `filters`,
    `enrichers`, and `pipelines`.

    !!! tip "When to Use?"
        Employ this class when you need to integrate street network data into your
        urban analysis workflows. It’s particularly handy for:

        - [x] Accidents on roads
        - [x] Traffic analysis
        - [x] Urban planning
        - [x] Road-Based Infrastructure development

    Attributes:
        network: The underlying `StreetNetwork` object managing `OSMnx` operations.
        layer: The `GeoDataFrame` holding the `street network` edges (set after loading).

    Examples:
        >>> from urban_mapper import UrbanMapper
        >>> mapper = UrbanMapper()
        >>> # Load streets for a specific place
        >>> streets = mapper.urban_layer.streets_roads().from_place("Edinburgh, Scotland")
        >>> # Load streets within a bounding box
        >>> bbox_streets = mapper.urban_layer.streets_roads().from_bbox(
        ...     (-3.21, 55.94, -3.17, 55.96)  # left, bottom, right, top
        ... )
    """

    def __init__(self) -> None:
        super().__init__()
        self.network: StreetNetwork | None = None

    def from_place(self, place_name: str, undirected: bool = True, **kwargs) -> None:
        """Load a street network for a named place.

        Retrieves the street network for a specified place name from `OpenStreetMap`,
        supporting `cities`, `neighbourhoods`, and other recognised geographic entities.

        Args:
            place_name: Name of the place to load (e.g., "Bristol, England").
            undirected: Whether to convert the network to an undirected graph (default: True).
            **kwargs: Additional parameters passed to OSMnx's graph_from_place.

        Returns:
            Self, enabling method chaining.

        Examples:
            >>> streets = mapper.urban_layer.streets_roads().from_place("Glasgow, Scotland")
            >>> # Load only drivable streets
            >>> streets = mapper.urban_layer.streets_roads().from_place(
            ...     "Cardiff, Wales", network_type="drive"
            ... )
        """
        self.network = StreetNetwork()
        self.network.load("place", query=place_name, undirected=undirected, **kwargs)
        self.network._graph = ox.convert.to_undirected(self.network.graph)
        gdf_nodes, gdf_edges = ox.graph_to_gdfs(self.network.graph)
        self.layer = gdf_edges.to_crs(self.coordinate_reference_system)

    def from_address(self, address: str, undirected: bool = True, **kwargs) -> None:
        """Load a street network around a specified address.

        Fetches the street network within a given distance of an address, requiring
        the distance to be specified in the keyword arguments.

        Args:
            address: The address to centre the network around (e.g., "10 Downing Street, London").
            undirected: Whether to convert the network to an undirected graph (default: True).
        Boots argues that the method retrieves the street network within a certain distance of a specified address.
            **kwargs: Additional parameters passed to OSMnx's graph_from_address.
                Must include 'dist' specifying the distance in metres.

        Returns:
            Self, enabling method chaining.

        Examples:
            >>> streets = mapper.urban_layer.streets_roads().from_address(
            ...     "Buckingham Palace, London", dist=1000
            ... )
        """
        self.network = StreetNetwork()
        self.network.load("address", address=address, undirected=undirected, **kwargs)
        gdf_nodes, gdf_edges = ox.graph_to_gdfs(self.network.graph)
        self.layer = gdf_edges.to_crs(self.coordinate_reference_system)

    def from_bbox(
        self, bbox: Tuple[float, float, float, float], undirected: bool = True, **kwargs
    ) -> None:
        """Load a street network within a bounding box.

        Retrieves the street network contained within the specified bounding box coordinates.

        Args:
            bbox: Tuple of (`left`, `bottom`, `right`, `top`) coordinates defining the bounding box.
            undirected: Whether to convert the network to an undirected graph (default: True).
            **kwargs: Additional parameters passed to OSMnx's graph_from_bbox.

        Returns:
            Self, enabling method chaining.

        Examples:
            >>> streets = mapper.urban_layer.streets_roads().from_bbox(
            ...     (-0.13, 51.50, -0.09, 51.52)  # Central London
            ... )
        """
        self.network = StreetNetwork()
        self.network.load("bbox", bbox=bbox, undirected=undirected, **kwargs)
        gdf_nodes, gdf_edges = ox.graph_to_gdfs(self.network.graph)
        self.layer = gdf_edges.to_crs(self.coordinate_reference_system)

    def from_point(
        self, center_point: Tuple[float, float], undirected: bool = True, **kwargs
    ) -> None:
        """Load a street network around a specified point.

        Fetches the street network within a certain distance of a geographic point,
        with the distance specified in the keyword arguments.

        Args:
            center_point: Tuple of (`latitude`, `longitude`) specifying the centre point.
            undirected: Whether to convert the network to an undirected graph (default: True).
            **kwargs: Additional parameters passed to OSMnx's graph_from_point.
                Must include 'dist' specifying the distance in metres.

        Returns:
            Self, enabling method chaining.

        Examples:
            >>> streets = mapper.urban_layer.streets_roads().from_point(
            ...     (51.5074, -0.1278), dist=500  # Near Trafalgar Square
            ... )
        """
        self.network = StreetNetwork()
        self.network.load(
            "point", center_point=center_point, undirected=undirected, **kwargs
        )
        gdf_nodes, gdf_edges = ox.graph_to_gdfs(self.network.graph)
        self.layer = gdf_edges.to_crs(self.coordinate_reference_system)

    def from_polygon(
        self, polygon: Polygon | MultiPolygon, undirected: bool = True, **kwargs
    ) -> None:
        """Load a street network within a polygon boundary.

        Retrieves the street network contained within the specified polygon boundary.

        Args:
            polygon: Shapely Polygon or MultiPolygon defining the boundary.
            undirected: Whether to convert the network to an undirected graph (default: True).
            **kwargs: Additional parameters passed to OSMnx's graph_from_polygon.

        Returns:
            Self, enabling method chaining.

        Examples:
            >>> from shapely.geometry import Polygon
            >>> boundary = Polygon([(-0.13, 51.50), (-0.09, 51.50), (-0.09, 51.52), (-0.13, 51.52)])
            >>> streets = mapper.urban_layer.streets_roads().from_polygon(boundary)
        """
        self.network = StreetNetwork()
        self.network.load("polygon", polygon=polygon, undirected=undirected, **kwargs)
        gdf_nodes, gdf_edges = ox.graph_to_gdfs(self.network.graph)
        self.layer = gdf_edges.to_crs(self.coordinate_reference_system)

    def from_xml(self, filepath: str | Path, undirected: bool = True, **kwargs) -> None:
        """Load a street network from an OSM XML file.

        Loads a street network from a local OpenStreetMap XML file.

        Args:
            filepath: Path to the OSM XML file.
            undirected: Whether to convert the network to an undirected graph (default: True).
            **kwargs: Additional parameters passed to OSMnx's graph_from_xml.

        Returns:
            Self, enabling method chaining.

        Examples:
            >>> streets = mapper.urban_layer.streets_roads().from_xml("london.osm")
        """
        self.network = StreetNetwork()
        self.network.load("xml", filepath=filepath, undirected=undirected, **kwargs)
        gdf_nodes, gdf_edges = ox.graph_to_gdfs(self.network.graph)
        self.layer = gdf_edges.to_crs(self.coordinate_reference_system)

    def from_file(self, file_path: str | Path, **kwargs) -> "OSMNXStreets":
        """Load a street network from a file.

        !!! warning "Not Implemented"
            This method is not supported for OSMNXStreets. Use from_xml() for OSM XML files instead.

        Args:
            file_path: Path to the file.
            **kwargs: Additional parameters (not used).

        Raises:
            NotImplementedError: Always raised, as this method is not supported.
        """
        raise NotImplementedError(
            "Loading from file is not supported for OSMNx street networks."
        )

    @require_attributes_not_none(
        "network",
        error_msg="Network not loaded. Call from_place() or other load methods first.",
    )
    def _map_nearest_layer(
        self,
        data: gpd.GeoDataFrame,
        longitude_column: str,
        latitude_column: str,
        output_column: str = "nearest_street",
        threshold_distance: float | None = None,
        _reset_layer_index: bool = True,
        **kwargs,
    ) -> Tuple[gpd.GeoDataFrame, gpd.GeoDataFrame]:
        """Map points to their nearest street edges.

        This internal method identifies the nearest street edge for each point in the
        input `GeoDataFrame`, adding a reference to that edge as a new column. It’s primarily
        used by `UrbanLayerBase.map_nearest_layer()` to perform spatial joins between point data and
        the street network.

        Args:
            data: `GeoDataFrame` containing point data to map.
            longitude_column: Name of the column with longitude values.
            latitude_column: Name of the column with latitude values.
            output_column: Name of the column to store nearest street indices (default: "nearest_street").
            threshold_distance: Maximum distance for a match, in CRS units (default: None).
            _reset_layer_index: Whether to reset the layer `GeoDataFrame`’s index (default: True).
            **kwargs: Additional parameters (not used).

        Returns:
            A tuple containing:
                - The street network `GeoDataFrame` (possibly with reset index)
                - The input `GeoDataFrame` with the new output_column (filtered if threshold_distance is set)
        """
        dataframe = data.copy()
        result = ox.distance.nearest_edges(
            self.network.graph,
            X=dataframe[longitude_column].values,
            Y=dataframe[latitude_column].values,
            return_dist=threshold_distance is not None,
        )
        if threshold_distance:
            nearest_edges, distances = result
            mask = np.array(distances) <= threshold_distance
            dataframe = dataframe[mask]
            nearest_edges = nearest_edges[mask]
        else:
            nearest_edges = result

        edge_to_idx = {k: i for i, k in enumerate(self.layer.index)}
        nearest_indices = [edge_to_idx[tuple(edge)] for edge in nearest_edges]

        dataframe[output_column] = nearest_indices
        if _reset_layer_index:
            self.layer = self.layer.reset_index()
        return self.layer, dataframe

    @require_attributes_not_none(
        "layer", error_msg="Layer not built. Call from_place() first."
    )
    def get_layer(self) -> gpd.GeoDataFrame:
        """Get the street network as a GeoDataFrame.

        Returns the street network edges as a GeoDataFrame for further analysis or visualisation.

        Returns:
            GeoDataFrame containing the street network edges.

        Raises:
            ValueError: If the layer has not been loaded yet.

        Examples:
            >>> streets = mapper.urban_layer.streets_roads().from_place("Birmingham, UK")
            >>> streets_gdf = streets.get_layer()
            >>> streets_gdf.plot()
        """
        return self.layer

    @require_attributes_not_none(
        "layer", error_msg="Layer not built. Call from_place() first."
    )
    def get_layer_bounding_box(self) -> Tuple[float, float, float, float]:
        """Get the bounding box of the street network.

        Returns the bounding box coordinates of the street network, useful for spatial
        queries or visualisation extents.

        Returns:
            Tuple of (`left`, `bottom`, `right`, `top`) coordinates defining the bounding box.

        Raises:
            ValueError: If the layer has not been loaded yet.

        Examples:
            >>> streets = mapper.urban_layer.streets_roads().from_place("Leeds, UK")
            >>> bbox = streets.get_layer_bounding_box()
            >>> print(f"Extent: {bbox}")
        """
        return tuple(self.layer.total_bounds)  # type: ignore

    @require_attributes_not_none(
        "network", error_msg="No network loaded yet. Try from_place() first!"
    )
    def static_render(self, **plot_kwargs) -> None:
        """Render the street network as a static plot.

        Creates a static visualisation of the street network using `OSMnx`’s plotting
        capabilities, displayed immediately.

        Args:
            **plot_kwargs: Additional keyword arguments passed to `OSMnx`’s plot_graph function,
                such as `node_size`, `edge_linewidth`, `node_color`, `edge_color`.

                See further in `OSMnx` documentation for more options, at [https://osmnx.readthedocs.io/en/stable/osmnx.html#osmnx.plot_graph](https://osmnx.readthedocs.io/en/stable/osmnx.html#osmnx.plot_graph).

        Raises:
            ValueError: If no network has been loaded yet.

        Examples:
            >>> streets = mapper.urban_layer.streets_roads().from_place("Oxford, UK")
            >>> streets.static_render(edge_color="grey", node_size=0)
        """
        ox.plot_graph(self.network.graph, show=True, close=False, **plot_kwargs)

    def preview(self, format: str = "ascii") -> Any:
        """Generate a preview of this urban layer.

        Produces a textual or structured representation of the `OSMNXStreets` layer for
        quick inspection, including metadata like the coordinate reference system and mappings.

        Args:
            format: Output format for the preview (default: "ascii").

                - [x] "ascii": Text-based format for terminal display
                - [x] "json": JSON-formatted data for programmatic use

        Returns:
            A string (for "ascii") or dictionary (for "json") representing the street network layer.

        Raises:
            ValueError: If an unsupported format is requested.

        Examples:
            >>> streets = mapper.urban_layer.streets_roads().from_place("Cambridge, UK")
            >>> print(streets.preview())
            >>> # JSON preview
            >>> import json
            >>> print(json.dumps(streets.preview(format="json"), indent=2))
        """
        mappings_str = (
            "\n".join(
                "Mapping:\n"
                f"    - lon={m.get('longitude_column', 'N/A')}, "
                f"lat={m.get('latitude_column', 'N/A')}, "
                f"output={m.get('output_column', 'N/A')}"
                for m in self.mappings
            )
            if self.mappings
            else "    No mappings"
        )
        if format == "ascii":
            return (
                f"Urban Layer: OSMNXStreets\n"
                f"  CRS: {self.coordinate_reference_system}\n"
                f"  Mappings:\n{mappings_str}"
            )
        elif format == "json":
            return {
                "urban_layer": "OSMNXStreets",
                "coordinate_reference_system": self.coordinate_reference_system,
                "mappings": self.mappings,
            }
        else:
            raise ValueError(f"Unsupported format '{format}'")

from_place(place_name, undirected=True, **kwargs)

Load a street network for a named place.

Retrieves the street network for a specified place name from OpenStreetMap, supporting cities, neighbourhoods, and other recognised geographic entities.

Parameters:

Name Type Description Default
place_name str

Name of the place to load (e.g., "Bristol, England").

required
undirected bool

Whether to convert the network to an undirected graph (default: True).

True
**kwargs

Additional parameters passed to OSMnx's graph_from_place.

{}

Returns:

Type Description
None

Self, enabling method chaining.

Examples:

>>> streets = mapper.urban_layer.streets_roads().from_place("Glasgow, Scotland")
>>> # Load only drivable streets
>>> streets = mapper.urban_layer.streets_roads().from_place(
...     "Cardiff, Wales", network_type="drive"
... )
Source code in src/urban_mapper/modules/urban_layer/urban_layers/osmnx_streets.py
def from_place(self, place_name: str, undirected: bool = True, **kwargs) -> None:
    """Load a street network for a named place.

    Retrieves the street network for a specified place name from `OpenStreetMap`,
    supporting `cities`, `neighbourhoods`, and other recognised geographic entities.

    Args:
        place_name: Name of the place to load (e.g., "Bristol, England").
        undirected: Whether to convert the network to an undirected graph (default: True).
        **kwargs: Additional parameters passed to OSMnx's graph_from_place.

    Returns:
        Self, enabling method chaining.

    Examples:
        >>> streets = mapper.urban_layer.streets_roads().from_place("Glasgow, Scotland")
        >>> # Load only drivable streets
        >>> streets = mapper.urban_layer.streets_roads().from_place(
        ...     "Cardiff, Wales", network_type="drive"
        ... )
    """
    self.network = StreetNetwork()
    self.network.load("place", query=place_name, undirected=undirected, **kwargs)
    self.network._graph = ox.convert.to_undirected(self.network.graph)
    gdf_nodes, gdf_edges = ox.graph_to_gdfs(self.network.graph)
    self.layer = gdf_edges.to_crs(self.coordinate_reference_system)

from_address(address, undirected=True, **kwargs)

Load a street network around a specified address.

Fetches the street network within a given distance of an address, requiring the distance to be specified in the keyword arguments.

Parameters:

Name Type Description Default
address str

The address to centre the network around (e.g., "10 Downing Street, London").

required
undirected bool

Whether to convert the network to an undirected graph (default: True).

True

Boots argues that the method retrieves the street network within a certain distance of a specified address. **kwargs: Additional parameters passed to OSMnx's graph_from_address. Must include 'dist' specifying the distance in metres.

Returns:

Type Description
None

Self, enabling method chaining.

Examples:

>>> streets = mapper.urban_layer.streets_roads().from_address(
...     "Buckingham Palace, London", dist=1000
... )
Source code in src/urban_mapper/modules/urban_layer/urban_layers/osmnx_streets.py
def from_address(self, address: str, undirected: bool = True, **kwargs) -> None:
    """Load a street network around a specified address.

    Fetches the street network within a given distance of an address, requiring
    the distance to be specified in the keyword arguments.

    Args:
        address: The address to centre the network around (e.g., "10 Downing Street, London").
        undirected: Whether to convert the network to an undirected graph (default: True).
    Boots argues that the method retrieves the street network within a certain distance of a specified address.
        **kwargs: Additional parameters passed to OSMnx's graph_from_address.
            Must include 'dist' specifying the distance in metres.

    Returns:
        Self, enabling method chaining.

    Examples:
        >>> streets = mapper.urban_layer.streets_roads().from_address(
        ...     "Buckingham Palace, London", dist=1000
        ... )
    """
    self.network = StreetNetwork()
    self.network.load("address", address=address, undirected=undirected, **kwargs)
    gdf_nodes, gdf_edges = ox.graph_to_gdfs(self.network.graph)
    self.layer = gdf_edges.to_crs(self.coordinate_reference_system)

from_bbox(bbox, undirected=True, **kwargs)

Load a street network within a bounding box.

Retrieves the street network contained within the specified bounding box coordinates.

Parameters:

Name Type Description Default
bbox Tuple[float, float, float, float]

Tuple of (left, bottom, right, top) coordinates defining the bounding box.

required
undirected bool

Whether to convert the network to an undirected graph (default: True).

True
**kwargs

Additional parameters passed to OSMnx's graph_from_bbox.

{}

Returns:

Type Description
None

Self, enabling method chaining.

Examples:

>>> streets = mapper.urban_layer.streets_roads().from_bbox(
...     (-0.13, 51.50, -0.09, 51.52)  # Central London
... )
Source code in src/urban_mapper/modules/urban_layer/urban_layers/osmnx_streets.py
def from_bbox(
    self, bbox: Tuple[float, float, float, float], undirected: bool = True, **kwargs
) -> None:
    """Load a street network within a bounding box.

    Retrieves the street network contained within the specified bounding box coordinates.

    Args:
        bbox: Tuple of (`left`, `bottom`, `right`, `top`) coordinates defining the bounding box.
        undirected: Whether to convert the network to an undirected graph (default: True).
        **kwargs: Additional parameters passed to OSMnx's graph_from_bbox.

    Returns:
        Self, enabling method chaining.

    Examples:
        >>> streets = mapper.urban_layer.streets_roads().from_bbox(
        ...     (-0.13, 51.50, -0.09, 51.52)  # Central London
        ... )
    """
    self.network = StreetNetwork()
    self.network.load("bbox", bbox=bbox, undirected=undirected, **kwargs)
    gdf_nodes, gdf_edges = ox.graph_to_gdfs(self.network.graph)
    self.layer = gdf_edges.to_crs(self.coordinate_reference_system)

from_point(center_point, undirected=True, **kwargs)

Load a street network around a specified point.

Fetches the street network within a certain distance of a geographic point, with the distance specified in the keyword arguments.

Parameters:

Name Type Description Default
center_point Tuple[float, float]

Tuple of (latitude, longitude) specifying the centre point.

required
undirected bool

Whether to convert the network to an undirected graph (default: True).

True
**kwargs

Additional parameters passed to OSMnx's graph_from_point. Must include 'dist' specifying the distance in metres.

{}

Returns:

Type Description
None

Self, enabling method chaining.

Examples:

>>> streets = mapper.urban_layer.streets_roads().from_point(
...     (51.5074, -0.1278), dist=500  # Near Trafalgar Square
... )
Source code in src/urban_mapper/modules/urban_layer/urban_layers/osmnx_streets.py
def from_point(
    self, center_point: Tuple[float, float], undirected: bool = True, **kwargs
) -> None:
    """Load a street network around a specified point.

    Fetches the street network within a certain distance of a geographic point,
    with the distance specified in the keyword arguments.

    Args:
        center_point: Tuple of (`latitude`, `longitude`) specifying the centre point.
        undirected: Whether to convert the network to an undirected graph (default: True).
        **kwargs: Additional parameters passed to OSMnx's graph_from_point.
            Must include 'dist' specifying the distance in metres.

    Returns:
        Self, enabling method chaining.

    Examples:
        >>> streets = mapper.urban_layer.streets_roads().from_point(
        ...     (51.5074, -0.1278), dist=500  # Near Trafalgar Square
        ... )
    """
    self.network = StreetNetwork()
    self.network.load(
        "point", center_point=center_point, undirected=undirected, **kwargs
    )
    gdf_nodes, gdf_edges = ox.graph_to_gdfs(self.network.graph)
    self.layer = gdf_edges.to_crs(self.coordinate_reference_system)

from_polygon(polygon, undirected=True, **kwargs)

Load a street network within a polygon boundary.

Retrieves the street network contained within the specified polygon boundary.

Parameters:

Name Type Description Default
polygon Polygon | MultiPolygon

Shapely Polygon or MultiPolygon defining the boundary.

required
undirected bool

Whether to convert the network to an undirected graph (default: True).

True
**kwargs

Additional parameters passed to OSMnx's graph_from_polygon.

{}

Returns:

Type Description
None

Self, enabling method chaining.

Examples:

>>> from shapely.geometry import Polygon
>>> boundary = Polygon([(-0.13, 51.50), (-0.09, 51.50), (-0.09, 51.52), (-0.13, 51.52)])
>>> streets = mapper.urban_layer.streets_roads().from_polygon(boundary)
Source code in src/urban_mapper/modules/urban_layer/urban_layers/osmnx_streets.py
def from_polygon(
    self, polygon: Polygon | MultiPolygon, undirected: bool = True, **kwargs
) -> None:
    """Load a street network within a polygon boundary.

    Retrieves the street network contained within the specified polygon boundary.

    Args:
        polygon: Shapely Polygon or MultiPolygon defining the boundary.
        undirected: Whether to convert the network to an undirected graph (default: True).
        **kwargs: Additional parameters passed to OSMnx's graph_from_polygon.

    Returns:
        Self, enabling method chaining.

    Examples:
        >>> from shapely.geometry import Polygon
        >>> boundary = Polygon([(-0.13, 51.50), (-0.09, 51.50), (-0.09, 51.52), (-0.13, 51.52)])
        >>> streets = mapper.urban_layer.streets_roads().from_polygon(boundary)
    """
    self.network = StreetNetwork()
    self.network.load("polygon", polygon=polygon, undirected=undirected, **kwargs)
    gdf_nodes, gdf_edges = ox.graph_to_gdfs(self.network.graph)
    self.layer = gdf_edges.to_crs(self.coordinate_reference_system)

from_xml(filepath, undirected=True, **kwargs)

Load a street network from an OSM XML file.

Loads a street network from a local OpenStreetMap XML file.

Parameters:

Name Type Description Default
filepath str | Path

Path to the OSM XML file.

required
undirected bool

Whether to convert the network to an undirected graph (default: True).

True
**kwargs

Additional parameters passed to OSMnx's graph_from_xml.

{}

Returns:

Type Description
None

Self, enabling method chaining.

Examples:

>>> streets = mapper.urban_layer.streets_roads().from_xml("london.osm")
Source code in src/urban_mapper/modules/urban_layer/urban_layers/osmnx_streets.py
def from_xml(self, filepath: str | Path, undirected: bool = True, **kwargs) -> None:
    """Load a street network from an OSM XML file.

    Loads a street network from a local OpenStreetMap XML file.

    Args:
        filepath: Path to the OSM XML file.
        undirected: Whether to convert the network to an undirected graph (default: True).
        **kwargs: Additional parameters passed to OSMnx's graph_from_xml.

    Returns:
        Self, enabling method chaining.

    Examples:
        >>> streets = mapper.urban_layer.streets_roads().from_xml("london.osm")
    """
    self.network = StreetNetwork()
    self.network.load("xml", filepath=filepath, undirected=undirected, **kwargs)
    gdf_nodes, gdf_edges = ox.graph_to_gdfs(self.network.graph)
    self.layer = gdf_edges.to_crs(self.coordinate_reference_system)

from_file(file_path, **kwargs)

Load a street network from a file.

Not Implemented

This method is not supported for OSMNXStreets. Use from_xml() for OSM XML files instead.

Parameters:

Name Type Description Default
file_path str | Path

Path to the file.

required
**kwargs

Additional parameters (not used).

{}

Raises:

Type Description
NotImplementedError

Always raised, as this method is not supported.

Source code in src/urban_mapper/modules/urban_layer/urban_layers/osmnx_streets.py
def from_file(self, file_path: str | Path, **kwargs) -> "OSMNXStreets":
    """Load a street network from a file.

    !!! warning "Not Implemented"
        This method is not supported for OSMNXStreets. Use from_xml() for OSM XML files instead.

    Args:
        file_path: Path to the file.
        **kwargs: Additional parameters (not used).

    Raises:
        NotImplementedError: Always raised, as this method is not supported.
    """
    raise NotImplementedError(
        "Loading from file is not supported for OSMNx street networks."
    )

_map_nearest_layer(data, longitude_column, latitude_column, output_column='nearest_street', threshold_distance=None, _reset_layer_index=True, **kwargs)

Map points to their nearest street edges.

This internal method identifies the nearest street edge for each point in the input GeoDataFrame, adding a reference to that edge as a new column. It’s primarily used by UrbanLayerBase.map_nearest_layer() to perform spatial joins between point data and the street network.

Parameters:

Name Type Description Default
data GeoDataFrame

GeoDataFrame containing point data to map.

required
longitude_column str

Name of the column with longitude values.

required
latitude_column str

Name of the column with latitude values.

required
output_column str

Name of the column to store nearest street indices (default: "nearest_street").

'nearest_street'
threshold_distance float | None

Maximum distance for a match, in CRS units (default: None).

None
_reset_layer_index bool

Whether to reset the layer GeoDataFrame’s index (default: True).

True
**kwargs

Additional parameters (not used).

{}

Returns:

Type Description
Tuple[GeoDataFrame, GeoDataFrame]

A tuple containing: - The street network GeoDataFrame (possibly with reset index) - The input GeoDataFrame with the new output_column (filtered if threshold_distance is set)

Source code in src/urban_mapper/modules/urban_layer/urban_layers/osmnx_streets.py
@require_attributes_not_none(
    "network",
    error_msg="Network not loaded. Call from_place() or other load methods first.",
)
def _map_nearest_layer(
    self,
    data: gpd.GeoDataFrame,
    longitude_column: str,
    latitude_column: str,
    output_column: str = "nearest_street",
    threshold_distance: float | None = None,
    _reset_layer_index: bool = True,
    **kwargs,
) -> Tuple[gpd.GeoDataFrame, gpd.GeoDataFrame]:
    """Map points to their nearest street edges.

    This internal method identifies the nearest street edge for each point in the
    input `GeoDataFrame`, adding a reference to that edge as a new column. It’s primarily
    used by `UrbanLayerBase.map_nearest_layer()` to perform spatial joins between point data and
    the street network.

    Args:
        data: `GeoDataFrame` containing point data to map.
        longitude_column: Name of the column with longitude values.
        latitude_column: Name of the column with latitude values.
        output_column: Name of the column to store nearest street indices (default: "nearest_street").
        threshold_distance: Maximum distance for a match, in CRS units (default: None).
        _reset_layer_index: Whether to reset the layer `GeoDataFrame`’s index (default: True).
        **kwargs: Additional parameters (not used).

    Returns:
        A tuple containing:
            - The street network `GeoDataFrame` (possibly with reset index)
            - The input `GeoDataFrame` with the new output_column (filtered if threshold_distance is set)
    """
    dataframe = data.copy()
    result = ox.distance.nearest_edges(
        self.network.graph,
        X=dataframe[longitude_column].values,
        Y=dataframe[latitude_column].values,
        return_dist=threshold_distance is not None,
    )
    if threshold_distance:
        nearest_edges, distances = result
        mask = np.array(distances) <= threshold_distance
        dataframe = dataframe[mask]
        nearest_edges = nearest_edges[mask]
    else:
        nearest_edges = result

    edge_to_idx = {k: i for i, k in enumerate(self.layer.index)}
    nearest_indices = [edge_to_idx[tuple(edge)] for edge in nearest_edges]

    dataframe[output_column] = nearest_indices
    if _reset_layer_index:
        self.layer = self.layer.reset_index()
    return self.layer, dataframe

get_layer()

Get the street network as a GeoDataFrame.

Returns the street network edges as a GeoDataFrame for further analysis or visualisation.

Returns:

Type Description
GeoDataFrame

GeoDataFrame containing the street network edges.

Raises:

Type Description
ValueError

If the layer has not been loaded yet.

Examples:

>>> streets = mapper.urban_layer.streets_roads().from_place("Birmingham, UK")
>>> streets_gdf = streets.get_layer()
>>> streets_gdf.plot()
Source code in src/urban_mapper/modules/urban_layer/urban_layers/osmnx_streets.py
@require_attributes_not_none(
    "layer", error_msg="Layer not built. Call from_place() first."
)
def get_layer(self) -> gpd.GeoDataFrame:
    """Get the street network as a GeoDataFrame.

    Returns the street network edges as a GeoDataFrame for further analysis or visualisation.

    Returns:
        GeoDataFrame containing the street network edges.

    Raises:
        ValueError: If the layer has not been loaded yet.

    Examples:
        >>> streets = mapper.urban_layer.streets_roads().from_place("Birmingham, UK")
        >>> streets_gdf = streets.get_layer()
        >>> streets_gdf.plot()
    """
    return self.layer

get_layer_bounding_box()

Get the bounding box of the street network.

Returns the bounding box coordinates of the street network, useful for spatial queries or visualisation extents.

Returns:

Type Description
Tuple[float, float, float, float]

Tuple of (left, bottom, right, top) coordinates defining the bounding box.

Raises:

Type Description
ValueError

If the layer has not been loaded yet.

Examples:

>>> streets = mapper.urban_layer.streets_roads().from_place("Leeds, UK")
>>> bbox = streets.get_layer_bounding_box()
>>> print(f"Extent: {bbox}")
Source code in src/urban_mapper/modules/urban_layer/urban_layers/osmnx_streets.py
@require_attributes_not_none(
    "layer", error_msg="Layer not built. Call from_place() first."
)
def get_layer_bounding_box(self) -> Tuple[float, float, float, float]:
    """Get the bounding box of the street network.

    Returns the bounding box coordinates of the street network, useful for spatial
    queries or visualisation extents.

    Returns:
        Tuple of (`left`, `bottom`, `right`, `top`) coordinates defining the bounding box.

    Raises:
        ValueError: If the layer has not been loaded yet.

    Examples:
        >>> streets = mapper.urban_layer.streets_roads().from_place("Leeds, UK")
        >>> bbox = streets.get_layer_bounding_box()
        >>> print(f"Extent: {bbox}")
    """
    return tuple(self.layer.total_bounds)  # type: ignore

static_render(**plot_kwargs)

Render the street network as a static plot.

Creates a static visualisation of the street network using OSMnx’s plotting capabilities, displayed immediately.

Parameters:

Name Type Description Default
**plot_kwargs

Additional keyword arguments passed to OSMnx’s plot_graph function, such as node_size, edge_linewidth, node_color, edge_color.

See further in OSMnx documentation for more options, at https://osmnx.readthedocs.io/en/stable/osmnx.html#osmnx.plot_graph.

{}

Raises:

Type Description
ValueError

If no network has been loaded yet.

Examples:

>>> streets = mapper.urban_layer.streets_roads().from_place("Oxford, UK")
>>> streets.static_render(edge_color="grey", node_size=0)
Source code in src/urban_mapper/modules/urban_layer/urban_layers/osmnx_streets.py
@require_attributes_not_none(
    "network", error_msg="No network loaded yet. Try from_place() first!"
)
def static_render(self, **plot_kwargs) -> None:
    """Render the street network as a static plot.

    Creates a static visualisation of the street network using `OSMnx`’s plotting
    capabilities, displayed immediately.

    Args:
        **plot_kwargs: Additional keyword arguments passed to `OSMnx`’s plot_graph function,
            such as `node_size`, `edge_linewidth`, `node_color`, `edge_color`.

            See further in `OSMnx` documentation for more options, at [https://osmnx.readthedocs.io/en/stable/osmnx.html#osmnx.plot_graph](https://osmnx.readthedocs.io/en/stable/osmnx.html#osmnx.plot_graph).

    Raises:
        ValueError: If no network has been loaded yet.

    Examples:
        >>> streets = mapper.urban_layer.streets_roads().from_place("Oxford, UK")
        >>> streets.static_render(edge_color="grey", node_size=0)
    """
    ox.plot_graph(self.network.graph, show=True, close=False, **plot_kwargs)

preview(format='ascii')

Generate a preview of this urban layer.

Produces a textual or structured representation of the OSMNXStreets layer for quick inspection, including metadata like the coordinate reference system and mappings.

Parameters:

Name Type Description Default
format str

Output format for the preview (default: "ascii").

  • "ascii": Text-based format for terminal display
  • "json": JSON-formatted data for programmatic use
'ascii'

Returns:

Type Description
Any

A string (for "ascii") or dictionary (for "json") representing the street network layer.

Raises:

Type Description
ValueError

If an unsupported format is requested.

Examples:

>>> streets = mapper.urban_layer.streets_roads().from_place("Cambridge, UK")
>>> print(streets.preview())
>>> # JSON preview
>>> import json
>>> print(json.dumps(streets.preview(format="json"), indent=2))
Source code in src/urban_mapper/modules/urban_layer/urban_layers/osmnx_streets.py
def preview(self, format: str = "ascii") -> Any:
    """Generate a preview of this urban layer.

    Produces a textual or structured representation of the `OSMNXStreets` layer for
    quick inspection, including metadata like the coordinate reference system and mappings.

    Args:
        format: Output format for the preview (default: "ascii").

            - [x] "ascii": Text-based format for terminal display
            - [x] "json": JSON-formatted data for programmatic use

    Returns:
        A string (for "ascii") or dictionary (for "json") representing the street network layer.

    Raises:
        ValueError: If an unsupported format is requested.

    Examples:
        >>> streets = mapper.urban_layer.streets_roads().from_place("Cambridge, UK")
        >>> print(streets.preview())
        >>> # JSON preview
        >>> import json
        >>> print(json.dumps(streets.preview(format="json"), indent=2))
    """
    mappings_str = (
        "\n".join(
            "Mapping:\n"
            f"    - lon={m.get('longitude_column', 'N/A')}, "
            f"lat={m.get('latitude_column', 'N/A')}, "
            f"output={m.get('output_column', 'N/A')}"
            for m in self.mappings
        )
        if self.mappings
        else "    No mappings"
    )
    if format == "ascii":
        return (
            f"Urban Layer: OSMNXStreets\n"
            f"  CRS: {self.coordinate_reference_system}\n"
            f"  Mappings:\n{mappings_str}"
        )
    elif format == "json":
        return {
            "urban_layer": "OSMNXStreets",
            "coordinate_reference_system": self.coordinate_reference_system,
            "mappings": self.mappings,
        }
    else:
        raise ValueError(f"Unsupported format '{format}'")

OSMNXIntersections

Bases: UrbanLayerBase

Urban layer implementation for OpenStreetMap street intersections.

This class provides methods for loading street intersection data from OpenStreetMap, and accessing it as urban layers. Intersections are represented as points (nodes in graph) where multiple street segments meet.

The class uses OSMnx to retrieve street networks and extract their nodes, which represent intersections. It implements the UrbanLayerBase interface, making it compatible with other UrbanMapper components.

When to use?

Street intersections are useful for:

  • Network analysis
  • Accessibility studies
  • Traffic modeling
  • Pedestrian safety analysis
  • Urban mobility research

Attributes:

Name Type Description
network StreetNetwork | None

The underlying StreetNetwork object that provides access to the OSMnx graph data.

layer GeoDataFrame | None

The GeoDataFrame containing the intersection points (set after loading).

Examples:

>>> from urban_mapper import UrbanMapper
>>>
>>> # Initialise UrbanMapper
>>> mapper = UrbanMapper()
>>>
>>> # Get intersections in Manhattan
>>> intersections = mapper.urban_layer.osmnx_intersections().from_place("Manhattan, New York")
>>>
>>> # Visualise the intersections
>>> intersections.static_render(node_size=5, node_color="red")
Source code in src/urban_mapper/modules/urban_layer/urban_layers/osmnx_intersections.py
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
@beartype
class OSMNXIntersections(UrbanLayerBase):
    """`Urban layer` implementation for `OpenStreetMap` `street intersections`.

    This class provides methods for loading `street intersection` data from `OpenStreetMap`,
    and accessing it as `urban layers`. `Intersections` are
    represented as points (nodes in graph) where multiple street segments meet.

    The class uses `OSMnx` to retrieve `street networks` and extract their `nodes`, which
    represent intersections. It implements the `UrbanLayerBase interface`, making it
    compatible with other `UrbanMapper components`.

    !!! tip "When to use?"
        Street intersections are useful for:

        - [x] Network analysis
        - [x] Accessibility studies
        - [x] Traffic modeling
        - [x] Pedestrian safety analysis
        - [x] Urban mobility research

    Attributes:
        network: The underlying `StreetNetwork` object that provides access to the
            `OSMnx` graph data.
        layer: The `GeoDataFrame` containing the intersection points (set after loading).

    Examples:
        >>> from urban_mapper import UrbanMapper
        >>>
        >>> # Initialise UrbanMapper
        >>> mapper = UrbanMapper()
        >>>
        >>> # Get intersections in Manhattan
        >>> intersections = mapper.urban_layer.osmnx_intersections().from_place("Manhattan, New York")
        >>>
        >>> # Visualise the intersections
        >>> intersections.static_render(node_size=5, node_color="red")
    """

    def __init__(self) -> None:
        """Initialise an empty `OSMNXIntersections` instance.

        Sets up an empty street intersections layer with default settings.
        """
        super().__init__()
        self.network: StreetNetwork | None = None

    def from_place(self, place_name: str, undirected: bool = True, **kwargs) -> None:
        """Load `street intersections` for a named place.

        This method retrieves street network data for a specified place name and
        extracts the intersections (nodes) from that network. The place name is
        geocoded to determine the appropriate area to query.

        Args:
            place_name: Name of the place to load intersections for
                (e.g., "Manhattan, New York", "Paris, France").
            undirected: Whether to consider the street network as undirected
                (default: True). When True, one-way streets are treated as
                bidirectional for the purposes of identifying intersections.
            **kwargs: Additional parameters passed to OSMnx's network retrieval
                functions. Common parameters include:

                - [x] network_type: Type of street network to retrieve ("drive",
                  "walk", "bike", etc.)
                - [x] simplify: Whether to simplify the network topology (default: True)
                - [x] retain_all: Whether to retain isolated nodes (default: False)

                More can be explored in OSMnx's documentation at [https://osmnx.readthedocs.io/en/stable/](https://osmnx.readthedocs.io/en/stable/).

        Returns:
            Self, for method chaining.

        Examples:
            >>> # Get walkable intersections in Brooklyn
            >>> intersections = OSMNXIntersections().from_place(
            ...     "Brooklyn, New York",
            ...     network_type="walk"
            ... )
            >>>
            >>> # Get all intersections, including isolated ones
            >>> all_intersections = OSMNXIntersections().from_place(
            ...     "Boston, MA",
            ...     retain_all=True
            ... )
        """
        self.network = StreetNetwork()
        self.network.load("place", query=place_name, undirected=undirected, **kwargs)
        gdf_nodes, gdf_edges = ox.graph_to_gdfs(self.network.graph)
        self.layer = gdf_nodes.to_crs(self.coordinate_reference_system)

    def from_address(self, address: str, undirected: bool = True, **kwargs) -> None:
        """Load `street intersections` for a specific address.

        This method retrieves street network data for a specified address and
        extracts the intersections (nodes) from that network. The address is geocoded
        to determine the appropriate area to query.

        Args:
            address: Address to load intersections for (e.g., "1600 Amphitheatre Parkway, Mountain View, CA").
            undirected: Whether to consider the street network as undirected
                (default: True). When True, one-way streets are treated as
                bidirectional for the purposes of identifying intersections.
            **kwargs: Additional parameters passed to OSMnx's network retrieval
                functions. Common parameters include:

                - [x] network_type: Type of street network to retrieve ("drive",
                  "walk", "bike", etc.)
                - [x] simplify: Whether to simplify the network topology (default: True)
                - [x] retain_all: Whether to retain isolated nodes (default: False)

                More can be explored in OSMnx's documentation at [https://osmnx.readthedocs.io/en/stable/](https://osmnx.readthedocs.io/en/stable/).

        Returns:
            Self, for method chaining.
        """
        self.network = StreetNetwork()
        self.network.load("address", address=address, undirected=undirected, **kwargs)
        gdf_nodes, gdf_edges = ox.graph_to_gdfs(self.network.graph)
        self.layer = gdf_nodes.to_crs(self.coordinate_reference_system)

    def from_bbox(
        self, bbox: Tuple[float, float, float, float], undirected: bool = True, **kwargs
    ) -> None:
        """Load `street intersections` for a specified bounding box.

        This method retrieves street network data for a specified bounding box
        and extracts the intersections (nodes) from that network. The bounding box
        is defined by its southwest and northeast corners.

        Args:
            bbox: Bounding box defined by southwest and northeast corners
                (e.g., (southwest_latitude, southwest_longitude, northeast_latitude, northeast_longitude)).
            undirected: Whether to consider the street network as undirected
                (default: True). When True, one-way streets are treated as
                bidirectional for the purposes of identifying intersections.
            **kwargs: Additional parameters passed to OSMnx's network retrieval
                functions. Common parameters include:

                - [x] network_type: Type of street network to retrieve ("drive",
                  "walk", "bike", etc.)
                - [x] simplify: Whether to simplify the network topology (default: True)
                - [x] retain_all: Whether to retain isolated nodes (default: False)

                More can be explored in OSMnx's documentation at [https://osmnx.readthedocs.io/en/stable/](https://osmnx.readthedocs.io/en/stable/).

        Returns:
            Self, for method chaining.
        """
        self.network = StreetNetwork()
        self.network.load("bbox", bbox=bbox, undirected=undirected, **kwargs)
        gdf_nodes, gdf_edges = ox.graph_to_gdfs(self.network.graph)
        self.layer = gdf_nodes.to_crs(self.coordinate_reference_system)

    def from_point(
        self, center_point: Tuple[float, float], undirected: bool = True, **kwargs
    ) -> None:
        """Load `street intersections` for a specified center point.

        This method retrieves street network data for a specified center point
        and extracts the intersections (nodes) from that network. The center point
        is used to determine the area to query.

        Args:
            center_point: Center point defined by its latitude and longitude
                (e.g., (latitude, longitude)).
            undirected: Whether to consider the street network as undirected
                (default: True). When True, one-way streets are treated as
                bidirectional for the purposes of identifying intersections.
            **kwargs: Additional parameters passed to OSMnx's network retrieval
                functions. Common parameters include:

                - [x] network_type: Type of street network to retrieve ("drive",
                  "walk", "bike", etc.)
                - [x] simplify: Whether to simplify the network topology (default: True)
                - [x] retain_all: Whether to retain isolated nodes (default: False)

                More can be explored in OSMnx's documentation at [https://osmnx.readthedocs.io/en/stable/](https://osmnx.readthedocs.io/en/stable/).

        Returns:
            Self, for method chaining.
        """
        self.network = StreetNetwork()
        self.network.load(
            "point", center_point=center_point, undirected=undirected, **kwargs
        )
        gdf_nodes, gdf_edges = ox.graph_to_gdfs(self.network.graph)
        self.layer = gdf_nodes.to_crs(self.coordinate_reference_system)

    def from_polygon(
        self, polygon: Polygon | MultiPolygon, undirected: bool = True, **kwargs
    ) -> None:
        """Load `street intersections` for a specified polygon.

        This method retrieves street network data for a specified polygon
        and extracts the intersections (nodes) from that network. The polygon
        is used to determine the area to query.

        Args:
            polygon: Polygon or MultiPolygon defining the area of interest.
            undirected: Whether to consider the street network as undirected
                (default: True). When True, one-way streets are treated as
                bidirectional for the purposes of identifying intersections.
            **kwargs: Additional parameters passed to OSMnx's network retrieval
                functions. Common parameters include:

                - [x] network_type: Type of street network to retrieve ("drive",
                  "walk", "bike", etc.)
                - [x] simplify: Whether to simplify the network topology (default: True)
                - [x] retain_all: Whether to retain isolated nodes (default: False)

                More can be explored in OSMnx's documentation at [https://osmnx.readthedocs.io/en/stable/](https://osmnx.readthedocs.io/en/stable/).

        Returns:
            Self, for method chaining.
        """
        self.network = StreetNetwork()
        self.network.load("polygon", polygon=polygon, undirected=undirected, **kwargs)
        gdf_nodes, gdf_edges = ox.graph_to_gdfs(self.network.graph)
        self.layer = gdf_nodes.to_crs(self.coordinate_reference_system)

    def from_xml(self, filepath: str | Path, undirected: bool = True, **kwargs) -> None:
        """Load `street intersections` from a specified XML file.

        This method retrieves street network data from a specified XML file
        and extracts the intersections (nodes) from that network.

        Args:
            filepath: Path to the XML file containing street network data.
            undirected: Whether to consider the street network as undirected
                (default: True). When True, one-way streets are treated as
                bidirectional for the purposes of identifying intersections.
            **kwargs: Additional parameters passed to OSMnx's network retrieval
                functions. Common parameters include:

                - [x] network_type: Type of street network to retrieve ("drive",
                  "walk", "bike", etc.)
                - [x] simplify: Whether to simplify the network topology (default: True)
                - [x] retain_all: Whether to retain isolated nodes (default: False)

                More can be explored in OSMnx's documentation at [https://osmnx.readthedocs.io/en/stable/](https://osmnx.readthedocs.io/en/stable/).

        Returns:
            Self, for method chaining.
        """
        self.network = StreetNetwork()
        self.network.load("xml", filepath=filepath, undirected=undirected, **kwargs)
        gdf_nodes, gdf_edges = ox.graph_to_gdfs(self.network.graph)
        self.layer = gdf_nodes.to_crs(self.coordinate_reference_system)

    def from_file(self, file_path: str | Path, **kwargs) -> None:
        """Load `street intersections` from a specified file.

        !!! danger "Not implemented"
            This method is not implemented for `OSMNXIntersections`. It raises a
            `NotImplementedError` if called.
        """
        raise NotImplementedError(
            "Loading from file is not supported for OSMNx intersection networks."
        )

    @require_attributes_not_none(
        "network",
        error_msg="Network not loaded. Call from_place() or other load methods first.",
    )
    def _map_nearest_layer(
        self,
        data: gpd.GeoDataFrame,
        longitude_column: str,
        latitude_column: str,
        output_column: str = "nearest_node_idx",
        threshold_distance: float | None = None,
        _reset_layer_index: bool = True,
        **kwargs,
    ) -> Tuple[gpd.GeoDataFrame, gpd.GeoDataFrame]:
        """Map points to their nearest `street intersections`.

        This internal method finds the nearest intersection (node) for each point in
        the input `GeoDataFrame` and adds a reference to that intersection as a new column.
        It's primarily used by the `UrbanLayerBase.map_nearest_layer()` method to
        implement spatial joining between point data and street intersections.

        The method uses `OSMnx's nearest_nodes` function, which efficiently identifies
        the closest node in the street network to each input point. If a threshold
        distance is specified, points beyond that distance will not be matched.

        Args:
            data: `GeoDataFrame` containing point data to map.
            longitude_column: Name of the column containing longitude values.
            latitude_column: Name of the column containing latitude values.
            output_column: Name of the column to store the indices of nearest nodes.
            threshold_distance: Maximum distance to consider a match, in the CRS units.
            _reset_layer_index: Whether to reset the index of the layer `GeoDataFrame`.
            **kwargs: Additional parameters (not used).

        Returns:
            A tuple containing:
                - The intersections `GeoDataFrame` (possibly with reset index)
                - The input `GeoDataFrame` with the new output_column added
                  (filtered if threshold_distance was specified)

        Notes:
            Unlike some other spatial joins which use GeoPandas' sjoin_nearest,
            this method uses OSMnx's optimised nearest_nodes function which
            is specifically designed for network analysis.
        """
        dataframe = data.copy()

        result = ox.distance.nearest_nodes(
            self.network.graph,
            X=dataframe[longitude_column].values,
            Y=dataframe[latitude_column].values,
            return_dist=threshold_distance is not None,
        )
        if threshold_distance:
            nearest_nodes, distances = result
            mask = np.array(distances) <= threshold_distance
            dataframe = dataframe[mask]
            nearest_nodes = nearest_nodes[mask]
        else:
            nearest_nodes = result

        edge_to_idx = {k: i for i, k in enumerate(self.layer.index)}
        nearest_indices = [edge_to_idx[edge] for edge in nearest_nodes]

        dataframe[output_column] = nearest_indices
        if _reset_layer_index:
            self.layer = self.layer.reset_index()
        return self.layer, dataframe

    @require_attributes_not_none(
        "layer", error_msg="Layer not built. Call from_place() first."
    )
    def get_layer(self) -> gpd.GeoDataFrame:
        """Get the `GeoDataFrame` of the layer.

        This method returns the `GeoDataFrame` containing the loaded street intersections.

        Returns:
            gpd.GeoDataFrame: The `GeoDataFrame` containing the street intersections.
        """
        return self.layer

    @require_attributes_not_none(
        "layer", error_msg="Layer not built. Call from_place() first."
    )
    def get_layer_bounding_box(self) -> Tuple[float, float, float, float]:
        """Get the bounding box of the layer.

        This method returns the bounding box of the loaded street intersections layer.

        Returns:
            Tuple[float, float, float, float]: The bounding box of the layer in the
                format (`minx`, `miny`, `maxx`, `maxy`).
        """
        return tuple(self.layer.total_bounds)  # type: ignore

    @require_attributes_not_none(
        "network", error_msg="No network loaded yet. Try from_place() first!"
    )
    def static_render(self, **plot_kwargs) -> None:
        """Render the `street intersections` on a static map.

        This method uses `OSMnx` to plot the street intersections on a static map.
        It can be used to visualise the intersections in the context of the
        surrounding street network.

        Args:
            **plot_kwargs: Additional parameters for the `OSMnx` plot function.
                Common parameters include:

                - [x] node_size: Size of the nodes (intersections) in the plot.
                - [x] node_color: Color of the nodes (intersections) in the plot.

                More can be explored in OSMnx's documentation at [https://osmnx.readthedocs.io/en/stable/](https://osmnx.readthedocs.io/en/stable/).
        """
        ox.plot_graph(self.network.graph, show=True, close=False, **plot_kwargs)

    def preview(self, format: str = "ascii") -> Any:
        """Preview the `OSMNXIntersections` layer.

        This method provides a summary of the `OSMNXIntersections` layer,
        including the coordinate reference system and mappings.
        It can return the preview in either ASCII or JSON format.

        Args:
            format: The format of the preview. Can be "ascii" or "json".

                - [x] "ascii": Returns a human-readable string preview.
                - [x] "json": Returns a JSON object with the layer details.

        Returns:
            str | dict: The preview of the `OSMNXIntersections` layer in the specified format.

        Raises:
            ValueError: If the specified format is not supported.
        """
        mappings_str = (
            "\n".join(
                "Mapping:\n"
                f"    - lon={m.get('longitude_column', 'N/A')}, "
                f"lat={m.get('latitude_column', 'N/A')}, "
                f"output={m.get('output_column', 'N/A')}"
                for m in self.mappings
            )
            if self.mappings
            else "    No mappings"
        )
        if format == "ascii":
            return (
                f"Urban Layer: OSMNXIntersections\n"
                f"  CRS: {self.coordinate_reference_system}\n"
                f"  Mappings:\n{mappings_str}"
            )
        elif format == "json":
            return {
                "urban_layer": "OSMNXIntersections",
                "coordinate_reference_system": self.coordinate_reference_system,
                "mappings": self.mappings,
            }
        else:
            raise ValueError(f"Unsupported format '{format}'")

from_place(place_name, undirected=True, **kwargs)

Load street intersections for a named place.

This method retrieves street network data for a specified place name and extracts the intersections (nodes) from that network. The place name is geocoded to determine the appropriate area to query.

Parameters:

Name Type Description Default
place_name str

Name of the place to load intersections for (e.g., "Manhattan, New York", "Paris, France").

required
undirected bool

Whether to consider the street network as undirected (default: True). When True, one-way streets are treated as bidirectional for the purposes of identifying intersections.

True
**kwargs

Additional parameters passed to OSMnx's network retrieval functions. Common parameters include:

  • network_type: Type of street network to retrieve ("drive", "walk", "bike", etc.)
  • simplify: Whether to simplify the network topology (default: True)
  • retain_all: Whether to retain isolated nodes (default: False)

More can be explored in OSMnx's documentation at https://osmnx.readthedocs.io/en/stable/.

{}

Returns:

Type Description
None

Self, for method chaining.

Examples:

>>> # Get walkable intersections in Brooklyn
>>> intersections = OSMNXIntersections().from_place(
...     "Brooklyn, New York",
...     network_type="walk"
... )
>>>
>>> # Get all intersections, including isolated ones
>>> all_intersections = OSMNXIntersections().from_place(
...     "Boston, MA",
...     retain_all=True
... )
Source code in src/urban_mapper/modules/urban_layer/urban_layers/osmnx_intersections.py
def from_place(self, place_name: str, undirected: bool = True, **kwargs) -> None:
    """Load `street intersections` for a named place.

    This method retrieves street network data for a specified place name and
    extracts the intersections (nodes) from that network. The place name is
    geocoded to determine the appropriate area to query.

    Args:
        place_name: Name of the place to load intersections for
            (e.g., "Manhattan, New York", "Paris, France").
        undirected: Whether to consider the street network as undirected
            (default: True). When True, one-way streets are treated as
            bidirectional for the purposes of identifying intersections.
        **kwargs: Additional parameters passed to OSMnx's network retrieval
            functions. Common parameters include:

            - [x] network_type: Type of street network to retrieve ("drive",
              "walk", "bike", etc.)
            - [x] simplify: Whether to simplify the network topology (default: True)
            - [x] retain_all: Whether to retain isolated nodes (default: False)

            More can be explored in OSMnx's documentation at [https://osmnx.readthedocs.io/en/stable/](https://osmnx.readthedocs.io/en/stable/).

    Returns:
        Self, for method chaining.

    Examples:
        >>> # Get walkable intersections in Brooklyn
        >>> intersections = OSMNXIntersections().from_place(
        ...     "Brooklyn, New York",
        ...     network_type="walk"
        ... )
        >>>
        >>> # Get all intersections, including isolated ones
        >>> all_intersections = OSMNXIntersections().from_place(
        ...     "Boston, MA",
        ...     retain_all=True
        ... )
    """
    self.network = StreetNetwork()
    self.network.load("place", query=place_name, undirected=undirected, **kwargs)
    gdf_nodes, gdf_edges = ox.graph_to_gdfs(self.network.graph)
    self.layer = gdf_nodes.to_crs(self.coordinate_reference_system)

from_address(address, undirected=True, **kwargs)

Load street intersections for a specific address.

This method retrieves street network data for a specified address and extracts the intersections (nodes) from that network. The address is geocoded to determine the appropriate area to query.

Parameters:

Name Type Description Default
address str

Address to load intersections for (e.g., "1600 Amphitheatre Parkway, Mountain View, CA").

required
undirected bool

Whether to consider the street network as undirected (default: True). When True, one-way streets are treated as bidirectional for the purposes of identifying intersections.

True
**kwargs

Additional parameters passed to OSMnx's network retrieval functions. Common parameters include:

  • network_type: Type of street network to retrieve ("drive", "walk", "bike", etc.)
  • simplify: Whether to simplify the network topology (default: True)
  • retain_all: Whether to retain isolated nodes (default: False)

More can be explored in OSMnx's documentation at https://osmnx.readthedocs.io/en/stable/.

{}

Returns:

Type Description
None

Self, for method chaining.

Source code in src/urban_mapper/modules/urban_layer/urban_layers/osmnx_intersections.py
def from_address(self, address: str, undirected: bool = True, **kwargs) -> None:
    """Load `street intersections` for a specific address.

    This method retrieves street network data for a specified address and
    extracts the intersections (nodes) from that network. The address is geocoded
    to determine the appropriate area to query.

    Args:
        address: Address to load intersections for (e.g., "1600 Amphitheatre Parkway, Mountain View, CA").
        undirected: Whether to consider the street network as undirected
            (default: True). When True, one-way streets are treated as
            bidirectional for the purposes of identifying intersections.
        **kwargs: Additional parameters passed to OSMnx's network retrieval
            functions. Common parameters include:

            - [x] network_type: Type of street network to retrieve ("drive",
              "walk", "bike", etc.)
            - [x] simplify: Whether to simplify the network topology (default: True)
            - [x] retain_all: Whether to retain isolated nodes (default: False)

            More can be explored in OSMnx's documentation at [https://osmnx.readthedocs.io/en/stable/](https://osmnx.readthedocs.io/en/stable/).

    Returns:
        Self, for method chaining.
    """
    self.network = StreetNetwork()
    self.network.load("address", address=address, undirected=undirected, **kwargs)
    gdf_nodes, gdf_edges = ox.graph_to_gdfs(self.network.graph)
    self.layer = gdf_nodes.to_crs(self.coordinate_reference_system)

from_bbox(bbox, undirected=True, **kwargs)

Load street intersections for a specified bounding box.

This method retrieves street network data for a specified bounding box and extracts the intersections (nodes) from that network. The bounding box is defined by its southwest and northeast corners.

Parameters:

Name Type Description Default
bbox Tuple[float, float, float, float]

Bounding box defined by southwest and northeast corners (e.g., (southwest_latitude, southwest_longitude, northeast_latitude, northeast_longitude)).

required
undirected bool

Whether to consider the street network as undirected (default: True). When True, one-way streets are treated as bidirectional for the purposes of identifying intersections.

True
**kwargs

Additional parameters passed to OSMnx's network retrieval functions. Common parameters include:

  • network_type: Type of street network to retrieve ("drive", "walk", "bike", etc.)
  • simplify: Whether to simplify the network topology (default: True)
  • retain_all: Whether to retain isolated nodes (default: False)

More can be explored in OSMnx's documentation at https://osmnx.readthedocs.io/en/stable/.

{}

Returns:

Type Description
None

Self, for method chaining.

Source code in src/urban_mapper/modules/urban_layer/urban_layers/osmnx_intersections.py
def from_bbox(
    self, bbox: Tuple[float, float, float, float], undirected: bool = True, **kwargs
) -> None:
    """Load `street intersections` for a specified bounding box.

    This method retrieves street network data for a specified bounding box
    and extracts the intersections (nodes) from that network. The bounding box
    is defined by its southwest and northeast corners.

    Args:
        bbox: Bounding box defined by southwest and northeast corners
            (e.g., (southwest_latitude, southwest_longitude, northeast_latitude, northeast_longitude)).
        undirected: Whether to consider the street network as undirected
            (default: True). When True, one-way streets are treated as
            bidirectional for the purposes of identifying intersections.
        **kwargs: Additional parameters passed to OSMnx's network retrieval
            functions. Common parameters include:

            - [x] network_type: Type of street network to retrieve ("drive",
              "walk", "bike", etc.)
            - [x] simplify: Whether to simplify the network topology (default: True)
            - [x] retain_all: Whether to retain isolated nodes (default: False)

            More can be explored in OSMnx's documentation at [https://osmnx.readthedocs.io/en/stable/](https://osmnx.readthedocs.io/en/stable/).

    Returns:
        Self, for method chaining.
    """
    self.network = StreetNetwork()
    self.network.load("bbox", bbox=bbox, undirected=undirected, **kwargs)
    gdf_nodes, gdf_edges = ox.graph_to_gdfs(self.network.graph)
    self.layer = gdf_nodes.to_crs(self.coordinate_reference_system)

from_point(center_point, undirected=True, **kwargs)

Load street intersections for a specified center point.

This method retrieves street network data for a specified center point and extracts the intersections (nodes) from that network. The center point is used to determine the area to query.

Parameters:

Name Type Description Default
center_point Tuple[float, float]

Center point defined by its latitude and longitude (e.g., (latitude, longitude)).

required
undirected bool

Whether to consider the street network as undirected (default: True). When True, one-way streets are treated as bidirectional for the purposes of identifying intersections.

True
**kwargs

Additional parameters passed to OSMnx's network retrieval functions. Common parameters include:

  • network_type: Type of street network to retrieve ("drive", "walk", "bike", etc.)
  • simplify: Whether to simplify the network topology (default: True)
  • retain_all: Whether to retain isolated nodes (default: False)

More can be explored in OSMnx's documentation at https://osmnx.readthedocs.io/en/stable/.

{}

Returns:

Type Description
None

Self, for method chaining.

Source code in src/urban_mapper/modules/urban_layer/urban_layers/osmnx_intersections.py
def from_point(
    self, center_point: Tuple[float, float], undirected: bool = True, **kwargs
) -> None:
    """Load `street intersections` for a specified center point.

    This method retrieves street network data for a specified center point
    and extracts the intersections (nodes) from that network. The center point
    is used to determine the area to query.

    Args:
        center_point: Center point defined by its latitude and longitude
            (e.g., (latitude, longitude)).
        undirected: Whether to consider the street network as undirected
            (default: True). When True, one-way streets are treated as
            bidirectional for the purposes of identifying intersections.
        **kwargs: Additional parameters passed to OSMnx's network retrieval
            functions. Common parameters include:

            - [x] network_type: Type of street network to retrieve ("drive",
              "walk", "bike", etc.)
            - [x] simplify: Whether to simplify the network topology (default: True)
            - [x] retain_all: Whether to retain isolated nodes (default: False)

            More can be explored in OSMnx's documentation at [https://osmnx.readthedocs.io/en/stable/](https://osmnx.readthedocs.io/en/stable/).

    Returns:
        Self, for method chaining.
    """
    self.network = StreetNetwork()
    self.network.load(
        "point", center_point=center_point, undirected=undirected, **kwargs
    )
    gdf_nodes, gdf_edges = ox.graph_to_gdfs(self.network.graph)
    self.layer = gdf_nodes.to_crs(self.coordinate_reference_system)

from_polygon(polygon, undirected=True, **kwargs)

Load street intersections for a specified polygon.

This method retrieves street network data for a specified polygon and extracts the intersections (nodes) from that network. The polygon is used to determine the area to query.

Parameters:

Name Type Description Default
polygon Polygon | MultiPolygon

Polygon or MultiPolygon defining the area of interest.

required
undirected bool

Whether to consider the street network as undirected (default: True). When True, one-way streets are treated as bidirectional for the purposes of identifying intersections.

True
**kwargs

Additional parameters passed to OSMnx's network retrieval functions. Common parameters include:

  • network_type: Type of street network to retrieve ("drive", "walk", "bike", etc.)
  • simplify: Whether to simplify the network topology (default: True)
  • retain_all: Whether to retain isolated nodes (default: False)

More can be explored in OSMnx's documentation at https://osmnx.readthedocs.io/en/stable/.

{}

Returns:

Type Description
None

Self, for method chaining.

Source code in src/urban_mapper/modules/urban_layer/urban_layers/osmnx_intersections.py
def from_polygon(
    self, polygon: Polygon | MultiPolygon, undirected: bool = True, **kwargs
) -> None:
    """Load `street intersections` for a specified polygon.

    This method retrieves street network data for a specified polygon
    and extracts the intersections (nodes) from that network. The polygon
    is used to determine the area to query.

    Args:
        polygon: Polygon or MultiPolygon defining the area of interest.
        undirected: Whether to consider the street network as undirected
            (default: True). When True, one-way streets are treated as
            bidirectional for the purposes of identifying intersections.
        **kwargs: Additional parameters passed to OSMnx's network retrieval
            functions. Common parameters include:

            - [x] network_type: Type of street network to retrieve ("drive",
              "walk", "bike", etc.)
            - [x] simplify: Whether to simplify the network topology (default: True)
            - [x] retain_all: Whether to retain isolated nodes (default: False)

            More can be explored in OSMnx's documentation at [https://osmnx.readthedocs.io/en/stable/](https://osmnx.readthedocs.io/en/stable/).

    Returns:
        Self, for method chaining.
    """
    self.network = StreetNetwork()
    self.network.load("polygon", polygon=polygon, undirected=undirected, **kwargs)
    gdf_nodes, gdf_edges = ox.graph_to_gdfs(self.network.graph)
    self.layer = gdf_nodes.to_crs(self.coordinate_reference_system)

from_xml(filepath, undirected=True, **kwargs)

Load street intersections from a specified XML file.

This method retrieves street network data from a specified XML file and extracts the intersections (nodes) from that network.

Parameters:

Name Type Description Default
filepath str | Path

Path to the XML file containing street network data.

required
undirected bool

Whether to consider the street network as undirected (default: True). When True, one-way streets are treated as bidirectional for the purposes of identifying intersections.

True
**kwargs

Additional parameters passed to OSMnx's network retrieval functions. Common parameters include:

  • network_type: Type of street network to retrieve ("drive", "walk", "bike", etc.)
  • simplify: Whether to simplify the network topology (default: True)
  • retain_all: Whether to retain isolated nodes (default: False)

More can be explored in OSMnx's documentation at https://osmnx.readthedocs.io/en/stable/.

{}

Returns:

Type Description
None

Self, for method chaining.

Source code in src/urban_mapper/modules/urban_layer/urban_layers/osmnx_intersections.py
def from_xml(self, filepath: str | Path, undirected: bool = True, **kwargs) -> None:
    """Load `street intersections` from a specified XML file.

    This method retrieves street network data from a specified XML file
    and extracts the intersections (nodes) from that network.

    Args:
        filepath: Path to the XML file containing street network data.
        undirected: Whether to consider the street network as undirected
            (default: True). When True, one-way streets are treated as
            bidirectional for the purposes of identifying intersections.
        **kwargs: Additional parameters passed to OSMnx's network retrieval
            functions. Common parameters include:

            - [x] network_type: Type of street network to retrieve ("drive",
              "walk", "bike", etc.)
            - [x] simplify: Whether to simplify the network topology (default: True)
            - [x] retain_all: Whether to retain isolated nodes (default: False)

            More can be explored in OSMnx's documentation at [https://osmnx.readthedocs.io/en/stable/](https://osmnx.readthedocs.io/en/stable/).

    Returns:
        Self, for method chaining.
    """
    self.network = StreetNetwork()
    self.network.load("xml", filepath=filepath, undirected=undirected, **kwargs)
    gdf_nodes, gdf_edges = ox.graph_to_gdfs(self.network.graph)
    self.layer = gdf_nodes.to_crs(self.coordinate_reference_system)

from_file(file_path, **kwargs)

Load street intersections from a specified file.

Not implemented

This method is not implemented for OSMNXIntersections. It raises a NotImplementedError if called.

Source code in src/urban_mapper/modules/urban_layer/urban_layers/osmnx_intersections.py
def from_file(self, file_path: str | Path, **kwargs) -> None:
    """Load `street intersections` from a specified file.

    !!! danger "Not implemented"
        This method is not implemented for `OSMNXIntersections`. It raises a
        `NotImplementedError` if called.
    """
    raise NotImplementedError(
        "Loading from file is not supported for OSMNx intersection networks."
    )

_map_nearest_layer(data, longitude_column, latitude_column, output_column='nearest_node_idx', threshold_distance=None, _reset_layer_index=True, **kwargs)

Map points to their nearest street intersections.

This internal method finds the nearest intersection (node) for each point in the input GeoDataFrame and adds a reference to that intersection as a new column. It's primarily used by the UrbanLayerBase.map_nearest_layer() method to implement spatial joining between point data and street intersections.

The method uses OSMnx's nearest_nodes function, which efficiently identifies the closest node in the street network to each input point. If a threshold distance is specified, points beyond that distance will not be matched.

Parameters:

Name Type Description Default
data GeoDataFrame

GeoDataFrame containing point data to map.

required
longitude_column str

Name of the column containing longitude values.

required
latitude_column str

Name of the column containing latitude values.

required
output_column str

Name of the column to store the indices of nearest nodes.

'nearest_node_idx'
threshold_distance float | None

Maximum distance to consider a match, in the CRS units.

None
_reset_layer_index bool

Whether to reset the index of the layer GeoDataFrame.

True
**kwargs

Additional parameters (not used).

{}

Returns:

Type Description
Tuple[GeoDataFrame, GeoDataFrame]

A tuple containing: - The intersections GeoDataFrame (possibly with reset index) - The input GeoDataFrame with the new output_column added (filtered if threshold_distance was specified)

Notes

Unlike some other spatial joins which use GeoPandas' sjoin_nearest, this method uses OSMnx's optimised nearest_nodes function which is specifically designed for network analysis.

Source code in src/urban_mapper/modules/urban_layer/urban_layers/osmnx_intersections.py
@require_attributes_not_none(
    "network",
    error_msg="Network not loaded. Call from_place() or other load methods first.",
)
def _map_nearest_layer(
    self,
    data: gpd.GeoDataFrame,
    longitude_column: str,
    latitude_column: str,
    output_column: str = "nearest_node_idx",
    threshold_distance: float | None = None,
    _reset_layer_index: bool = True,
    **kwargs,
) -> Tuple[gpd.GeoDataFrame, gpd.GeoDataFrame]:
    """Map points to their nearest `street intersections`.

    This internal method finds the nearest intersection (node) for each point in
    the input `GeoDataFrame` and adds a reference to that intersection as a new column.
    It's primarily used by the `UrbanLayerBase.map_nearest_layer()` method to
    implement spatial joining between point data and street intersections.

    The method uses `OSMnx's nearest_nodes` function, which efficiently identifies
    the closest node in the street network to each input point. If a threshold
    distance is specified, points beyond that distance will not be matched.

    Args:
        data: `GeoDataFrame` containing point data to map.
        longitude_column: Name of the column containing longitude values.
        latitude_column: Name of the column containing latitude values.
        output_column: Name of the column to store the indices of nearest nodes.
        threshold_distance: Maximum distance to consider a match, in the CRS units.
        _reset_layer_index: Whether to reset the index of the layer `GeoDataFrame`.
        **kwargs: Additional parameters (not used).

    Returns:
        A tuple containing:
            - The intersections `GeoDataFrame` (possibly with reset index)
            - The input `GeoDataFrame` with the new output_column added
              (filtered if threshold_distance was specified)

    Notes:
        Unlike some other spatial joins which use GeoPandas' sjoin_nearest,
        this method uses OSMnx's optimised nearest_nodes function which
        is specifically designed for network analysis.
    """
    dataframe = data.copy()

    result = ox.distance.nearest_nodes(
        self.network.graph,
        X=dataframe[longitude_column].values,
        Y=dataframe[latitude_column].values,
        return_dist=threshold_distance is not None,
    )
    if threshold_distance:
        nearest_nodes, distances = result
        mask = np.array(distances) <= threshold_distance
        dataframe = dataframe[mask]
        nearest_nodes = nearest_nodes[mask]
    else:
        nearest_nodes = result

    edge_to_idx = {k: i for i, k in enumerate(self.layer.index)}
    nearest_indices = [edge_to_idx[edge] for edge in nearest_nodes]

    dataframe[output_column] = nearest_indices
    if _reset_layer_index:
        self.layer = self.layer.reset_index()
    return self.layer, dataframe

get_layer()

Get the GeoDataFrame of the layer.

This method returns the GeoDataFrame containing the loaded street intersections.

Returns:

Type Description
GeoDataFrame

gpd.GeoDataFrame: The GeoDataFrame containing the street intersections.

Source code in src/urban_mapper/modules/urban_layer/urban_layers/osmnx_intersections.py
@require_attributes_not_none(
    "layer", error_msg="Layer not built. Call from_place() first."
)
def get_layer(self) -> gpd.GeoDataFrame:
    """Get the `GeoDataFrame` of the layer.

    This method returns the `GeoDataFrame` containing the loaded street intersections.

    Returns:
        gpd.GeoDataFrame: The `GeoDataFrame` containing the street intersections.
    """
    return self.layer

get_layer_bounding_box()

Get the bounding box of the layer.

This method returns the bounding box of the loaded street intersections layer.

Returns:

Type Description
Tuple[float, float, float, float]

Tuple[float, float, float, float]: The bounding box of the layer in the format (minx, miny, maxx, maxy).

Source code in src/urban_mapper/modules/urban_layer/urban_layers/osmnx_intersections.py
@require_attributes_not_none(
    "layer", error_msg="Layer not built. Call from_place() first."
)
def get_layer_bounding_box(self) -> Tuple[float, float, float, float]:
    """Get the bounding box of the layer.

    This method returns the bounding box of the loaded street intersections layer.

    Returns:
        Tuple[float, float, float, float]: The bounding box of the layer in the
            format (`minx`, `miny`, `maxx`, `maxy`).
    """
    return tuple(self.layer.total_bounds)  # type: ignore

static_render(**plot_kwargs)

Render the street intersections on a static map.

This method uses OSMnx to plot the street intersections on a static map. It can be used to visualise the intersections in the context of the surrounding street network.

Parameters:

Name Type Description Default
**plot_kwargs

Additional parameters for the OSMnx plot function. Common parameters include:

  • node_size: Size of the nodes (intersections) in the plot.
  • node_color: Color of the nodes (intersections) in the plot.

More can be explored in OSMnx's documentation at https://osmnx.readthedocs.io/en/stable/.

{}
Source code in src/urban_mapper/modules/urban_layer/urban_layers/osmnx_intersections.py
@require_attributes_not_none(
    "network", error_msg="No network loaded yet. Try from_place() first!"
)
def static_render(self, **plot_kwargs) -> None:
    """Render the `street intersections` on a static map.

    This method uses `OSMnx` to plot the street intersections on a static map.
    It can be used to visualise the intersections in the context of the
    surrounding street network.

    Args:
        **plot_kwargs: Additional parameters for the `OSMnx` plot function.
            Common parameters include:

            - [x] node_size: Size of the nodes (intersections) in the plot.
            - [x] node_color: Color of the nodes (intersections) in the plot.

            More can be explored in OSMnx's documentation at [https://osmnx.readthedocs.io/en/stable/](https://osmnx.readthedocs.io/en/stable/).
    """
    ox.plot_graph(self.network.graph, show=True, close=False, **plot_kwargs)

preview(format='ascii')

Preview the OSMNXIntersections layer.

This method provides a summary of the OSMNXIntersections layer, including the coordinate reference system and mappings. It can return the preview in either ASCII or JSON format.

Parameters:

Name Type Description Default
format str

The format of the preview. Can be "ascii" or "json".

  • "ascii": Returns a human-readable string preview.
  • "json": Returns a JSON object with the layer details.
'ascii'

Returns:

Type Description
Any

str | dict: The preview of the OSMNXIntersections layer in the specified format.

Raises:

Type Description
ValueError

If the specified format is not supported.

Source code in src/urban_mapper/modules/urban_layer/urban_layers/osmnx_intersections.py
def preview(self, format: str = "ascii") -> Any:
    """Preview the `OSMNXIntersections` layer.

    This method provides a summary of the `OSMNXIntersections` layer,
    including the coordinate reference system and mappings.
    It can return the preview in either ASCII or JSON format.

    Args:
        format: The format of the preview. Can be "ascii" or "json".

            - [x] "ascii": Returns a human-readable string preview.
            - [x] "json": Returns a JSON object with the layer details.

    Returns:
        str | dict: The preview of the `OSMNXIntersections` layer in the specified format.

    Raises:
        ValueError: If the specified format is not supported.
    """
    mappings_str = (
        "\n".join(
            "Mapping:\n"
            f"    - lon={m.get('longitude_column', 'N/A')}, "
            f"lat={m.get('latitude_column', 'N/A')}, "
            f"output={m.get('output_column', 'N/A')}"
            for m in self.mappings
        )
        if self.mappings
        else "    No mappings"
    )
    if format == "ascii":
        return (
            f"Urban Layer: OSMNXIntersections\n"
            f"  CRS: {self.coordinate_reference_system}\n"
            f"  Mappings:\n{mappings_str}"
        )
    elif format == "json":
        return {
            "urban_layer": "OSMNXIntersections",
            "coordinate_reference_system": self.coordinate_reference_system,
            "mappings": self.mappings,
        }
    else:
        raise ValueError(f"Unsupported format '{format}'")

OSMFeatures

Bases: UrbanLayerBase

Urban layer implementation for arbitrary OpenStreetMap features.

This class provides methods for loading various types of OpenStreetMap features into UrbanMapper, based on user-specified tags. It handles the details of querying OSM data using different spatial contexts (place names, addresses, bounding boxes, etc.) and converting the results to GeoDataFrames.

The class is designed to be both usable directly and subclassed for more specific feature types. It implements the UrbanLayerBase interface, making it compatible with other UrbanMapper components.

When to use?

OSM Features are useful for:

  • Looking to do analysis on specific features in OpenStreetMap.
  • Wanting to load features based on tags (e.g., amenity, building, etc.).

Attributes:

Name Type Description
feature_network AdminFeatures | None

The underlying AdminFeatures object used to fetch OSM data.

tags Dict[str, str] | None

Dictionary of OpenStreetMap tags used to filter features.

layer GeoDataFrame | None

The GeoDataFrame containing the loaded OSM features (set after loading).

Examples:

>>> from urban_mapper import UrbanMapper
>>> mapper = UrbanMapper()
>>>
>>> # Load all restaurants in Manhattan
>>> restaurants = mapper.urban_layer.osm_features().from_place(
...     "Manhattan, New York",
...     tags={"amenity": "restaurant"}
... )
>>>
>>> # Load parks within a bounding box
>>> parks = mapper.urban_layer.osm_features().from_bbox(
...     (-74.01, 40.70, -73.97, 40.75),  # NYC area
...     tags={"leisure": "park"}
... )
Source code in src/urban_mapper/modules/urban_layer/urban_layers/osm_features.py
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
@beartype
class OSMFeatures(UrbanLayerBase):
    """`Urban layer` implementation for arbitrary `OpenStreetMap features`.

    This class provides methods for loading various types of `OpenStreetMap features`
    into `UrbanMapper`, based on user-specified `tags`. It handles the details of
    querying `OSM data` using different spatial contexts (`place names`, `addresses`,
    `bounding boxes`, etc.) and converting the results to `GeoDataFrames`.

    The class is designed to be both usable directly and subclassed for more
    specific feature types. It implements the `UrbanLayerBase interface`, making
    it compatible with other `UrbanMapper` components.

    !!! tip "When to use?"
        OSM Features are useful for:

        - [x] Looking to do analysis on specific features in `OpenStreetMap`.
        - [x] Wanting to load features based on `tags` (e.g., `amenity`, `building`, etc.).

    Attributes:
        feature_network: The underlying `AdminFeatures` object used to fetch OSM data.
        tags: Dictionary of OpenStreetMap `tags` used to filter features.
        layer: The `GeoDataFrame` containing the loaded OSM features (set after loading).

    Examples:
        >>> from urban_mapper import UrbanMapper
        >>> mapper = UrbanMapper()
        >>>
        >>> # Load all restaurants in Manhattan
        >>> restaurants = mapper.urban_layer.osm_features().from_place(
        ...     "Manhattan, New York",
        ...     tags={"amenity": "restaurant"}
        ... )
        >>>
        >>> # Load parks within a bounding box
        >>> parks = mapper.urban_layer.osm_features().from_bbox(
        ...     (-74.01, 40.70, -73.97, 40.75),  # NYC area
        ...     tags={"leisure": "park"}
        ... )
    """

    def __init__(self) -> None:
        super().__init__()
        self.feature_network: AdminFeatures | None = None
        self.tags: Dict[str, str] | None = None

    def from_place(
        self, place_name: str, tags: Dict[str, str | bool | dict | list], **kwargs
    ) -> None:
        """Load `OpenStreetMap features` for a named place.

        This method retrieves OSM features matching the specified tags for a
        given place name. The place name is geocoded to determine the appropriate
        area to query.

        Args:
            place_name: Name of the place to query (e.g., `Manhattan, New York`).
            tags: Dictionary of OSM tags to filter features. Examples:

                - [x] {"amenity": "restaurant"} - All restaurants
                - [x] {"building": True} - All buildings
                - [x] {"leisure": ["park", "garden"]} - Parks and gardens
                - [x] {"natural": "water"} - Water bodies

                See more in the OSM documentation, [here](https://wiki.openstreetmap.org/wiki/Map_Features)
                as well as in the `OSMnx` documentation, [here](https://osmnx.readthedocs.io/en/stable/osmnx.html#osmnx.features_from_place).
            **kwargs: Additional parameters passed to OSMnx's features_from_place.

        Returns:
            Self, for method chaining.

        Examples:
            >>> # Load all hospitals in Chicago
            >>> hospitals = OSMFeatures().from_place(
            ...     "Chicago, Illinois",
            ...     tags={"amenity": "hospital"}
            ... )
            >>>
            >>> # Load multiple amenity types with a list
            >>> poi = OSMFeatures().from_place(
            ...     "Paris, France",
            ...     tags={"tourism": ["hotel", "museum", "attraction"]}
            ... )
        """
        self.tags = tags
        self.feature_network = AdminFeatures()
        self.feature_network.load("place", tags, query=place_name, **kwargs)
        self.layer = self.feature_network.features.to_crs(
            self.coordinate_reference_system
        )

    def from_address(
        self,
        address: str,
        tags: Dict[str, str | bool | dict | list],
        dist: float,
        **kwargs,
    ) -> None:
        """Load `OpenStreetMap features` for a specific address.

        This method retrieves `OSM` features matching the specified tags for a
        given address. The address is geocoded to determine the appropriate
        area to query.

        Args:
            address: Address to query (e.g., `1600 Amphitheatre Parkway, Mountain View, CA`).
            tags: Dictionary of OSM tags to filter features. Examples:

                - [x] {"amenity": "restaurant"} - All restaurants
                - [x] {"building": True} - All buildings
                - [x] {"leisure": ["park", "garden"]} - Parks and gardens
                - [x] {"natural": "water"} - Water bodies

                See more in the OSM documentation, [here](https://wiki.openstreetmap.org/wiki/Map_Features)
                as well as in the `OSMnx` documentation, [here](https://osmnx.readthedocs.io/en/stable/osmnx.html#osmnx.features_from_address).

            dist: Distance in meters to search around the address.
            **kwargs: Additional parameters passed to OSMnx's features_from_address.

        Returns:
            Self, for method chaining.

        Examples:
            >>> # Load all restaurants within 500 meters of a specific address
            >>> restaurants = OSMFeatures().from_address(
            ...     "1600 Amphitheatre Parkway, Mountain View, CA",
            ...     tags={"amenity": "restaurant"},
            ...     dist=500
            ... )
            >>>
            # Load all parks within 1 km of a specific address
            >>> parks = OSMFeatures().from_address(
            ...     "Central Park, New York, NY",
            ...     tags={"leisure": "park"},
            ...     dist=1000
            ... )
        """

        self.tags = tags
        self.feature_network = AdminFeatures()
        self.feature_network.load("address", tags, address=address, dist=dist, **kwargs)
        self.layer = self.feature_network.features.to_crs(
            self.coordinate_reference_system
        )

    def from_bbox(
        self,
        bbox: Tuple[float, float, float, float],
        tags: Dict[str, str | bool | dict | list],
        **kwargs,
    ) -> None:
        """Load `OpenStreetMap features` for a specific bounding box.

        This method retrieves OSM features matching the specified tags for a
        given bounding box. The bounding box is defined by its coordinates.

        Args:
            bbox: Bounding box coordinates in the format (min_lon, min_lat, max_lon, max_lat).
            tags: Dictionary of OSM tags to filter features. Examples:

                - [x] {"amenity": "restaurant"} - All restaurants
                - [x] {"building": True} - All buildings
                - [x] {"leisure": ["park", "garden"]} - Parks and gardens
                - [x] {"natural": "water"} - Water bodies

                See more in the OSM documentation, [here](https://wiki.openstreetmap.org/wiki/Map_Features)
                as well as in the `OSMnx` documentation, [here](https://osmnx.readthedocs.io/en/stable/osmnx.html#osmnx.features_from_bbox).

            **kwargs: Additional parameters passed to OSMnx's features_from_bbox.

        Returns:
            Self, for method chaining.

        Examples:
            >>> # Load all schools within a bounding box in San Francisco
            >>> schools = OSMFeatures().from_bbox(
            ...     (-122.45, 37.75, -122.40, 37.80),
            ...     tags={"amenity": "school"}
            ... )
        """
        self.tags = tags
        self.feature_network = AdminFeatures()
        self.feature_network.load("bbox", tags, bbox=bbox, **kwargs)
        self.layer = self.feature_network.features.to_crs(
            self.coordinate_reference_system
        )

    def from_point(
        self,
        center_point: Tuple[float, float],
        tags: Dict[str, str | bool | dict | list],
        dist: float,
        **kwargs,
    ) -> None:
        """Load `OpenStreetMap features` for a specific point.

        This method retrieves OSM features matching the specified tags for a
        given point. The point is defined by its coordinates.

        Args:
            center_point: Coordinates of the point in the format (longitude, latitude).
            tags: Dictionary of OSM tags to filter features. Examples:

                - [x] {"amenity": "restaurant"} - All restaurants
                - [x] {"building": True} - All buildings
                - [x] {"leisure": ["park", "garden"]} - Parks and gardens
                - [x] {"natural": "water"} - Water bodies

                See more in the OSM documentation, [here](https://wiki.openstreetmap.org/wiki/Map_Features)
                as well as in the `OSMnx` documentation, [here](https://osmnx.readthedocs.io/en/stable/osmnx.html#osmnx.features_from_point).

            dist: Distance in meters to search around the point.
            **kwargs: Additional parameters passed to OSMnx's features_from_point.

        Returns:
            Self, for method chaining.

        Examples:
            >>> # Load all restaurants within 500 meters of a specific point
            >>> restaurants = OSMFeatures().from_point(
            ...     (37.7749, -122.4194),  # San Francisco coordinates
            ...     tags={"amenity": "restaurant"},
            ...     dist=500
            ... )
        """
        self.tags = tags
        self.feature_network = AdminFeatures()
        self.feature_network.load(
            "point", tags, center_point=center_point, dist=dist, **kwargs
        )
        self.layer = self.feature_network.features.to_crs(
            self.coordinate_reference_system
        )

    def from_polygon(
        self,
        polygon: Polygon | MultiPolygon,
        tags: Dict[str, str | bool | dict | list],
        **kwargs,
    ) -> None:
        """Load `OpenStreetMap features` for a specific polygon.

        This method retrieves OSM features matching the specified tags for a
        given polygon. The polygon is defined by its geometry.

        Args:
            polygon: Shapely Polygon or MultiPolygon object defining the area.
            tags: Dictionary of OSM tags to filter features. Examples:

                - [x] {"amenity": "restaurant"} - All restaurants
                - [x] {"building": True} - All buildings
                - [x] {"leisure": ["park", "garden"]} - Parks and gardens
                - [x] {"natural": "water"} - Water bodies

                See more in the OSM documentation, [here](https://wiki.openstreetmap.org/wiki/Map_Features)
                as well as in the `OSMnx` documentation, [here](https://osmnx.readthedocs.io/en/stable/osmnx.html#osmnx.features_from_polygon).

            **kwargs: Additional parameters passed to OSMnx's features_from_polygon.

        Returns:
            Self, for method chaining.

        Examples:
            >>> # Load all parks within a specific polygon
            >>> parks = OSMFeatures().from_polygon(
            ...     Polygon([(37.7749, -122.4194), (37.7849, -122.4294), (37.7949, -122.4194)]),
            ...     # πŸ‘†Can get polygon from geocoding an address/place via GeoPY.
            ...     tags={"leisure": "park"}
            ... )
        """
        self.tags = tags
        self.feature_network = AdminFeatures()
        self.feature_network.load("polygon", tags, polygon=polygon, **kwargs)
        self.layer = self.feature_network.features.to_crs(
            self.coordinate_reference_system
        )

    def from_file(self, file_path: str | Path, **kwargs) -> None:
        """Load `OpenStreetMap features` from a file.

        !!! danger "Not Implemented"
            This method is not implemented yet. It raises a `NotImplementedError`.
            You can use the other loading methods (e.g., `from_place`, `from_bbox`)
            to load OSM features.
        """
        raise NotImplementedError("Loading OSM features from file is not supported.")

    @require_attributes_not_none(
        "layer",
        error_msg="Layer not loaded. Call a loading method (e.g., from_place) first.",
    )
    def _map_nearest_layer(
        self,
        data: gpd.GeoDataFrame,
        longitude_column: str,
        latitude_column: str,
        output_column: str = "nearest_feature",
        threshold_distance: float | None = None,
        _reset_layer_index: bool = True,
        **kwargs,
    ) -> Tuple[gpd.GeoDataFrame, gpd.GeoDataFrame]:
        """Map points to their nearest `OSM features`.

        This internal method finds the nearest OSM feature for each point in
        the input `GeoDataFrame` and adds a reference to that feature as a new column.
        It's primarily used by the `UrbanLayerBase.map_nearest_layer()` method to
        implement spatial joining between your dataset point data and OSM features compoents.

        The method handles both points with explicit geometry and points defined
        by longitude/latitude columns. It also automatically converts coordinate
        systems to ensure accurate distance calculations.

        Args:
            data: `GeoDataFrame` containing point data to map.
            longitude_column: Name of the column containing longitude values.
            latitude_column: Name of the column containing latitude values.
            output_column: Name of the column to store the indices of nearest features.
            threshold_distance: Maximum distance to consider a match, in the CRS units.
            _reset_layer_index: Whether to reset the index of the layer GeoDataFrame.
            **kwargs: Additional parameters (not used).

        Returns:
            A tuple containing:

                - The OSM features GeoDataFrame (possibly with reset index)
                - The input `GeoDataFrame` with the new output_column added
                  (filtered if threshold_distance was specified)

        !!! note "To Keep in Mind"

            - [x] The method preferentially uses `OSM IDs` when available, otherwise
              falls back to `DataFrame indices`.
            - [x] The method converts to a projected `CRS` for accurate distance calculations.
        """
        dataframe = data.copy()
        if "geometry" not in dataframe.columns:
            dataframe = gpd.GeoDataFrame(
                dataframe,
                geometry=gpd.points_from_xy(
                    dataframe[longitude_column], dataframe[latitude_column]
                ),
                crs=self.coordinate_reference_system,
            )
        if not dataframe.crs.is_projected:
            utm_crs = dataframe.estimate_utm_crs()
            dataframe = dataframe.to_crs(utm_crs)
            layer_projected = self.layer.to_crs(utm_crs)
        else:
            layer_projected = self.layer

        features_reset = layer_projected.reset_index()
        unique_id = "osmid" if "osmid" in features_reset.columns else "index"

        mapped_data = gpd.sjoin_nearest(
            dataframe,
            features_reset[["geometry", unique_id]],
            how="left",
            max_distance=threshold_distance,
            distance_col="distance_to_feature",
        )
        mapped_data[output_column] = mapped_data[unique_id]
        return self.layer, mapped_data.drop(
            columns=[unique_id, "distance_to_feature", "index_right"],
            errors="ignore",
        )

    @require_attributes_not_none(
        "layer",
        error_msg="Layer not built. Call a loading method (e.g., from_place) first.",
    )
    def get_layer(self) -> gpd.GeoDataFrame:
        """Get the loaded `OSM features` layer.

        This method returns the `GeoDataFrame` containing the loaded OSM features.
        It's primarily used for accessing the layer after loading it using
        methods like `from_place`, `from_bbox`, etc.

        Returns:
            The `GeoDataFrame` containing the loaded OSM features.
        """
        return self.layer

    @require_attributes_not_none(
        "layer",
        error_msg="Layer not built. Call a loading method (e.g., from_place) first.",
    )
    def get_layer_bounding_box(self) -> Tuple[float, float, float, float]:
        """Get the bounding box of the loaded `OSM features` layer.

        This method returns the bounding box coordinates of the loaded OSM features
        in the format (`min_lon`, `min_lat`, `max_lon`, `max_lat`). It's useful for
        understanding the spatial extent of the layer.

        Returns:
            A tuple containing the bounding box coordinates in the format
            (`min_lon`, `min_lat`, `max_lon`, `max_lat`).
        """
        return tuple(self.layer.total_bounds)  # type: ignore

    @require_attributes_not_none(
        "layer",
        error_msg="Layer not built. Call a loading method (e.g., from_place) first.",
    )
    def static_render(self, **plot_kwargs) -> None:
        """Render the loaded `OSM features` layer.

        This method visualises the loaded OSM features using the specified
        plotting parameters. It uses the `plot()` method of the `GeoDataFrame`
        to create a static plot.

        Args:
            **plot_kwargs: Additional parameters passed to the `GeoDataFrame.plot()` method.

        Returns:
            None: The method does not return anything. It directly displays the plot.
        """
        self.layer.plot(**plot_kwargs)

    def preview(self, format: str = "ascii") -> Any:
        """Preview the loaded `OSM features` layer.

        This method provides a summary of the loaded `OSM features`, including
        the `tags` used for filtering, the `coordinate reference system` (CRS),
        and the `mappings` between the input data and the OSM features.

        Args:
            format: Format of the preview output. Options are "ascii" or "json".
                - [x] "ascii" - Plain text summary.
                - [x] "json" - JSON representation of the summary.

        Returns:
            A string or dictionary containing the preview information.

        Raises:
            ValueError: If an unsupported format is specified.
        """
        mappings_str = (
            "\n".join(
                "Mapping:\n"
                f"    - lon={m.get('longitude_column', 'N/A')}, "
                f"lat={m.get('latitude_column', 'N/A')}, "
                f"output={m.get('output_column', 'N/A')}"
                for m in self.mappings
            )
            if self.mappings
            else "    No mappings"
        )
        if format == "ascii":
            return (
                f"Urban Layer: OSMFeatures\n"
                f"  Focussing tags: {self.tags}\n"
                f"  CRS: {self.coordinate_reference_system}\n"
                f"  Mappings:\n{mappings_str}"
            )
        elif format == "json":
            return {
                "urban_layer": "OSMFeatures",
                "tags": self.tags,
                "coordinate_reference_system": self.coordinate_reference_system,
                "mappings": self.mappings,
            }
        else:
            raise ValueError(f"Unsupported format '{format}'")

from_place(place_name, tags, **kwargs)

Load OpenStreetMap features for a named place.

This method retrieves OSM features matching the specified tags for a given place name. The place name is geocoded to determine the appropriate area to query.

Parameters:

Name Type Description Default
place_name str

Name of the place to query (e.g., Manhattan, New York).

required
tags Dict[str, str | bool | dict | list]

Dictionary of OSM tags to filter features. Examples:

  • {"amenity": "restaurant"} - All restaurants
  • {"building": True} - All buildings
  • {"leisure": ["park", "garden"]} - Parks and gardens
  • {"natural": "water"} - Water bodies

See more in the OSM documentation, here as well as in the OSMnx documentation, here.

required
**kwargs

Additional parameters passed to OSMnx's features_from_place.

{}

Returns:

Type Description
None

Self, for method chaining.

Examples:

>>> # Load all hospitals in Chicago
>>> hospitals = OSMFeatures().from_place(
...     "Chicago, Illinois",
...     tags={"amenity": "hospital"}
... )
>>>
>>> # Load multiple amenity types with a list
>>> poi = OSMFeatures().from_place(
...     "Paris, France",
...     tags={"tourism": ["hotel", "museum", "attraction"]}
... )
Source code in src/urban_mapper/modules/urban_layer/urban_layers/osm_features.py
def from_place(
    self, place_name: str, tags: Dict[str, str | bool | dict | list], **kwargs
) -> None:
    """Load `OpenStreetMap features` for a named place.

    This method retrieves OSM features matching the specified tags for a
    given place name. The place name is geocoded to determine the appropriate
    area to query.

    Args:
        place_name: Name of the place to query (e.g., `Manhattan, New York`).
        tags: Dictionary of OSM tags to filter features. Examples:

            - [x] {"amenity": "restaurant"} - All restaurants
            - [x] {"building": True} - All buildings
            - [x] {"leisure": ["park", "garden"]} - Parks and gardens
            - [x] {"natural": "water"} - Water bodies

            See more in the OSM documentation, [here](https://wiki.openstreetmap.org/wiki/Map_Features)
            as well as in the `OSMnx` documentation, [here](https://osmnx.readthedocs.io/en/stable/osmnx.html#osmnx.features_from_place).
        **kwargs: Additional parameters passed to OSMnx's features_from_place.

    Returns:
        Self, for method chaining.

    Examples:
        >>> # Load all hospitals in Chicago
        >>> hospitals = OSMFeatures().from_place(
        ...     "Chicago, Illinois",
        ...     tags={"amenity": "hospital"}
        ... )
        >>>
        >>> # Load multiple amenity types with a list
        >>> poi = OSMFeatures().from_place(
        ...     "Paris, France",
        ...     tags={"tourism": ["hotel", "museum", "attraction"]}
        ... )
    """
    self.tags = tags
    self.feature_network = AdminFeatures()
    self.feature_network.load("place", tags, query=place_name, **kwargs)
    self.layer = self.feature_network.features.to_crs(
        self.coordinate_reference_system
    )

from_address(address, tags, dist, **kwargs)

Load OpenStreetMap features for a specific address.

This method retrieves OSM features matching the specified tags for a given address. The address is geocoded to determine the appropriate area to query.

Parameters:

Name Type Description Default
address str

Address to query (e.g., 1600 Amphitheatre Parkway, Mountain View, CA).

required
tags Dict[str, str | bool | dict | list]

Dictionary of OSM tags to filter features. Examples:

  • {"amenity": "restaurant"} - All restaurants
  • {"building": True} - All buildings
  • {"leisure": ["park", "garden"]} - Parks and gardens
  • {"natural": "water"} - Water bodies

See more in the OSM documentation, here as well as in the OSMnx documentation, here.

required
dist float

Distance in meters to search around the address.

required
**kwargs

Additional parameters passed to OSMnx's features_from_address.

{}

Returns:

Type Description
None

Self, for method chaining.

Examples:

>>> # Load all restaurants within 500 meters of a specific address
>>> restaurants = OSMFeatures().from_address(
...     "1600 Amphitheatre Parkway, Mountain View, CA",
...     tags={"amenity": "restaurant"},
...     dist=500
... )
>>>
# Load all parks within 1 km of a specific address
>>> parks = OSMFeatures().from_address(
...     "Central Park, New York, NY",
...     tags={"leisure": "park"},
...     dist=1000
... )
Source code in src/urban_mapper/modules/urban_layer/urban_layers/osm_features.py
def from_address(
    self,
    address: str,
    tags: Dict[str, str | bool | dict | list],
    dist: float,
    **kwargs,
) -> None:
    """Load `OpenStreetMap features` for a specific address.

    This method retrieves `OSM` features matching the specified tags for a
    given address. The address is geocoded to determine the appropriate
    area to query.

    Args:
        address: Address to query (e.g., `1600 Amphitheatre Parkway, Mountain View, CA`).
        tags: Dictionary of OSM tags to filter features. Examples:

            - [x] {"amenity": "restaurant"} - All restaurants
            - [x] {"building": True} - All buildings
            - [x] {"leisure": ["park", "garden"]} - Parks and gardens
            - [x] {"natural": "water"} - Water bodies

            See more in the OSM documentation, [here](https://wiki.openstreetmap.org/wiki/Map_Features)
            as well as in the `OSMnx` documentation, [here](https://osmnx.readthedocs.io/en/stable/osmnx.html#osmnx.features_from_address).

        dist: Distance in meters to search around the address.
        **kwargs: Additional parameters passed to OSMnx's features_from_address.

    Returns:
        Self, for method chaining.

    Examples:
        >>> # Load all restaurants within 500 meters of a specific address
        >>> restaurants = OSMFeatures().from_address(
        ...     "1600 Amphitheatre Parkway, Mountain View, CA",
        ...     tags={"amenity": "restaurant"},
        ...     dist=500
        ... )
        >>>
        # Load all parks within 1 km of a specific address
        >>> parks = OSMFeatures().from_address(
        ...     "Central Park, New York, NY",
        ...     tags={"leisure": "park"},
        ...     dist=1000
        ... )
    """

    self.tags = tags
    self.feature_network = AdminFeatures()
    self.feature_network.load("address", tags, address=address, dist=dist, **kwargs)
    self.layer = self.feature_network.features.to_crs(
        self.coordinate_reference_system
    )

from_bbox(bbox, tags, **kwargs)

Load OpenStreetMap features for a specific bounding box.

This method retrieves OSM features matching the specified tags for a given bounding box. The bounding box is defined by its coordinates.

Parameters:

Name Type Description Default
bbox Tuple[float, float, float, float]

Bounding box coordinates in the format (min_lon, min_lat, max_lon, max_lat).

required
tags Dict[str, str | bool | dict | list]

Dictionary of OSM tags to filter features. Examples:

  • {"amenity": "restaurant"} - All restaurants
  • {"building": True} - All buildings
  • {"leisure": ["park", "garden"]} - Parks and gardens
  • {"natural": "water"} - Water bodies

See more in the OSM documentation, here as well as in the OSMnx documentation, here.

required
**kwargs

Additional parameters passed to OSMnx's features_from_bbox.

{}

Returns:

Type Description
None

Self, for method chaining.

Examples:

>>> # Load all schools within a bounding box in San Francisco
>>> schools = OSMFeatures().from_bbox(
...     (-122.45, 37.75, -122.40, 37.80),
...     tags={"amenity": "school"}
... )
Source code in src/urban_mapper/modules/urban_layer/urban_layers/osm_features.py
def from_bbox(
    self,
    bbox: Tuple[float, float, float, float],
    tags: Dict[str, str | bool | dict | list],
    **kwargs,
) -> None:
    """Load `OpenStreetMap features` for a specific bounding box.

    This method retrieves OSM features matching the specified tags for a
    given bounding box. The bounding box is defined by its coordinates.

    Args:
        bbox: Bounding box coordinates in the format (min_lon, min_lat, max_lon, max_lat).
        tags: Dictionary of OSM tags to filter features. Examples:

            - [x] {"amenity": "restaurant"} - All restaurants
            - [x] {"building": True} - All buildings
            - [x] {"leisure": ["park", "garden"]} - Parks and gardens
            - [x] {"natural": "water"} - Water bodies

            See more in the OSM documentation, [here](https://wiki.openstreetmap.org/wiki/Map_Features)
            as well as in the `OSMnx` documentation, [here](https://osmnx.readthedocs.io/en/stable/osmnx.html#osmnx.features_from_bbox).

        **kwargs: Additional parameters passed to OSMnx's features_from_bbox.

    Returns:
        Self, for method chaining.

    Examples:
        >>> # Load all schools within a bounding box in San Francisco
        >>> schools = OSMFeatures().from_bbox(
        ...     (-122.45, 37.75, -122.40, 37.80),
        ...     tags={"amenity": "school"}
        ... )
    """
    self.tags = tags
    self.feature_network = AdminFeatures()
    self.feature_network.load("bbox", tags, bbox=bbox, **kwargs)
    self.layer = self.feature_network.features.to_crs(
        self.coordinate_reference_system
    )

from_point(center_point, tags, dist, **kwargs)

Load OpenStreetMap features for a specific point.

This method retrieves OSM features matching the specified tags for a given point. The point is defined by its coordinates.

Parameters:

Name Type Description Default
center_point Tuple[float, float]

Coordinates of the point in the format (longitude, latitude).

required
tags Dict[str, str | bool | dict | list]

Dictionary of OSM tags to filter features. Examples:

  • {"amenity": "restaurant"} - All restaurants
  • {"building": True} - All buildings
  • {"leisure": ["park", "garden"]} - Parks and gardens
  • {"natural": "water"} - Water bodies

See more in the OSM documentation, here as well as in the OSMnx documentation, here.

required
dist float

Distance in meters to search around the point.

required
**kwargs

Additional parameters passed to OSMnx's features_from_point.

{}

Returns:

Type Description
None

Self, for method chaining.

Examples:

>>> # Load all restaurants within 500 meters of a specific point
>>> restaurants = OSMFeatures().from_point(
...     (37.7749, -122.4194),  # San Francisco coordinates
...     tags={"amenity": "restaurant"},
...     dist=500
... )
Source code in src/urban_mapper/modules/urban_layer/urban_layers/osm_features.py
def from_point(
    self,
    center_point: Tuple[float, float],
    tags: Dict[str, str | bool | dict | list],
    dist: float,
    **kwargs,
) -> None:
    """Load `OpenStreetMap features` for a specific point.

    This method retrieves OSM features matching the specified tags for a
    given point. The point is defined by its coordinates.

    Args:
        center_point: Coordinates of the point in the format (longitude, latitude).
        tags: Dictionary of OSM tags to filter features. Examples:

            - [x] {"amenity": "restaurant"} - All restaurants
            - [x] {"building": True} - All buildings
            - [x] {"leisure": ["park", "garden"]} - Parks and gardens
            - [x] {"natural": "water"} - Water bodies

            See more in the OSM documentation, [here](https://wiki.openstreetmap.org/wiki/Map_Features)
            as well as in the `OSMnx` documentation, [here](https://osmnx.readthedocs.io/en/stable/osmnx.html#osmnx.features_from_point).

        dist: Distance in meters to search around the point.
        **kwargs: Additional parameters passed to OSMnx's features_from_point.

    Returns:
        Self, for method chaining.

    Examples:
        >>> # Load all restaurants within 500 meters of a specific point
        >>> restaurants = OSMFeatures().from_point(
        ...     (37.7749, -122.4194),  # San Francisco coordinates
        ...     tags={"amenity": "restaurant"},
        ...     dist=500
        ... )
    """
    self.tags = tags
    self.feature_network = AdminFeatures()
    self.feature_network.load(
        "point", tags, center_point=center_point, dist=dist, **kwargs
    )
    self.layer = self.feature_network.features.to_crs(
        self.coordinate_reference_system
    )

from_polygon(polygon, tags, **kwargs)

Load OpenStreetMap features for a specific polygon.

This method retrieves OSM features matching the specified tags for a given polygon. The polygon is defined by its geometry.

Parameters:

Name Type Description Default
polygon Polygon | MultiPolygon

Shapely Polygon or MultiPolygon object defining the area.

required
tags Dict[str, str | bool | dict | list]

Dictionary of OSM tags to filter features. Examples:

  • {"amenity": "restaurant"} - All restaurants
  • {"building": True} - All buildings
  • {"leisure": ["park", "garden"]} - Parks and gardens
  • {"natural": "water"} - Water bodies

See more in the OSM documentation, here as well as in the OSMnx documentation, here.

required
**kwargs

Additional parameters passed to OSMnx's features_from_polygon.

{}

Returns:

Type Description
None

Self, for method chaining.

Examples:

>>> # Load all parks within a specific polygon
>>> parks = OSMFeatures().from_polygon(
...     Polygon([(37.7749, -122.4194), (37.7849, -122.4294), (37.7949, -122.4194)]),
...     # πŸ‘†Can get polygon from geocoding an address/place via GeoPY.
...     tags={"leisure": "park"}
... )
Source code in src/urban_mapper/modules/urban_layer/urban_layers/osm_features.py
def from_polygon(
    self,
    polygon: Polygon | MultiPolygon,
    tags: Dict[str, str | bool | dict | list],
    **kwargs,
) -> None:
    """Load `OpenStreetMap features` for a specific polygon.

    This method retrieves OSM features matching the specified tags for a
    given polygon. The polygon is defined by its geometry.

    Args:
        polygon: Shapely Polygon or MultiPolygon object defining the area.
        tags: Dictionary of OSM tags to filter features. Examples:

            - [x] {"amenity": "restaurant"} - All restaurants
            - [x] {"building": True} - All buildings
            - [x] {"leisure": ["park", "garden"]} - Parks and gardens
            - [x] {"natural": "water"} - Water bodies

            See more in the OSM documentation, [here](https://wiki.openstreetmap.org/wiki/Map_Features)
            as well as in the `OSMnx` documentation, [here](https://osmnx.readthedocs.io/en/stable/osmnx.html#osmnx.features_from_polygon).

        **kwargs: Additional parameters passed to OSMnx's features_from_polygon.

    Returns:
        Self, for method chaining.

    Examples:
        >>> # Load all parks within a specific polygon
        >>> parks = OSMFeatures().from_polygon(
        ...     Polygon([(37.7749, -122.4194), (37.7849, -122.4294), (37.7949, -122.4194)]),
        ...     # πŸ‘†Can get polygon from geocoding an address/place via GeoPY.
        ...     tags={"leisure": "park"}
        ... )
    """
    self.tags = tags
    self.feature_network = AdminFeatures()
    self.feature_network.load("polygon", tags, polygon=polygon, **kwargs)
    self.layer = self.feature_network.features.to_crs(
        self.coordinate_reference_system
    )

from_file(file_path, **kwargs)

Load OpenStreetMap features from a file.

Not Implemented

This method is not implemented yet. It raises a NotImplementedError. You can use the other loading methods (e.g., from_place, from_bbox) to load OSM features.

Source code in src/urban_mapper/modules/urban_layer/urban_layers/osm_features.py
def from_file(self, file_path: str | Path, **kwargs) -> None:
    """Load `OpenStreetMap features` from a file.

    !!! danger "Not Implemented"
        This method is not implemented yet. It raises a `NotImplementedError`.
        You can use the other loading methods (e.g., `from_place`, `from_bbox`)
        to load OSM features.
    """
    raise NotImplementedError("Loading OSM features from file is not supported.")

_map_nearest_layer(data, longitude_column, latitude_column, output_column='nearest_feature', threshold_distance=None, _reset_layer_index=True, **kwargs)

Map points to their nearest OSM features.

This internal method finds the nearest OSM feature for each point in the input GeoDataFrame and adds a reference to that feature as a new column. It's primarily used by the UrbanLayerBase.map_nearest_layer() method to implement spatial joining between your dataset point data and OSM features compoents.

The method handles both points with explicit geometry and points defined by longitude/latitude columns. It also automatically converts coordinate systems to ensure accurate distance calculations.

Parameters:

Name Type Description Default
data GeoDataFrame

GeoDataFrame containing point data to map.

required
longitude_column str

Name of the column containing longitude values.

required
latitude_column str

Name of the column containing latitude values.

required
output_column str

Name of the column to store the indices of nearest features.

'nearest_feature'
threshold_distance float | None

Maximum distance to consider a match, in the CRS units.

None
_reset_layer_index bool

Whether to reset the index of the layer GeoDataFrame.

True
**kwargs

Additional parameters (not used).

{}

Returns:

Type Description
Tuple[GeoDataFrame, GeoDataFrame]

A tuple containing:

  • The OSM features GeoDataFrame (possibly with reset index)
  • The input GeoDataFrame with the new output_column added (filtered if threshold_distance was specified)

To Keep in Mind

  • The method preferentially uses OSM IDs when available, otherwise falls back to DataFrame indices.
  • The method converts to a projected CRS for accurate distance calculations.
Source code in src/urban_mapper/modules/urban_layer/urban_layers/osm_features.py
@require_attributes_not_none(
    "layer",
    error_msg="Layer not loaded. Call a loading method (e.g., from_place) first.",
)
def _map_nearest_layer(
    self,
    data: gpd.GeoDataFrame,
    longitude_column: str,
    latitude_column: str,
    output_column: str = "nearest_feature",
    threshold_distance: float | None = None,
    _reset_layer_index: bool = True,
    **kwargs,
) -> Tuple[gpd.GeoDataFrame, gpd.GeoDataFrame]:
    """Map points to their nearest `OSM features`.

    This internal method finds the nearest OSM feature for each point in
    the input `GeoDataFrame` and adds a reference to that feature as a new column.
    It's primarily used by the `UrbanLayerBase.map_nearest_layer()` method to
    implement spatial joining between your dataset point data and OSM features compoents.

    The method handles both points with explicit geometry and points defined
    by longitude/latitude columns. It also automatically converts coordinate
    systems to ensure accurate distance calculations.

    Args:
        data: `GeoDataFrame` containing point data to map.
        longitude_column: Name of the column containing longitude values.
        latitude_column: Name of the column containing latitude values.
        output_column: Name of the column to store the indices of nearest features.
        threshold_distance: Maximum distance to consider a match, in the CRS units.
        _reset_layer_index: Whether to reset the index of the layer GeoDataFrame.
        **kwargs: Additional parameters (not used).

    Returns:
        A tuple containing:

            - The OSM features GeoDataFrame (possibly with reset index)
            - The input `GeoDataFrame` with the new output_column added
              (filtered if threshold_distance was specified)

    !!! note "To Keep in Mind"

        - [x] The method preferentially uses `OSM IDs` when available, otherwise
          falls back to `DataFrame indices`.
        - [x] The method converts to a projected `CRS` for accurate distance calculations.
    """
    dataframe = data.copy()
    if "geometry" not in dataframe.columns:
        dataframe = gpd.GeoDataFrame(
            dataframe,
            geometry=gpd.points_from_xy(
                dataframe[longitude_column], dataframe[latitude_column]
            ),
            crs=self.coordinate_reference_system,
        )
    if not dataframe.crs.is_projected:
        utm_crs = dataframe.estimate_utm_crs()
        dataframe = dataframe.to_crs(utm_crs)
        layer_projected = self.layer.to_crs(utm_crs)
    else:
        layer_projected = self.layer

    features_reset = layer_projected.reset_index()
    unique_id = "osmid" if "osmid" in features_reset.columns else "index"

    mapped_data = gpd.sjoin_nearest(
        dataframe,
        features_reset[["geometry", unique_id]],
        how="left",
        max_distance=threshold_distance,
        distance_col="distance_to_feature",
    )
    mapped_data[output_column] = mapped_data[unique_id]
    return self.layer, mapped_data.drop(
        columns=[unique_id, "distance_to_feature", "index_right"],
        errors="ignore",
    )

get_layer()

Get the loaded OSM features layer.

This method returns the GeoDataFrame containing the loaded OSM features. It's primarily used for accessing the layer after loading it using methods like from_place, from_bbox, etc.

Returns:

Type Description
GeoDataFrame

The GeoDataFrame containing the loaded OSM features.

Source code in src/urban_mapper/modules/urban_layer/urban_layers/osm_features.py
@require_attributes_not_none(
    "layer",
    error_msg="Layer not built. Call a loading method (e.g., from_place) first.",
)
def get_layer(self) -> gpd.GeoDataFrame:
    """Get the loaded `OSM features` layer.

    This method returns the `GeoDataFrame` containing the loaded OSM features.
    It's primarily used for accessing the layer after loading it using
    methods like `from_place`, `from_bbox`, etc.

    Returns:
        The `GeoDataFrame` containing the loaded OSM features.
    """
    return self.layer

get_layer_bounding_box()

Get the bounding box of the loaded OSM features layer.

This method returns the bounding box coordinates of the loaded OSM features in the format (min_lon, min_lat, max_lon, max_lat). It's useful for understanding the spatial extent of the layer.

Returns:

Type Description
float

A tuple containing the bounding box coordinates in the format

float

(min_lon, min_lat, max_lon, max_lat).

Source code in src/urban_mapper/modules/urban_layer/urban_layers/osm_features.py
@require_attributes_not_none(
    "layer",
    error_msg="Layer not built. Call a loading method (e.g., from_place) first.",
)
def get_layer_bounding_box(self) -> Tuple[float, float, float, float]:
    """Get the bounding box of the loaded `OSM features` layer.

    This method returns the bounding box coordinates of the loaded OSM features
    in the format (`min_lon`, `min_lat`, `max_lon`, `max_lat`). It's useful for
    understanding the spatial extent of the layer.

    Returns:
        A tuple containing the bounding box coordinates in the format
        (`min_lon`, `min_lat`, `max_lon`, `max_lat`).
    """
    return tuple(self.layer.total_bounds)  # type: ignore

static_render(**plot_kwargs)

Render the loaded OSM features layer.

This method visualises the loaded OSM features using the specified plotting parameters. It uses the plot() method of the GeoDataFrame to create a static plot.

Parameters:

Name Type Description Default
**plot_kwargs

Additional parameters passed to the GeoDataFrame.plot() method.

{}

Returns:

Name Type Description
None None

The method does not return anything. It directly displays the plot.

Source code in src/urban_mapper/modules/urban_layer/urban_layers/osm_features.py
@require_attributes_not_none(
    "layer",
    error_msg="Layer not built. Call a loading method (e.g., from_place) first.",
)
def static_render(self, **plot_kwargs) -> None:
    """Render the loaded `OSM features` layer.

    This method visualises the loaded OSM features using the specified
    plotting parameters. It uses the `plot()` method of the `GeoDataFrame`
    to create a static plot.

    Args:
        **plot_kwargs: Additional parameters passed to the `GeoDataFrame.plot()` method.

    Returns:
        None: The method does not return anything. It directly displays the plot.
    """
    self.layer.plot(**plot_kwargs)

preview(format='ascii')

Preview the loaded OSM features layer.

This method provides a summary of the loaded OSM features, including the tags used for filtering, the coordinate reference system (CRS), and the mappings between the input data and the OSM features.

Parameters:

Name Type Description Default
format str

Format of the preview output. Options are "ascii" or "json". - [x] "ascii" - Plain text summary. - [x] "json" - JSON representation of the summary.

'ascii'

Returns:

Type Description
Any

A string or dictionary containing the preview information.

Raises:

Type Description
ValueError

If an unsupported format is specified.

Source code in src/urban_mapper/modules/urban_layer/urban_layers/osm_features.py
def preview(self, format: str = "ascii") -> Any:
    """Preview the loaded `OSM features` layer.

    This method provides a summary of the loaded `OSM features`, including
    the `tags` used for filtering, the `coordinate reference system` (CRS),
    and the `mappings` between the input data and the OSM features.

    Args:
        format: Format of the preview output. Options are "ascii" or "json".
            - [x] "ascii" - Plain text summary.
            - [x] "json" - JSON representation of the summary.

    Returns:
        A string or dictionary containing the preview information.

    Raises:
        ValueError: If an unsupported format is specified.
    """
    mappings_str = (
        "\n".join(
            "Mapping:\n"
            f"    - lon={m.get('longitude_column', 'N/A')}, "
            f"lat={m.get('latitude_column', 'N/A')}, "
            f"output={m.get('output_column', 'N/A')}"
            for m in self.mappings
        )
        if self.mappings
        else "    No mappings"
    )
    if format == "ascii":
        return (
            f"Urban Layer: OSMFeatures\n"
            f"  Focussing tags: {self.tags}\n"
            f"  CRS: {self.coordinate_reference_system}\n"
            f"  Mappings:\n{mappings_str}"
        )
    elif format == "json":
        return {
            "urban_layer": "OSMFeatures",
            "tags": self.tags,
            "coordinate_reference_system": self.coordinate_reference_system,
            "mappings": self.mappings,
        }
    else:
        raise ValueError(f"Unsupported format '{format}'")

Tile2NetSidewalks

Bases: UrbanLayerBase

Urban layer implementation for sidewalks extracted with Tile2Net.

This class provides access to sidewalk data extracted from aerial imagery using the Tile2Net deep learning framework. It implements the UrbanLayerBase interface, ensuring compatibility with other UrbanMapper components such as filters, enrichers, and pipelines.

When to Use?

Sidewalk data is particularly useful for:

  • Pedestrian accessibility studies
  • Walkability analysis
  • Urban planning and design
  • Mobility assessment for people with disabilities

Attributes:

Name Type Description
layer GeoDataFrame

The GeoDataFrame containing sidewalk geometries (set after loading).

Examples:

Load and visualise sidewalk data:

>>> from urban_mapper import UrbanMapper
>>> mapper = UrbanMapper()
>>> sidewalks = mapper.urban_layer.tile2net_sidewalks().from_file("path/to/sidewalks.geojson")
>>> sidewalks.static_render(figsize=(10, 8), colour="blue")

Data Source

Sidewalk data must be pre-extracted using Tile2Net and loaded from files. Direct loading from place names or other spatial queries is not supported.

See further here: Tile2Net VIDA NYU && This Feature Request

Source code in src/urban_mapper/modules/urban_layer/urban_layers/tile2net_sidewalks.py
@beartype
class Tile2NetSidewalks(UrbanLayerBase):
    """Urban layer implementation for sidewalks extracted with `Tile2Net`.

    This class provides access to sidewalk data extracted from aerial imagery using the
    `Tile2Net` deep learning framework. It implements the `UrbanLayerBase` interface, ensuring
    compatibility with other `UrbanMapper` components such as filters, enrichers, and pipelines.

    !!! tip "When to Use?"
        Sidewalk data is particularly useful for:

        - [x] Pedestrian accessibility studies
        - [x] Walkability analysis
        - [x] Urban planning and design
        - [x] Mobility assessment for people with disabilities

    Attributes:
        layer (GeoDataFrame): The GeoDataFrame containing sidewalk geometries (set after loading).

    Examples:
        Load and visualise sidewalk data:
        >>> from urban_mapper import UrbanMapper
        >>> mapper = UrbanMapper()
        >>> sidewalks = mapper.urban_layer.tile2net_sidewalks().from_file("path/to/sidewalks.geojson")
        >>> sidewalks.static_render(figsize=(10, 8), colour="blue")

    !!! note "Data Source"
        Sidewalk data must be pre-extracted using `Tile2Net` and loaded from files. Direct
        loading from place names or other spatial queries is not supported.

        See further here: [Tile2Net VIDA NYU](https://github.com/VIDA-NYU/tile2net) && [This Feature Request](https://github.com/VIDA-NYU/UrbanMapper/issues/17)
    """

    def from_file(self, file_path: str | Path, **kwargs) -> None:
        """Load sidewalk data from a file produced by `Tile2Net`.

        This method reads a spatial data file containing `Tile2Net` output, filters for `sidewalk
        features`, and prepares them for use within the UrbanMapper's workflow.

        Args:
            file_path (str | Path): Path to the file containing `Tile2Net` output. Supported formats
                include `GeoJSON`, `Shapefile`, and others compatible with GeoPandas. Note that it needs to be
                exported out of `Tile2Net`. If `Tile2Net` exports only `Shapefile`, then the file_path
                should point to the `.shp` file, and all other files in the same directory will be loaded.
                If `Tile2Net` supports `GeoJSON` or other `Geopandas` formats at some points, it'll be automatically
                supported here.
            **kwargs: Additional parameters passed to `gpd.read_file()`.

        Returns:
            Self, enabling method chaining.

        Raises:
            ValueError: If the file contains a `feature_id` column, which conflicts with the ID
                column added by this method.
            FileNotFoundError: If the specified file does not exist.

        Examples:
            >>> sidewalks = Tile2NetSidewalks().from_file("path/to/tile2net_output.geojson")
        """
        self.layer = gpd.read_file(file_path)
        self.layer = self.layer[self.layer["f_type"] == "sidewalk"]
        self.layer = self.layer.to_crs(self.coordinate_reference_system)
        self.layer = self.layer.reset_index(drop=True)
        if "feature_id" in self.layer.columns:
            raise ValueError(
                "Feature ID column already exists in the layer. Please remove it before loading."
            )
        self.layer["feature_id"] = self.layer.index

    def from_place(self, place_name: str, **kwargs) -> None:
        """Load sidewalk data for a specific place.

        !!! danger "Not Implemented"
            This method is not yet implemented. Currently, sidewalk data can only be loaded
            from files produced by `Tile2Net`. Future versions may support loading from
            geographic place names or other spatial queries.

            See further in [this feature request](https://github.com/VIDA-NYU/UrbanMapper/issues/17)
        """
        raise NotImplementedError(
            "Loading sidewalks from place is not yet implemented."
        )

    @require_attributes_not_none(
        "layer", error_msg="Layer not loaded. Call from_file() first."
    )
    def _map_nearest_layer(
        self,
        data: gpd.GeoDataFrame,
        longitude_column: str,
        latitude_column: str,
        output_column: str = "nearest_sidewalk",
        threshold_distance: float | None = None,
        _reset_layer_index: bool = True,
        **kwargs,
    ) -> Tuple[gpd.GeoDataFrame, gpd.GeoDataFrame]:
        """Map points to their nearest sidewalk segments.

        This internal method identifies the nearest sidewalk segment for each point in the input
        `GeoDataFrame` and adds a reference to that segment as a new column. It’s primarily
        used by `UrbanLayerBase.map_nearest_layer()` to perform spatial joins between point
        data and sidewalks.

        The method utilises GeoPandas’ spatial join with nearest match to find the closest
        sidewalk segment for each point. If a threshold distance is specified, points beyond that
        distance will not be matched.

        Args:
            data (GeoDataFrame): GeoDataFrame containing point data to map.
            longitude_column (str): Name of the column containing longitude values.
            latitude_column (str): Name of the column containing latitude values.
            output_column (str): Name of the column to store the indices of nearest sidewalks
                (default: "nearest_sidewalk").
            threshold_distance (float | None): Maximum distance to consider a match, in CRS units
                (default: None).
            _reset_layer_index (bool): Whether to reset the index of the layer GeoDataFrame
                (default: True).
            **kwargs: Additional parameters (not used).

        Returns:
            Tuple[GeoDataFrame, GeoDataFrame]: A tuple containing:
                - The sidewalk network GeoDataFrame (possibly with reset index)
                - The input GeoDataFrame with the new output_column added (filtered if
                  threshold_distance was specified)

        !!! note "Coordinate Reference System"
            The method automatically converts the input data to a projected CRS if it’s not
            already projected, ensuring accurate distance calculations.
        """
        dataframe = data.copy()
        if "geometry" not in dataframe.columns:
            dataframe = gpd.GeoDataFrame(
                dataframe,
                geometry=gpd.points_from_xy(
                    dataframe[longitude_column], dataframe[latitude_column]
                ),
                crs=self.coordinate_reference_system,
            )

        if not dataframe.crs.is_projected:
            utm_crs = dataframe.estimate_utm_crs()
            dataframe = dataframe.to_crs(utm_crs)
            layer_projected = self.layer.to_crs(utm_crs)
        else:
            layer_projected = self.layer

        mapped_data = gpd.sjoin_nearest(
            dataframe,
            layer_projected[["geometry", "feature_id"]],
            how="left",
            max_distance=threshold_distance,
            distance_col="distance_to_sidewalk",
        )
        mapped_data[output_column] = mapped_data["feature_id"]

        if _reset_layer_index:
            self.layer = self.layer.reset_index()

        mapped_data = mapped_data[~mapped_data.index.duplicated(keep="first")]

        return self.layer, mapped_data.drop(
            columns=["feature_id", "distance_to_sidewalk", "index_right"]
        )

    @require_attributes_not_none(
        "layer", error_msg="Layer not built. Call from_file() first."
    )
    def get_layer(self) -> gpd.GeoDataFrame:
        """Get the sidewalk network as a GeoDataFrame.

        Returns the sidewalk network as a GeoDataFrame for further analysis or visualisation.

        Returns:
            GeoDataFrame: GeoDataFrame containing the sidewalk segments.

        Raises:
            ValueError: If the layer has not been loaded yet.

        Examples:
            >>> sidewalks = Tile2NetSidewalks().from_file("path/to/sidewalks.geojson")
            >>> sidewalks_gdf = sidewalks.get_layer()
            >>> print(f"Loaded {len(sidewalks_gdf)} sidewalk segments")
        """
        return self.layer

    @require_attributes_not_none(
        "layer", error_msg="Layer not built. Call from_file() first."
    )
    def get_layer_bounding_box(self) -> Tuple[float, float, float, float]:
        """Get the bounding box of the sidewalk network.

        Returns the bounding box coordinates of the sidewalk network, useful for spatial
        queries or visualisation extents.

        Returns:
            Tuple[float, float, float, float]: Tuple of (left, bottom, right, top) coordinates
                defining the bounding box.

        Raises:
            ValueError: If the layer has not been loaded yet.

        Examples:
            >>> sidewalks = Tile2NetSidewalks().from_file("path/to/sidewalks.geojson")
            >>> bbox = sidewalks.get_layer_bounding_box()
            >>> print(f"Sidewalks extent: {bbox}")
        """
        return tuple(self.layer.total_bounds)  # type: ignore

    @require_attributes_not_none(
        "layer", error_msg="No layer built. Call from_file() first."
    )
    def static_render(self, **plot_kwargs) -> None:
        """Render the sidewalk network as a static plot.

        Creates a static visualisation of the sidewalk network using GeoPandas’ plotting
        functionality, displayed immediately.

        Args:
            **plot_kwargs: Additional keyword arguments passed to `GeoDataFrame.plot()`.
                Common options include:
                - figsize: Size of the figure as a tuple (width, height)
                - colour: Colour for the sidewalks
                - alpha: Transparency level
                - linewidth: Width of the sidewalk lines
                - edgecolour: Colour for the edges of polygons (if applicable)

        Raises:
            ValueError: If no layer has been loaded yet.

        Examples:
            >>> sidewalks = Tile2NetSidewalks().from_file("path/to/sidewalks.geojson")
            >>> sidewalks.static_render(figsize=(10, 8), colour="green", linewidth=0.8)
        """
        self.layer.plot(**plot_kwargs)

    def preview(self, format: str = "ascii") -> Any:
        """Generate a preview of this urban layer.

        Produces a textual or structured representation of the `Tile2NetSidewalks` layer for
        quick inspection, including metadata like the coordinate reference system and mappings.

        Args:
            format (str): Output format for the preview (default: "ascii").

                - [x] "ascii": Text-based format for terminal display
                - [x] "json": JSON-formatted data for programmatic use

        Returns:
            str | dict: A string (for "ascii") or dictionary (for "json") representing the
                sidewalk network layer.

        Raises:
            ValueError: If an unsupported format is requested.

        Examples:
            >>> sidewalks = Tile2NetSidewalks().from_file("path/to/sidewalks.geojson")
            >>> print(sidewalks.preview())
            >>> # JSON preview
            >>> import json
            >>> print(json.dumps(sidewalks.preview(format="json"), indent=2))
        """
        mappings_str = (
            "\n".join(
                "Mapping:\n"
                f"    - lon={m.get('longitude_column', 'N/A')}, "
                f"lat={m.get('latitude_column', 'N/A')}, "
                f"output={m.get('output_column', 'N/A')}"
                for m in self.mappings
            )
            if self.mappings
            else "    No mappings"
        )
        if format == "ascii":
            return (
                f"Urban Layer: Tile2NetSidewalks\n"
                f"  CRS: {self.coordinate_reference_system}\n"
                f"  Mappings:\n{mappings_str}"
            )
        elif format == "json":
            return {
                "urban_layer": "Tile2NetSidewalks",
                "coordinate_reference_system": self.coordinate_reference_system,
                "mappings": self.mappings,
            }
        else:
            raise ValueError(f"Unsupported format '{format}'")

from_file(file_path, **kwargs)

Load sidewalk data from a file produced by Tile2Net.

This method reads a spatial data file containing Tile2Net output, filters for sidewalk features, and prepares them for use within the UrbanMapper's workflow.

Parameters:

Name Type Description Default
file_path str | Path

Path to the file containing Tile2Net output. Supported formats include GeoJSON, Shapefile, and others compatible with GeoPandas. Note that it needs to be exported out of Tile2Net. If Tile2Net exports only Shapefile, then the file_path should point to the .shp file, and all other files in the same directory will be loaded. If Tile2Net supports GeoJSON or other Geopandas formats at some points, it'll be automatically supported here.

required
**kwargs

Additional parameters passed to gpd.read_file().

{}

Returns:

Type Description
None

Self, enabling method chaining.

Raises:

Type Description
ValueError

If the file contains a feature_id column, which conflicts with the ID column added by this method.

FileNotFoundError

If the specified file does not exist.

Examples:

>>> sidewalks = Tile2NetSidewalks().from_file("path/to/tile2net_output.geojson")
Source code in src/urban_mapper/modules/urban_layer/urban_layers/tile2net_sidewalks.py
def from_file(self, file_path: str | Path, **kwargs) -> None:
    """Load sidewalk data from a file produced by `Tile2Net`.

    This method reads a spatial data file containing `Tile2Net` output, filters for `sidewalk
    features`, and prepares them for use within the UrbanMapper's workflow.

    Args:
        file_path (str | Path): Path to the file containing `Tile2Net` output. Supported formats
            include `GeoJSON`, `Shapefile`, and others compatible with GeoPandas. Note that it needs to be
            exported out of `Tile2Net`. If `Tile2Net` exports only `Shapefile`, then the file_path
            should point to the `.shp` file, and all other files in the same directory will be loaded.
            If `Tile2Net` supports `GeoJSON` or other `Geopandas` formats at some points, it'll be automatically
            supported here.
        **kwargs: Additional parameters passed to `gpd.read_file()`.

    Returns:
        Self, enabling method chaining.

    Raises:
        ValueError: If the file contains a `feature_id` column, which conflicts with the ID
            column added by this method.
        FileNotFoundError: If the specified file does not exist.

    Examples:
        >>> sidewalks = Tile2NetSidewalks().from_file("path/to/tile2net_output.geojson")
    """
    self.layer = gpd.read_file(file_path)
    self.layer = self.layer[self.layer["f_type"] == "sidewalk"]
    self.layer = self.layer.to_crs(self.coordinate_reference_system)
    self.layer = self.layer.reset_index(drop=True)
    if "feature_id" in self.layer.columns:
        raise ValueError(
            "Feature ID column already exists in the layer. Please remove it before loading."
        )
    self.layer["feature_id"] = self.layer.index

from_place(place_name, **kwargs)

Load sidewalk data for a specific place.

Not Implemented

This method is not yet implemented. Currently, sidewalk data can only be loaded from files produced by Tile2Net. Future versions may support loading from geographic place names or other spatial queries.

See further in this feature request

Source code in src/urban_mapper/modules/urban_layer/urban_layers/tile2net_sidewalks.py
def from_place(self, place_name: str, **kwargs) -> None:
    """Load sidewalk data for a specific place.

    !!! danger "Not Implemented"
        This method is not yet implemented. Currently, sidewalk data can only be loaded
        from files produced by `Tile2Net`. Future versions may support loading from
        geographic place names or other spatial queries.

        See further in [this feature request](https://github.com/VIDA-NYU/UrbanMapper/issues/17)
    """
    raise NotImplementedError(
        "Loading sidewalks from place is not yet implemented."
    )

_map_nearest_layer(data, longitude_column, latitude_column, output_column='nearest_sidewalk', threshold_distance=None, _reset_layer_index=True, **kwargs)

Map points to their nearest sidewalk segments.

This internal method identifies the nearest sidewalk segment for each point in the input GeoDataFrame and adds a reference to that segment as a new column. It’s primarily used by UrbanLayerBase.map_nearest_layer() to perform spatial joins between point data and sidewalks.

The method utilises GeoPandas’ spatial join with nearest match to find the closest sidewalk segment for each point. If a threshold distance is specified, points beyond that distance will not be matched.

Parameters:

Name Type Description Default
data GeoDataFrame

GeoDataFrame containing point data to map.

required
longitude_column str

Name of the column containing longitude values.

required
latitude_column str

Name of the column containing latitude values.

required
output_column str

Name of the column to store the indices of nearest sidewalks (default: "nearest_sidewalk").

'nearest_sidewalk'
threshold_distance float | None

Maximum distance to consider a match, in CRS units (default: None).

None
_reset_layer_index bool

Whether to reset the index of the layer GeoDataFrame (default: True).

True
**kwargs

Additional parameters (not used).

{}

Returns:

Type Description
Tuple[GeoDataFrame, GeoDataFrame]

Tuple[GeoDataFrame, GeoDataFrame]: A tuple containing: - The sidewalk network GeoDataFrame (possibly with reset index) - The input GeoDataFrame with the new output_column added (filtered if threshold_distance was specified)

Coordinate Reference System

The method automatically converts the input data to a projected CRS if it’s not already projected, ensuring accurate distance calculations.

Source code in src/urban_mapper/modules/urban_layer/urban_layers/tile2net_sidewalks.py
@require_attributes_not_none(
    "layer", error_msg="Layer not loaded. Call from_file() first."
)
def _map_nearest_layer(
    self,
    data: gpd.GeoDataFrame,
    longitude_column: str,
    latitude_column: str,
    output_column: str = "nearest_sidewalk",
    threshold_distance: float | None = None,
    _reset_layer_index: bool = True,
    **kwargs,
) -> Tuple[gpd.GeoDataFrame, gpd.GeoDataFrame]:
    """Map points to their nearest sidewalk segments.

    This internal method identifies the nearest sidewalk segment for each point in the input
    `GeoDataFrame` and adds a reference to that segment as a new column. It’s primarily
    used by `UrbanLayerBase.map_nearest_layer()` to perform spatial joins between point
    data and sidewalks.

    The method utilises GeoPandas’ spatial join with nearest match to find the closest
    sidewalk segment for each point. If a threshold distance is specified, points beyond that
    distance will not be matched.

    Args:
        data (GeoDataFrame): GeoDataFrame containing point data to map.
        longitude_column (str): Name of the column containing longitude values.
        latitude_column (str): Name of the column containing latitude values.
        output_column (str): Name of the column to store the indices of nearest sidewalks
            (default: "nearest_sidewalk").
        threshold_distance (float | None): Maximum distance to consider a match, in CRS units
            (default: None).
        _reset_layer_index (bool): Whether to reset the index of the layer GeoDataFrame
            (default: True).
        **kwargs: Additional parameters (not used).

    Returns:
        Tuple[GeoDataFrame, GeoDataFrame]: A tuple containing:
            - The sidewalk network GeoDataFrame (possibly with reset index)
            - The input GeoDataFrame with the new output_column added (filtered if
              threshold_distance was specified)

    !!! note "Coordinate Reference System"
        The method automatically converts the input data to a projected CRS if it’s not
        already projected, ensuring accurate distance calculations.
    """
    dataframe = data.copy()
    if "geometry" not in dataframe.columns:
        dataframe = gpd.GeoDataFrame(
            dataframe,
            geometry=gpd.points_from_xy(
                dataframe[longitude_column], dataframe[latitude_column]
            ),
            crs=self.coordinate_reference_system,
        )

    if not dataframe.crs.is_projected:
        utm_crs = dataframe.estimate_utm_crs()
        dataframe = dataframe.to_crs(utm_crs)
        layer_projected = self.layer.to_crs(utm_crs)
    else:
        layer_projected = self.layer

    mapped_data = gpd.sjoin_nearest(
        dataframe,
        layer_projected[["geometry", "feature_id"]],
        how="left",
        max_distance=threshold_distance,
        distance_col="distance_to_sidewalk",
    )
    mapped_data[output_column] = mapped_data["feature_id"]

    if _reset_layer_index:
        self.layer = self.layer.reset_index()

    mapped_data = mapped_data[~mapped_data.index.duplicated(keep="first")]

    return self.layer, mapped_data.drop(
        columns=["feature_id", "distance_to_sidewalk", "index_right"]
    )

get_layer()

Get the sidewalk network as a GeoDataFrame.

Returns the sidewalk network as a GeoDataFrame for further analysis or visualisation.

Returns:

Name Type Description
GeoDataFrame GeoDataFrame

GeoDataFrame containing the sidewalk segments.

Raises:

Type Description
ValueError

If the layer has not been loaded yet.

Examples:

>>> sidewalks = Tile2NetSidewalks().from_file("path/to/sidewalks.geojson")
>>> sidewalks_gdf = sidewalks.get_layer()
>>> print(f"Loaded {len(sidewalks_gdf)} sidewalk segments")
Source code in src/urban_mapper/modules/urban_layer/urban_layers/tile2net_sidewalks.py
@require_attributes_not_none(
    "layer", error_msg="Layer not built. Call from_file() first."
)
def get_layer(self) -> gpd.GeoDataFrame:
    """Get the sidewalk network as a GeoDataFrame.

    Returns the sidewalk network as a GeoDataFrame for further analysis or visualisation.

    Returns:
        GeoDataFrame: GeoDataFrame containing the sidewalk segments.

    Raises:
        ValueError: If the layer has not been loaded yet.

    Examples:
        >>> sidewalks = Tile2NetSidewalks().from_file("path/to/sidewalks.geojson")
        >>> sidewalks_gdf = sidewalks.get_layer()
        >>> print(f"Loaded {len(sidewalks_gdf)} sidewalk segments")
    """
    return self.layer

get_layer_bounding_box()

Get the bounding box of the sidewalk network.

Returns the bounding box coordinates of the sidewalk network, useful for spatial queries or visualisation extents.

Returns:

Type Description
Tuple[float, float, float, float]

Tuple[float, float, float, float]: Tuple of (left, bottom, right, top) coordinates defining the bounding box.

Raises:

Type Description
ValueError

If the layer has not been loaded yet.

Examples:

>>> sidewalks = Tile2NetSidewalks().from_file("path/to/sidewalks.geojson")
>>> bbox = sidewalks.get_layer_bounding_box()
>>> print(f"Sidewalks extent: {bbox}")
Source code in src/urban_mapper/modules/urban_layer/urban_layers/tile2net_sidewalks.py
@require_attributes_not_none(
    "layer", error_msg="Layer not built. Call from_file() first."
)
def get_layer_bounding_box(self) -> Tuple[float, float, float, float]:
    """Get the bounding box of the sidewalk network.

    Returns the bounding box coordinates of the sidewalk network, useful for spatial
    queries or visualisation extents.

    Returns:
        Tuple[float, float, float, float]: Tuple of (left, bottom, right, top) coordinates
            defining the bounding box.

    Raises:
        ValueError: If the layer has not been loaded yet.

    Examples:
        >>> sidewalks = Tile2NetSidewalks().from_file("path/to/sidewalks.geojson")
        >>> bbox = sidewalks.get_layer_bounding_box()
        >>> print(f"Sidewalks extent: {bbox}")
    """
    return tuple(self.layer.total_bounds)  # type: ignore

static_render(**plot_kwargs)

Render the sidewalk network as a static plot.

Creates a static visualisation of the sidewalk network using GeoPandas’ plotting functionality, displayed immediately.

Parameters:

Name Type Description Default
**plot_kwargs

Additional keyword arguments passed to GeoDataFrame.plot(). Common options include: - figsize: Size of the figure as a tuple (width, height) - colour: Colour for the sidewalks - alpha: Transparency level - linewidth: Width of the sidewalk lines - edgecolour: Colour for the edges of polygons (if applicable)

{}

Raises:

Type Description
ValueError

If no layer has been loaded yet.

Examples:

>>> sidewalks = Tile2NetSidewalks().from_file("path/to/sidewalks.geojson")
>>> sidewalks.static_render(figsize=(10, 8), colour="green", linewidth=0.8)
Source code in src/urban_mapper/modules/urban_layer/urban_layers/tile2net_sidewalks.py
@require_attributes_not_none(
    "layer", error_msg="No layer built. Call from_file() first."
)
def static_render(self, **plot_kwargs) -> None:
    """Render the sidewalk network as a static plot.

    Creates a static visualisation of the sidewalk network using GeoPandas’ plotting
    functionality, displayed immediately.

    Args:
        **plot_kwargs: Additional keyword arguments passed to `GeoDataFrame.plot()`.
            Common options include:
            - figsize: Size of the figure as a tuple (width, height)
            - colour: Colour for the sidewalks
            - alpha: Transparency level
            - linewidth: Width of the sidewalk lines
            - edgecolour: Colour for the edges of polygons (if applicable)

    Raises:
        ValueError: If no layer has been loaded yet.

    Examples:
        >>> sidewalks = Tile2NetSidewalks().from_file("path/to/sidewalks.geojson")
        >>> sidewalks.static_render(figsize=(10, 8), colour="green", linewidth=0.8)
    """
    self.layer.plot(**plot_kwargs)

preview(format='ascii')

Generate a preview of this urban layer.

Produces a textual or structured representation of the Tile2NetSidewalks layer for quick inspection, including metadata like the coordinate reference system and mappings.

Parameters:

Name Type Description Default
format str

Output format for the preview (default: "ascii").

  • "ascii": Text-based format for terminal display
  • "json": JSON-formatted data for programmatic use
'ascii'

Returns:

Type Description
Any

str | dict: A string (for "ascii") or dictionary (for "json") representing the sidewalk network layer.

Raises:

Type Description
ValueError

If an unsupported format is requested.

Examples:

>>> sidewalks = Tile2NetSidewalks().from_file("path/to/sidewalks.geojson")
>>> print(sidewalks.preview())
>>> # JSON preview
>>> import json
>>> print(json.dumps(sidewalks.preview(format="json"), indent=2))
Source code in src/urban_mapper/modules/urban_layer/urban_layers/tile2net_sidewalks.py
def preview(self, format: str = "ascii") -> Any:
    """Generate a preview of this urban layer.

    Produces a textual or structured representation of the `Tile2NetSidewalks` layer for
    quick inspection, including metadata like the coordinate reference system and mappings.

    Args:
        format (str): Output format for the preview (default: "ascii").

            - [x] "ascii": Text-based format for terminal display
            - [x] "json": JSON-formatted data for programmatic use

    Returns:
        str | dict: A string (for "ascii") or dictionary (for "json") representing the
            sidewalk network layer.

    Raises:
        ValueError: If an unsupported format is requested.

    Examples:
        >>> sidewalks = Tile2NetSidewalks().from_file("path/to/sidewalks.geojson")
        >>> print(sidewalks.preview())
        >>> # JSON preview
        >>> import json
        >>> print(json.dumps(sidewalks.preview(format="json"), indent=2))
    """
    mappings_str = (
        "\n".join(
            "Mapping:\n"
            f"    - lon={m.get('longitude_column', 'N/A')}, "
            f"lat={m.get('latitude_column', 'N/A')}, "
            f"output={m.get('output_column', 'N/A')}"
            for m in self.mappings
        )
        if self.mappings
        else "    No mappings"
    )
    if format == "ascii":
        return (
            f"Urban Layer: Tile2NetSidewalks\n"
            f"  CRS: {self.coordinate_reference_system}\n"
            f"  Mappings:\n{mappings_str}"
        )
    elif format == "json":
        return {
            "urban_layer": "Tile2NetSidewalks",
            "coordinate_reference_system": self.coordinate_reference_system,
            "mappings": self.mappings,
        }
    else:
        raise ValueError(f"Unsupported format '{format}'")

Tile2NetCrosswalks

Bases: UrbanLayerBase

Urban layer implementation for crosswalks extracted with Tile2Net.

This class provides access to crosswalk data extracted from aerial imagery using the Tile2Net deep learning framework. It implements the UrbanLayerBase interface, ensuring compatibility with other UrbanMapper components such as filters, enrichers, and pipelines.

When to Use?

Crosswalk data is particularly useful for:

  • Pedestrian safety analysis
  • Intersection safety studies
  • Accessibility assessment
  • Walkability index calculations
  • Complete streets planning

Attributes:

Name Type Description
layer GeoDataFrame

The GeoDataFrame containing crosswalk geometries (set after loading).

Examples:

Load and visualise crosswalk data:

>>> from urban_mapper import UrbanMapper
>>> mapper = UrbanMapper()
>>> crosswalks = mapper.urban_layer.tile2net_crosswalks().from_file("path/to/crosswalks.geojson")
>>> crosswalks.static_render(figsize=(10, 8), colour="red")

Data Source

Crosswalk data must be pre-extracted using Tile2Net and loaded from files. Direct loading from place names or other spatial queries is not supported.

See further here: Tile2Net VIDA NYU && This Feature Request

Source code in src/urban_mapper/modules/urban_layer/urban_layers/tile2net_crosswalks.py
@beartype
class Tile2NetCrosswalks(UrbanLayerBase):
    """Urban layer implementation for crosswalks extracted with `Tile2Net`.

    This class provides access to crosswalk data extracted from aerial imagery using the
    `Tile2Net` deep learning framework. It implements the `UrbanLayerBase` interface, ensuring
    compatibility with other `UrbanMapper` components such as filters, enrichers, and pipelines.

    !!! tip "When to Use?"
        Crosswalk data is particularly useful for:

        - [x] Pedestrian safety analysis
        - [x] Intersection safety studies
        - [x] Accessibility assessment
        - [x] Walkability index calculations
        - [x] Complete streets planning

    Attributes:
        layer (GeoDataFrame): The GeoDataFrame containing crosswalk geometries (set after loading).

    Examples:
        Load and visualise crosswalk data:
        >>> from urban_mapper import UrbanMapper
        >>> mapper = UrbanMapper()
        >>> crosswalks = mapper.urban_layer.tile2net_crosswalks().from_file("path/to/crosswalks.geojson")
        >>> crosswalks.static_render(figsize=(10, 8), colour="red")

    !!! note "Data Source"
        Crosswalk data must be pre-extracted using `Tile2Net` and loaded from files. Direct
        loading from place names or other spatial queries is not supported.

        See further here: [Tile2Net VIDA NYU](https://github.com/VIDA-NYU/tile2net) && [This Feature Request](https://github.com/VIDA-NYU/UrbanMapper/issues/17)
    """

    def from_file(self, file_path: str | Path, **kwargs) -> None:
        """Load crosswalk data from a file produced by `Tile2Net`.

        This method reads a spatial data file containing `Tile2Net` output, filters for `crosswalk
        features`, and prepares them for use within the UrbanMapper's workflow

        Args:
            file_path (str | Path): Path to the file containing `Tile2Net` output. Supported formats
                include `GeoJSON`, `Shapefile`, and others compatible with GeoPandas. Note that it needs to be
                exported out of `Tile2Net`. If `Tile2Net` exports is only `Shapefile`, then the file_path
                should point to the `.shp` file, and all other files in the same directory will be loaded.
                If `Tile2Net` supports `GeoJSON` or other `Geopandas` formats at some points, it'll be automatically
                supported here.
            **kwargs: Additional parameters passed to `gpd.read_file()`.

        Returns:
            Self, enabling method chaining.

        Raises:
            ValueError: If the file contains a `feature_id` column, which conflicts with the ID
                column added by this method.
            FileNotFoundError: If the specified file does not exist.

        Examples:
            >>> crosswalks = Tile2NetCrosswalks().from_file("path/to/tile2net_output.geojson")
        """
        self.layer = gpd.read_file(file_path)
        self.layer = self.layer[self.layer["f_type"] == "crosswalk"]
        self.layer = self.layer.to_crs(self.coordinate_reference_system)
        if "feature_id" in self.layer.columns:
            raise ValueError(
                "Feature ID column already exists in the layer. Please remove it before loading."
            )
        self.layer["feature_id"] = self.layer.index

    def from_place(self, place_name: str, **kwargs) -> None:
        """Load crosswalk data for a specific place.

        !!! danger "Not Implemented"
            This method is not yet implemented. Currently, crosswalk data can only be loaded
            from files produced by `Tile2Net`. Future versions may support loading from
            geographic place names or other spatial queries.

            See further in [this feature request](https://github.com/VIDA-NYU/UrbanMapper/issues/17)
        """
        raise NotImplementedError(
            "Loading crosswalks from place is not yet implemented."
        )

    @require_attributes_not_none(
        "layer", error_msg="Layer not loaded. Call from_file() first."
    )
    def _map_nearest_layer(
        self,
        data: gpd.GeoDataFrame,
        longitude_column: str,
        latitude_column: str,
        output_column: str = "nearest_crosswalk",
        threshold_distance: float | None = None,
        _reset_layer_index: bool = True,
        **kwargs,
    ) -> Tuple[gpd.GeoDataFrame, gpd.GeoDataFrame]:
        """Map points to their nearest crosswalk features.

        This internal method identifies the nearest crosswalk for each point in the input
        `GeoDataFrame` and adds a reference to that crosswalk as a new column. It’s primarily
        used by `UrbanLayerBase.map_nearest_layer()` to perform spatial joins between point
        data and crosswalks.

        The method utilises GeoPandas’ spatial join with nearest match to find the closest
        crosswalk for each point. If a threshold distance is specified, points beyond that
        distance will not be matched.

        Args:
            data (GeoDataFrame): GeoDataFrame containing point data to map.
            longitude_column (str): Name of the column containing longitude values.
            latitude_column (str): Name of the column containing latitude values.
            output_column (str): Name of the column to store the indices of nearest crosswalks
                (default: "nearest_crosswalk").
            threshold_distance (float | None): Maximum distance to consider a match, in CRS units
                (default: None).
            _reset_layer_index (bool): Whether to reset the index of the layer GeoDataFrame
                (default: True).
            **kwargs: Additional parameters (not used).

        Returns:
            Tuple[GeoDataFrame, GeoDataFrame]: A tuple containing:
                - The crosswalk network GeoDataFrame (possibly with reset index)
                - The input GeoDataFrame with the new output_column added (filtered if
                  threshold_distance was specified)

        !!! note "Coordinate Reference System"
            The method automatically converts the input data to a projected CRS if it’s not
            already projected, ensuring accurate distance calculations.
        """
        dataframe = data.copy()
        if "geometry" not in dataframe.columns:
            dataframe = gpd.GeoDataFrame(
                dataframe,
                geometry=gpd.points_from_xy(
                    dataframe[longitude_column], dataframe[latitude_column]
                ),
                crs=self.coordinate_reference_system,
            )
        if not dataframe.crs.is_projected:
            utm_crs = dataframe.estimate_utm_crs()
            dataframe = dataframe.to_crs(utm_crs)
            layer_projected = self.layer.to_crs(utm_crs)
        else:
            layer_projected = self.layer

        mapped_data = gpd.sjoin_nearest(
            dataframe,
            layer_projected[["geometry", "feature_id"]],
            how="left",
            max_distance=threshold_distance,
            distance_col="distance_to_crosswalk",
        )
        mapped_data[output_column] = mapped_data["feature_id"]
        return self.layer, mapped_data.drop(
            columns=["feature_id", "distance_to_crosswalk", "index_right"]
        )

    @require_attributes_not_none(
        "layer", error_msg="Layer not built. Call from_file() first."
    )
    def get_layer(self) -> gpd.GeoDataFrame:
        """Get the crosswalk network as a GeoDataFrame.

        Returns the crosswalk network as a GeoDataFrame for further analysis or visualisation.

        Returns:
            GeoDataFrame: GeoDataFrame containing the crosswalk segments.

        Raises:
            ValueError: If the layer has not been loaded yet.

        Examples:
            >>> crosswalks = Tile2NetCrosswalks().from_file("path/to/crosswalks.geojson")
            >>> crosswalks_gdf = crosswalks.get_layer()
            >>> print(f"Loaded {len(crosswalks_gdf)} crosswalk segments")
        """
        return self.layer

    @require_attributes_not_none(
        "layer", error_msg="Layer not built. Call from_file() first."
    )
    def get_layer_bounding_box(self) -> Tuple[float, float, float, float]:
        """Get the bounding box of the crosswalk network.

        Returns the bounding box coordinates of the crosswalk network, useful for spatial
        queries or visualisation extents.

        Returns:
            Tuple[float, float, float, float]: Tuple of (left, bottom, right, top) coordinates
                defining the bounding box.

        Raises:
            ValueError: If the layer has not been loaded yet.

        Examples:
            >>> crosswalks = Tile2NetCrosswalks().from_file("path/to/crosswalks.geojson")
            >>> bbox = crosswalks.get_layer_bounding_box()
            >>> print(f"Crosswalks extent: {bbox}")
        """
        return tuple(self.layer.total_bounds)  # type: ignore

    @require_attributes_not_none(
        "layer", error_msg="Layer not built. Call from_file() first."
    )
    def static_render(self, **plot_kwargs) -> None:
        """Render the crosswalk network as a static plot.

        Creates a static visualisation of the crosswalk network using GeoPandas’ plotting
        functionality, displayed immediately.

        Args:
            **plot_kwargs: Additional keyword arguments passed to `GeoDataFrame.plot()`.
                Common options include:
                - figsize: Size of the figure as a tuple (width, height)
                - colour: Colour for the crosswalks
                - alpha: Transparency level
                - linewidth: Width of the crosswalk lines
                - edgecolour: Colour for the edges of polygons (if applicable)

        Raises:
            ValueError: If no layer has been loaded yet.

        Examples:
            >>> crosswalks = Tile2NetCrosswalks().from_file("path/to/crosswalks.geojson")
            >>> crosswalks.static_render(figsize=(10, 8), colour="yellow", linewidth=1.5)
        """
        self.layer.plot(**plot_kwargs)

    def preview(self, format: str = "ascii") -> Any:
        """Generate a preview of this urban layer.

        Produces a textual or structured representation of the `Tile2NetCrosswalks` layer for
        quick inspection, including metadata like the coordinate reference system and mappings.

        Args:
            format (str): Output format for the preview (default: "ascii").
                - "ascii": Text-based format for terminal display
                - "json": JSON-formatted data for programmatic use

        Returns:
            str | dict: A string (for "ascii") or dictionary (for "json") representing the
                crosswalk network layer.

        Raises:
            ValueError: If an unsupported format is requested.

        Examples:
            >>> crosswalks = Tile2NetCrosswalks().from_file("path/to/crosswalks.geojson")
            >>> print(crosswalks.preview())
            >>> # JSON preview
            >>> import json
            >>> print(json.dumps(crosswalks.preview(format="json"), indent=2))
        """
        mappings_str = (
            "\n".join(
                "Mapping:\n"
                f"    - lon={m.get('longitude_column', 'N/A')}, "
                f"lat={m.get('latitude_column', 'N/A')}, "
                f"output={m.get('output_column', 'N/A')}"
                for m in self.mappings
            )
            if self.mappings
            else "    No mappings"
        )
        if format == "ascii":
            return (
                f"Urban Layer: Tile2NetCrosswalks\n"
                f"  CRS: {self.coordinate_reference_system}\n"
                f"  Mappings:\n{mappings_str}"
            )
        elif format == "json":
            return {
                "urban_layer": "Tile2NetCrosswalks",
                "coordinate_reference_system": self.coordinate_reference_system,
                "mappings": self.mappings,
            }
        else:
            raise ValueError(f"Unsupported format '{format}'")

from_file(file_path, **kwargs)

Load crosswalk data from a file produced by Tile2Net.

This method reads a spatial data file containing Tile2Net output, filters for crosswalk features, and prepares them for use within the UrbanMapper's workflow

Parameters:

Name Type Description Default
file_path str | Path

Path to the file containing Tile2Net output. Supported formats include GeoJSON, Shapefile, and others compatible with GeoPandas. Note that it needs to be exported out of Tile2Net. If Tile2Net exports is only Shapefile, then the file_path should point to the .shp file, and all other files in the same directory will be loaded. If Tile2Net supports GeoJSON or other Geopandas formats at some points, it'll be automatically supported here.

required
**kwargs

Additional parameters passed to gpd.read_file().

{}

Returns:

Type Description
None

Self, enabling method chaining.

Raises:

Type Description
ValueError

If the file contains a feature_id column, which conflicts with the ID column added by this method.

FileNotFoundError

If the specified file does not exist.

Examples:

>>> crosswalks = Tile2NetCrosswalks().from_file("path/to/tile2net_output.geojson")
Source code in src/urban_mapper/modules/urban_layer/urban_layers/tile2net_crosswalks.py
def from_file(self, file_path: str | Path, **kwargs) -> None:
    """Load crosswalk data from a file produced by `Tile2Net`.

    This method reads a spatial data file containing `Tile2Net` output, filters for `crosswalk
    features`, and prepares them for use within the UrbanMapper's workflow

    Args:
        file_path (str | Path): Path to the file containing `Tile2Net` output. Supported formats
            include `GeoJSON`, `Shapefile`, and others compatible with GeoPandas. Note that it needs to be
            exported out of `Tile2Net`. If `Tile2Net` exports is only `Shapefile`, then the file_path
            should point to the `.shp` file, and all other files in the same directory will be loaded.
            If `Tile2Net` supports `GeoJSON` or other `Geopandas` formats at some points, it'll be automatically
            supported here.
        **kwargs: Additional parameters passed to `gpd.read_file()`.

    Returns:
        Self, enabling method chaining.

    Raises:
        ValueError: If the file contains a `feature_id` column, which conflicts with the ID
            column added by this method.
        FileNotFoundError: If the specified file does not exist.

    Examples:
        >>> crosswalks = Tile2NetCrosswalks().from_file("path/to/tile2net_output.geojson")
    """
    self.layer = gpd.read_file(file_path)
    self.layer = self.layer[self.layer["f_type"] == "crosswalk"]
    self.layer = self.layer.to_crs(self.coordinate_reference_system)
    if "feature_id" in self.layer.columns:
        raise ValueError(
            "Feature ID column already exists in the layer. Please remove it before loading."
        )
    self.layer["feature_id"] = self.layer.index

from_place(place_name, **kwargs)

Load crosswalk data for a specific place.

Not Implemented

This method is not yet implemented. Currently, crosswalk data can only be loaded from files produced by Tile2Net. Future versions may support loading from geographic place names or other spatial queries.

See further in this feature request

Source code in src/urban_mapper/modules/urban_layer/urban_layers/tile2net_crosswalks.py
def from_place(self, place_name: str, **kwargs) -> None:
    """Load crosswalk data for a specific place.

    !!! danger "Not Implemented"
        This method is not yet implemented. Currently, crosswalk data can only be loaded
        from files produced by `Tile2Net`. Future versions may support loading from
        geographic place names or other spatial queries.

        See further in [this feature request](https://github.com/VIDA-NYU/UrbanMapper/issues/17)
    """
    raise NotImplementedError(
        "Loading crosswalks from place is not yet implemented."
    )

_map_nearest_layer(data, longitude_column, latitude_column, output_column='nearest_crosswalk', threshold_distance=None, _reset_layer_index=True, **kwargs)

Map points to their nearest crosswalk features.

This internal method identifies the nearest crosswalk for each point in the input GeoDataFrame and adds a reference to that crosswalk as a new column. It’s primarily used by UrbanLayerBase.map_nearest_layer() to perform spatial joins between point data and crosswalks.

The method utilises GeoPandas’ spatial join with nearest match to find the closest crosswalk for each point. If a threshold distance is specified, points beyond that distance will not be matched.

Parameters:

Name Type Description Default
data GeoDataFrame

GeoDataFrame containing point data to map.

required
longitude_column str

Name of the column containing longitude values.

required
latitude_column str

Name of the column containing latitude values.

required
output_column str

Name of the column to store the indices of nearest crosswalks (default: "nearest_crosswalk").

'nearest_crosswalk'
threshold_distance float | None

Maximum distance to consider a match, in CRS units (default: None).

None
_reset_layer_index bool

Whether to reset the index of the layer GeoDataFrame (default: True).

True
**kwargs

Additional parameters (not used).

{}

Returns:

Type Description
Tuple[GeoDataFrame, GeoDataFrame]

Tuple[GeoDataFrame, GeoDataFrame]: A tuple containing: - The crosswalk network GeoDataFrame (possibly with reset index) - The input GeoDataFrame with the new output_column added (filtered if threshold_distance was specified)

Coordinate Reference System

The method automatically converts the input data to a projected CRS if it’s not already projected, ensuring accurate distance calculations.

Source code in src/urban_mapper/modules/urban_layer/urban_layers/tile2net_crosswalks.py
@require_attributes_not_none(
    "layer", error_msg="Layer not loaded. Call from_file() first."
)
def _map_nearest_layer(
    self,
    data: gpd.GeoDataFrame,
    longitude_column: str,
    latitude_column: str,
    output_column: str = "nearest_crosswalk",
    threshold_distance: float | None = None,
    _reset_layer_index: bool = True,
    **kwargs,
) -> Tuple[gpd.GeoDataFrame, gpd.GeoDataFrame]:
    """Map points to their nearest crosswalk features.

    This internal method identifies the nearest crosswalk for each point in the input
    `GeoDataFrame` and adds a reference to that crosswalk as a new column. It’s primarily
    used by `UrbanLayerBase.map_nearest_layer()` to perform spatial joins between point
    data and crosswalks.

    The method utilises GeoPandas’ spatial join with nearest match to find the closest
    crosswalk for each point. If a threshold distance is specified, points beyond that
    distance will not be matched.

    Args:
        data (GeoDataFrame): GeoDataFrame containing point data to map.
        longitude_column (str): Name of the column containing longitude values.
        latitude_column (str): Name of the column containing latitude values.
        output_column (str): Name of the column to store the indices of nearest crosswalks
            (default: "nearest_crosswalk").
        threshold_distance (float | None): Maximum distance to consider a match, in CRS units
            (default: None).
        _reset_layer_index (bool): Whether to reset the index of the layer GeoDataFrame
            (default: True).
        **kwargs: Additional parameters (not used).

    Returns:
        Tuple[GeoDataFrame, GeoDataFrame]: A tuple containing:
            - The crosswalk network GeoDataFrame (possibly with reset index)
            - The input GeoDataFrame with the new output_column added (filtered if
              threshold_distance was specified)

    !!! note "Coordinate Reference System"
        The method automatically converts the input data to a projected CRS if it’s not
        already projected, ensuring accurate distance calculations.
    """
    dataframe = data.copy()
    if "geometry" not in dataframe.columns:
        dataframe = gpd.GeoDataFrame(
            dataframe,
            geometry=gpd.points_from_xy(
                dataframe[longitude_column], dataframe[latitude_column]
            ),
            crs=self.coordinate_reference_system,
        )
    if not dataframe.crs.is_projected:
        utm_crs = dataframe.estimate_utm_crs()
        dataframe = dataframe.to_crs(utm_crs)
        layer_projected = self.layer.to_crs(utm_crs)
    else:
        layer_projected = self.layer

    mapped_data = gpd.sjoin_nearest(
        dataframe,
        layer_projected[["geometry", "feature_id"]],
        how="left",
        max_distance=threshold_distance,
        distance_col="distance_to_crosswalk",
    )
    mapped_data[output_column] = mapped_data["feature_id"]
    return self.layer, mapped_data.drop(
        columns=["feature_id", "distance_to_crosswalk", "index_right"]
    )

get_layer()

Get the crosswalk network as a GeoDataFrame.

Returns the crosswalk network as a GeoDataFrame for further analysis or visualisation.

Returns:

Name Type Description
GeoDataFrame GeoDataFrame

GeoDataFrame containing the crosswalk segments.

Raises:

Type Description
ValueError

If the layer has not been loaded yet.

Examples:

>>> crosswalks = Tile2NetCrosswalks().from_file("path/to/crosswalks.geojson")
>>> crosswalks_gdf = crosswalks.get_layer()
>>> print(f"Loaded {len(crosswalks_gdf)} crosswalk segments")
Source code in src/urban_mapper/modules/urban_layer/urban_layers/tile2net_crosswalks.py
@require_attributes_not_none(
    "layer", error_msg="Layer not built. Call from_file() first."
)
def get_layer(self) -> gpd.GeoDataFrame:
    """Get the crosswalk network as a GeoDataFrame.

    Returns the crosswalk network as a GeoDataFrame for further analysis or visualisation.

    Returns:
        GeoDataFrame: GeoDataFrame containing the crosswalk segments.

    Raises:
        ValueError: If the layer has not been loaded yet.

    Examples:
        >>> crosswalks = Tile2NetCrosswalks().from_file("path/to/crosswalks.geojson")
        >>> crosswalks_gdf = crosswalks.get_layer()
        >>> print(f"Loaded {len(crosswalks_gdf)} crosswalk segments")
    """
    return self.layer

get_layer_bounding_box()

Get the bounding box of the crosswalk network.

Returns the bounding box coordinates of the crosswalk network, useful for spatial queries or visualisation extents.

Returns:

Type Description
Tuple[float, float, float, float]

Tuple[float, float, float, float]: Tuple of (left, bottom, right, top) coordinates defining the bounding box.

Raises:

Type Description
ValueError

If the layer has not been loaded yet.

Examples:

>>> crosswalks = Tile2NetCrosswalks().from_file("path/to/crosswalks.geojson")
>>> bbox = crosswalks.get_layer_bounding_box()
>>> print(f"Crosswalks extent: {bbox}")
Source code in src/urban_mapper/modules/urban_layer/urban_layers/tile2net_crosswalks.py
@require_attributes_not_none(
    "layer", error_msg="Layer not built. Call from_file() first."
)
def get_layer_bounding_box(self) -> Tuple[float, float, float, float]:
    """Get the bounding box of the crosswalk network.

    Returns the bounding box coordinates of the crosswalk network, useful for spatial
    queries or visualisation extents.

    Returns:
        Tuple[float, float, float, float]: Tuple of (left, bottom, right, top) coordinates
            defining the bounding box.

    Raises:
        ValueError: If the layer has not been loaded yet.

    Examples:
        >>> crosswalks = Tile2NetCrosswalks().from_file("path/to/crosswalks.geojson")
        >>> bbox = crosswalks.get_layer_bounding_box()
        >>> print(f"Crosswalks extent: {bbox}")
    """
    return tuple(self.layer.total_bounds)  # type: ignore

static_render(**plot_kwargs)

Render the crosswalk network as a static plot.

Creates a static visualisation of the crosswalk network using GeoPandas’ plotting functionality, displayed immediately.

Parameters:

Name Type Description Default
**plot_kwargs

Additional keyword arguments passed to GeoDataFrame.plot(). Common options include: - figsize: Size of the figure as a tuple (width, height) - colour: Colour for the crosswalks - alpha: Transparency level - linewidth: Width of the crosswalk lines - edgecolour: Colour for the edges of polygons (if applicable)

{}

Raises:

Type Description
ValueError

If no layer has been loaded yet.

Examples:

>>> crosswalks = Tile2NetCrosswalks().from_file("path/to/crosswalks.geojson")
>>> crosswalks.static_render(figsize=(10, 8), colour="yellow", linewidth=1.5)
Source code in src/urban_mapper/modules/urban_layer/urban_layers/tile2net_crosswalks.py
@require_attributes_not_none(
    "layer", error_msg="Layer not built. Call from_file() first."
)
def static_render(self, **plot_kwargs) -> None:
    """Render the crosswalk network as a static plot.

    Creates a static visualisation of the crosswalk network using GeoPandas’ plotting
    functionality, displayed immediately.

    Args:
        **plot_kwargs: Additional keyword arguments passed to `GeoDataFrame.plot()`.
            Common options include:
            - figsize: Size of the figure as a tuple (width, height)
            - colour: Colour for the crosswalks
            - alpha: Transparency level
            - linewidth: Width of the crosswalk lines
            - edgecolour: Colour for the edges of polygons (if applicable)

    Raises:
        ValueError: If no layer has been loaded yet.

    Examples:
        >>> crosswalks = Tile2NetCrosswalks().from_file("path/to/crosswalks.geojson")
        >>> crosswalks.static_render(figsize=(10, 8), colour="yellow", linewidth=1.5)
    """
    self.layer.plot(**plot_kwargs)

preview(format='ascii')

Generate a preview of this urban layer.

Produces a textual or structured representation of the Tile2NetCrosswalks layer for quick inspection, including metadata like the coordinate reference system and mappings.

Parameters:

Name Type Description Default
format str

Output format for the preview (default: "ascii"). - "ascii": Text-based format for terminal display - "json": JSON-formatted data for programmatic use

'ascii'

Returns:

Type Description
Any

str | dict: A string (for "ascii") or dictionary (for "json") representing the crosswalk network layer.

Raises:

Type Description
ValueError

If an unsupported format is requested.

Examples:

>>> crosswalks = Tile2NetCrosswalks().from_file("path/to/crosswalks.geojson")
>>> print(crosswalks.preview())
>>> # JSON preview
>>> import json
>>> print(json.dumps(crosswalks.preview(format="json"), indent=2))
Source code in src/urban_mapper/modules/urban_layer/urban_layers/tile2net_crosswalks.py
def preview(self, format: str = "ascii") -> Any:
    """Generate a preview of this urban layer.

    Produces a textual or structured representation of the `Tile2NetCrosswalks` layer for
    quick inspection, including metadata like the coordinate reference system and mappings.

    Args:
        format (str): Output format for the preview (default: "ascii").
            - "ascii": Text-based format for terminal display
            - "json": JSON-formatted data for programmatic use

    Returns:
        str | dict: A string (for "ascii") or dictionary (for "json") representing the
            crosswalk network layer.

    Raises:
        ValueError: If an unsupported format is requested.

    Examples:
        >>> crosswalks = Tile2NetCrosswalks().from_file("path/to/crosswalks.geojson")
        >>> print(crosswalks.preview())
        >>> # JSON preview
        >>> import json
        >>> print(json.dumps(crosswalks.preview(format="json"), indent=2))
    """
    mappings_str = (
        "\n".join(
            "Mapping:\n"
            f"    - lon={m.get('longitude_column', 'N/A')}, "
            f"lat={m.get('latitude_column', 'N/A')}, "
            f"output={m.get('output_column', 'N/A')}"
            for m in self.mappings
        )
        if self.mappings
        else "    No mappings"
    )
    if format == "ascii":
        return (
            f"Urban Layer: Tile2NetCrosswalks\n"
            f"  CRS: {self.coordinate_reference_system}\n"
            f"  Mappings:\n{mappings_str}"
        )
    elif format == "json":
        return {
            "urban_layer": "Tile2NetCrosswalks",
            "coordinate_reference_system": self.coordinate_reference_system,
            "mappings": self.mappings,
        }
    else:
        raise ValueError(f"Unsupported format '{format}'")

RegionNeighborhoods

Bases: AdminRegions

Urban layer implementation for neighbourhood-level administrative regions.

This class provides methods for loading neighbourhood boundaries from OpenStreetMap. It extends the AdminRegions base class, specifically targeting neighbourhood-level administrative divisions. The class automatically attempts to identify the appropriate administrative level for neighbourhoods in different regions, with an option for manual override.

Neighbourhoods are small, local administrative or cultural divisions typically found within cities. They often correspond to OpenStreetMap’s admin_level 10, though this can vary by country. The class employs heuristics to determine the correct level based on the size and number of divisions within a specified area.

When to Use?

Use this class when you need neighbourhood-level administrative boundaries for tasks such as urban planning, local governance studies, or providing geographic context for neighbourhood-specific datasets.

Attributes:

Name Type Description
division_type str

Set to "neighborhood" to instruct the parent class to look for neighbourhood-level administrative boundaries.

layer GeoDataFrame

The GeoDataFrame containing the neighbourhood boundaries (set after loading).

Examples:

Load and visualise neighbourhood boundaries for a city:

>>> from urban_mapper import UrbanMapper
>>> mapper = UrbanMapper()
>>> neighborhoods = mapper.urban_layer.region_neighborhoods().from_place("Brooklyn, NY")
>>> neighborhoods.static_render(
...     figsize=(10, 8),
...     edgecolor="black",
...     alpha=0.7,
...     column="name"  # Colour by neighbourhood name if available
... )

Specify a custom administrative level:

>>> neighborhoods = mapper.urban_layer.region_neighborhoods().from_place(
...     "Paris, France",
...     overwrite_admin_level="9"  # Use level 9 in Paris instead of the inferred level
... )

Administrative Level Inference

This class uses heuristics to infer the correct administrative level for neighbourhoods. However, as administrative levels vary globally, accuracy is not guaranteed. Use the overwrite_admin_level parameter in loading methods to manually specify the level if necessary.

Source code in src/urban_mapper/modules/urban_layer/urban_layers/region_neighborhoods.py
@beartype
class RegionNeighborhoods(AdminRegions):
    """Urban layer implementation for neighbourhood-level administrative regions.

    This class provides methods for loading neighbourhood boundaries from OpenStreetMap.
    It extends the `AdminRegions` base class, specifically targeting neighbourhood-level
    administrative divisions. The class automatically attempts to identify the appropriate
    administrative level for neighbourhoods in different regions, with an option for manual override.

    Neighbourhoods are small, local administrative or cultural divisions typically found within
    cities. They often correspond to OpenStreetMap’s `admin_level` 10, though this can vary by country.
    The class employs heuristics to determine the correct level based on the size and number of divisions
    within a specified area.

    !!! tip "When to Use?"
        Use this class when you need neighbourhood-level administrative boundaries for tasks such as
        urban planning, local governance studies, or providing geographic context for neighbourhood-specific
        datasets.

    Attributes:
        division_type (str): Set to "neighborhood" to instruct the parent class to look for
            neighbourhood-level administrative boundaries.
        layer (GeoDataFrame): The GeoDataFrame containing the neighbourhood boundaries (set after loading).

    Examples:
        Load and visualise neighbourhood boundaries for a city:
        >>> from urban_mapper import UrbanMapper
        >>> mapper = UrbanMapper()
        >>> neighborhoods = mapper.urban_layer.region_neighborhoods().from_place("Brooklyn, NY")
        >>> neighborhoods.static_render(
        ...     figsize=(10, 8),
        ...     edgecolor="black",
        ...     alpha=0.7,
        ...     column="name"  # Colour by neighbourhood name if available
        ... )

        Specify a custom administrative level:
        >>> neighborhoods = mapper.urban_layer.region_neighborhoods().from_place(
        ...     "Paris, France",
        ...     overwrite_admin_level="9"  # Use level 9 in Paris instead of the inferred level
        ... )

    !!! note "Administrative Level Inference"
        This class uses heuristics to infer the correct administrative level for neighbourhoods.
        However, as administrative levels vary globally, accuracy is not guaranteed. Use the
        `overwrite_admin_level` parameter in loading methods to manually specify the level if necessary.
    """

    def __init__(self) -> None:
        super().__init__()
        self.division_type = "neighborhood"

RegionCities

Bases: AdminRegions

Urban layer implementation for city-level administrative regions.

This class facilitates the loading of city and municipal boundaries from OpenStreetMap, extending the AdminRegions base class to target city-level administrative divisions. It employs heuristics to automatically determine the appropriate administrative level for cities across different regions, with an option for manual override.

Cities and municipalities represent medium-sized administrative units, typically under local government jurisdiction. While they often correspond to OpenStreetMap’s admin_level 8, variations across countries are common. This class adapts to such differences by analysing the size and number of divisions within a specified area.

When to Use?

Employ this class when you need city-level administrative boundaries for tasks like urban analysis, municipal planning, population studies, or providing geographic context for city-specific datasets.

Attributes:

Name Type Description
division_type str

Fixed to "city", directing the parent class to target city-level administrative boundaries.

layer GeoDataFrame

Holds the city boundaries as a GeoDataFrame, populated after loading via methods like from_place.

Examples:

Load and visualise city boundaries for a region:

>>> from urban_mapper import UrbanMapper
>>> mapper = UrbanMapper()
>>> cities = mapper.urban_layer.region_cities().from_place("Greater Manchester, England")
>>> cities.static_render(
...     figsize=(12, 10),
...     edgecolor="black",
...     alpha=0.5,
...     column="name"  # Colour by city name if available
... )

Specify a custom administrative level:

>>> cities = mapper.urban_layer.region_cities().from_place(
...     "Bavaria, Germany",
...     overwrite_admin_level="6"  # Override inferred level with 6
... )

Administrative Level Inference

This class attempts to infer the correct administrative level for cities using heuristics. However, as levels differ globally, accuracy isn’t guaranteed. Use the overwrite_admin_level parameter in loading methods to manually set the level if needed.

Source code in src/urban_mapper/modules/urban_layer/urban_layers/region_cities.py
@beartype
class RegionCities(AdminRegions):
    """Urban layer implementation for city-level administrative regions.

    This class facilitates the loading of city and municipal boundaries from OpenStreetMap,
    extending the `AdminRegions` base class to target city-level administrative divisions.
    It employs heuristics to automatically determine the appropriate administrative level
    for cities across different regions, with an option for manual override.

    Cities and municipalities represent medium-sized administrative units, typically under
    local government jurisdiction. While they often correspond to OpenStreetMap’s
    `admin_level` 8, variations across countries are common. This class adapts to such
    differences by analysing the size and number of divisions within a specified area.

    !!! tip "When to Use?"
        Employ this class when you need city-level administrative boundaries for tasks like
        urban analysis, municipal planning, population studies, or providing geographic
        context for city-specific datasets.

    Attributes:
        division_type (str): Fixed to "city", directing the parent class to target city-level
            administrative boundaries.
        layer (GeoDataFrame): Holds the city boundaries as a `GeoDataFrame`, populated after
            loading via methods like `from_place`.

    Examples:
        Load and visualise city boundaries for a region:
        >>> from urban_mapper import UrbanMapper
        >>> mapper = UrbanMapper()
        >>> cities = mapper.urban_layer.region_cities().from_place("Greater Manchester, England")
        >>> cities.static_render(
        ...     figsize=(12, 10),
        ...     edgecolor="black",
        ...     alpha=0.5,
        ...     column="name"  # Colour by city name if available
        ... )

        Specify a custom administrative level:
        >>> cities = mapper.urban_layer.region_cities().from_place(
        ...     "Bavaria, Germany",
        ...     overwrite_admin_level="6"  # Override inferred level with 6
        ... )

    !!! note "Administrative Level Inference"
        This class attempts to infer the correct administrative level for cities using
        heuristics. However, as levels differ globally, accuracy isn’t guaranteed. Use the
        `overwrite_admin_level` parameter in loading methods to manually set the level if
        needed.
    """

    def __init__(self) -> None:
        super().__init__()
        self.division_type = "city"

RegionStates

Bases: AdminRegions

Urban layer implementation for state and province-level administrative regions.

This class provides methods for loading state and province boundaries from OpenStreetMap. It extends the AdminRegions base class, specifically targeting state-level administrative divisions. The class automatically attempts to identify the appropriate administrative level for states or provinces in different regions, with an option for manual override.

States and provinces are mid-level administrative units, typically found within countries. They often correspond to OpenStreetMap’s admin_level 4, though this can vary by country. The class employs heuristics to determine the correct level based on the size and number of divisions within a specified area.

When to Use?

Use this class when you need state or province-level administrative boundaries for tasks such as regional analysis, governance studies, or providing geographic context for state-specific datasets.

Attributes:

Name Type Description
division_type str

Set to "state" to instruct the parent class to look for state-level administrative boundaries.

layer GeoDataFrame

The GeoDataFrame containing the state boundaries (set after loading).

Examples:

Load and visualise state boundaries for a country:

>>> from urban_mapper import UrbanMapper
>>> mapper = UrbanMapper()
>>> states = mapper.urban_layer.region_states().from_place("United States")
>>> states.static_render(
...     figsize=(12, 8),
...     edgecolor="black",
...     alpha=0.5,
...     column="name"  # Colour by state name
... )

Specify a custom administrative level:

>>> states = mapper.urban_layer.region_states().from_place(
...     "Canada",
...     overwrite_admin_level="4"  # Explicitly use level 4 for Canadian provinces
... )

Administrative Level Inference

This class uses heuristics to infer the correct administrative level for states. However, as administrative levels vary globally, accuracy is not guaranteed. Use the overwrite_admin_level parameter in loading methods to manually specify the level if necessary.

Source code in src/urban_mapper/modules/urban_layer/urban_layers/region_states.py
@beartype
class RegionStates(AdminRegions):
    """Urban layer implementation for state and province-level administrative regions.

    This class provides methods for loading state and province boundaries from OpenStreetMap.
    It extends the `AdminRegions` base class, specifically targeting state-level administrative
    divisions. The class automatically attempts to identify the appropriate administrative
    level for states or provinces in different regions, with an option for manual override.

    States and provinces are mid-level administrative units, typically found within countries.
    They often correspond to OpenStreetMap’s `admin_level` 4, though this can vary by country.
    The class employs heuristics to determine the correct level based on the size and number
    of divisions within a specified area.

    !!! tip "When to Use?"
        Use this class when you need state or province-level administrative boundaries for
        tasks such as regional analysis, governance studies, or providing geographic context
        for state-specific datasets.

    Attributes:
        division_type (str): Set to "state" to instruct the parent class to look for
            state-level administrative boundaries.
        layer (GeoDataFrame): The GeoDataFrame containing the state boundaries (set after loading).

    Examples:
        Load and visualise state boundaries for a country:
        >>> from urban_mapper import UrbanMapper
        >>> mapper = UrbanMapper()
        >>> states = mapper.urban_layer.region_states().from_place("United States")
        >>> states.static_render(
        ...     figsize=(12, 8),
        ...     edgecolor="black",
        ...     alpha=0.5,
        ...     column="name"  # Colour by state name
        ... )

        Specify a custom administrative level:
        >>> states = mapper.urban_layer.region_states().from_place(
        ...     "Canada",
        ...     overwrite_admin_level="4"  # Explicitly use level 4 for Canadian provinces
        ... )

    !!! note "Administrative Level Inference"
        This class uses heuristics to infer the correct administrative level for states.
        However, as administrative levels vary globally, accuracy is not guaranteed. Use the
        `overwrite_admin_level` parameter in loading methods to manually specify the level
        if necessary.
    """

    def __init__(self) -> None:
        super().__init__()
        self.division_type = "state"

RegionCountries

Bases: AdminRegions

Urban layer implementation for country-level administrative regions.

This class provides methods for loading country boundaries from OpenStreetMap. It extends the AdminRegions base class, specifically targeting country-level administrative divisions. The class automatically attempts to identify the appropriate administrative level for countries in different contexts, with an option for manual override.

Countries represent the highest level of administrative divisions in most global datasets. They typically correspond to OpenStreetMap’s admin_level 2, though variations exist in some special cases. The class uses heuristics to determine the correct level based on the size and connectivity of boundaries within a specified area.

When to Use?

Use this class when you need country-level administrative boundaries for tasks such as global analysis, international comparisons, or providing geographic context for country-specific datasets.

Attributes:

Name Type Description
division_type str

Set to "country" to instruct the parent class to look for country-level administrative boundaries.

layer GeoDataFrame

The GeoDataFrame containing the country boundaries (set after loading).

Examples:

Load and visualise country boundaries for a continent:

>>> from urban_mapper import UrbanMapper
>>> mapper = UrbanMapper()
>>> countries = mapper.urban_layer.region_countries().from_place("Europe")
>>> countries.static_render(
...     figsize=(15, 10),
...     edgecolor="black",
...     alpha=0.6,
...     column="name"  # Colour by country name
... )

Specify a custom administrative level:

>>> countries = mapper.urban_layer.region_countries().from_place(
...     "World",
...     overwrite_admin_level="2"  # Explicitly use level 2 for countries
... )

Administrative Level Inference

This class uses heuristics to infer the correct administrative level for countries. However, as administrative levels can vary, accuracy is not guaranteed. Use the overwrite_admin_level parameter in loading methods to manually specify the level if necessary.

Source code in src/urban_mapper/modules/urban_layer/urban_layers/region_countries.py
@beartype
class RegionCountries(AdminRegions):
    """Urban layer implementation for country-level administrative regions.

    This class provides methods for loading country boundaries from OpenStreetMap.
    It extends the `AdminRegions` base class, specifically targeting country-level
    administrative divisions. The class automatically attempts to identify the appropriate
    administrative level for countries in different contexts, with an option for manual override.

    Countries represent the highest level of administrative divisions in most global datasets.
    They typically correspond to OpenStreetMap’s `admin_level` 2, though variations exist in
    some special cases. The class uses heuristics to determine the correct level based on the
    size and connectivity of boundaries within a specified area.

    !!! tip "When to Use?"
        Use this class when you need country-level administrative boundaries for tasks such as
        global analysis, international comparisons, or providing geographic context for country-specific
        datasets.

    Attributes:
        division_type (str): Set to "country" to instruct the parent class to look for
            country-level administrative boundaries.
        layer (GeoDataFrame): The GeoDataFrame containing the country boundaries (set after loading).

    Examples:
        Load and visualise country boundaries for a continent:
        >>> from urban_mapper import UrbanMapper
        >>> mapper = UrbanMapper()
        >>> countries = mapper.urban_layer.region_countries().from_place("Europe")
        >>> countries.static_render(
        ...     figsize=(15, 10),
        ...     edgecolor="black",
        ...     alpha=0.6,
        ...     column="name"  # Colour by country name
        ... )

        Specify a custom administrative level:
        >>> countries = mapper.urban_layer.region_countries().from_place(
        ...     "World",
        ...     overwrite_admin_level="2"  # Explicitly use level 2 for countries
        ... )

    !!! note "Administrative Level Inference"
        This class uses heuristics to infer the correct administrative level for countries.
        However, as administrative levels can vary, accuracy is not guaranteed. Use the
        `overwrite_admin_level` parameter in loading methods to manually specify the level
        if necessary.
    """

    def __init__(self) -> None:
        super().__init__()
        self.division_type = "country"

CustomUrbanLayer

Bases: UrbanLayerBase

urban_layer implementation for user-defined spatial layer.

This class allows users to create urban layers from their own spatial information, either by loading from files (shapefiles, GeoJSON) or by using existing urban_layers from other libraries, if any. It implements the UrbanLayerBase interface, making it compatible with other UrbanMapper components like filters, enrichers, and pipelines.

Why use CustomUrbanLayer?

Custom urban_layers are useful for incorporating domain-specific spatial data that may not be available through standard APIs, such as:

  • Local government datasets
  • Research-specific spatial information
  • Historical or projected data
  • Specialised analysis results
  • None of the other urban_layers solved your need

Attributes:

Name Type Description
layer GeoDataFrame | None

The GeoDataFrame containing the custom spatial data (set after loading).

source str | None

String indicating how the layer was loaded ("file" or "urban_layer").

Examples:

>>> from urban_mapper import UrbanMapper
>>>
>>> # Initialise UrbanMapper
>>> mapper = UrbanMapper()
>>>
>>> # Load data from a GeoJSON file
>>> custom_data = mapper.urban_layer.custom_urban_layer().from_file("path/to/data.geojson")
>>>
>>> # Or create from an existing urban layer
>>> neighborhoods = mapper.urban_layer.region_neighborhoods().from_place("Brooklyn, NY")
>>> custom_layer = mapper.urban_layer.custom_urban_layer().from_urban_layer(neighborhoods)
Source code in src/urban_mapper/modules/urban_layer/urban_layers/custom_urban_layer.py
@beartype
class CustomUrbanLayer(UrbanLayerBase):
    """`urban_layer` implementation for user-defined spatial layer.

    This class allows users to create `urban layers` from their own spatial information,
    either by loading from files (`shapefiles`, `GeoJSON`) or by using existing
    `urban_layer`s from other libraries, if any. It implements the `UrbanLayerBase interface`, making it compatible
    with other `UrbanMapper` components like `filters`, `enrichers`, and `pipelines`.

    !!! note "Why use CustomUrbanLayer?"
        Custom `urban_layer`s are useful for incorporating domain-specific spatial data
        that may not be available through standard `APIs`, such as:

        - [x] Local government datasets
        - [x] Research-specific spatial information
        - [x] Historical or projected data
        - [x] Specialised analysis results
        - [x] None of the other `urban_layer`s solved your need

    Attributes:
        layer: The `GeoDataFrame` containing the custom spatial data (set after loading).
        source: String indicating how the layer was loaded ("file" or "urban_layer").

    Examples:
        >>> from urban_mapper import UrbanMapper
        >>>
        >>> # Initialise UrbanMapper
        >>> mapper = UrbanMapper()
        >>>
        >>> # Load data from a GeoJSON file
        >>> custom_data = mapper.urban_layer.custom_urban_layer().from_file("path/to/data.geojson")
        >>>
        >>> # Or create from an existing urban layer
        >>> neighborhoods = mapper.urban_layer.region_neighborhoods().from_place("Brooklyn, NY")
        >>> custom_layer = mapper.urban_layer.custom_urban_layer().from_urban_layer(neighborhoods)
    """

    def __init__(self) -> None:
        super().__init__()
        self.source: str | None = None

    def from_file(self, file_path: str | Path, **kwargs) -> "CustomUrbanLayer":
        """Load custom spatial data from a file.

        This method reads spatial data from a `shapefile` (.shp) or `GeoJSON` (.geojson) file
        and prepares it for use as an `urban_layer`. The data is automatically converted
        to the default `coordinate reference system`, i.e `EPSG:4326` (WGS 84),

        Args:
            file_path: Path to the file containing spatial data. Must be a `shapefile`
                or `GeoJSON` file.
            **kwargs: Additional parameters passed to gpd.read_file().

        Returns:
            Self, for method chaining.

        Raises:
            ValueError: If the file format is not supported or if the file doesn't
                contain a geometry column.
            FileNotFoundError: If the specified file does not exist.

        Examples:
            >>> custom_layer = CustomUrbanLayer().from_file("path/to/districts.geojson")
            >>> # Visualise the loaded data
            >>> custom_layer.static_render(figsize=(10, 8), column="district_name")
        """
        if not (str(file_path).endswith(".shp") or str(file_path).endswith(".geojson")):
            raise ValueError(
                "Only shapefiles (.shp) and GeoJSON (.geojson) are supported for loading from file."
            )

        self.layer = gpd.read_file(file_path)
        if self.layer.crs is None:
            self.layer.set_crs(DEFAULT_CRS, inplace=True)
        else:
            self.layer = self.layer.to_crs(DEFAULT_CRS)

        if "geometry" not in self.layer.columns:
            raise ValueError("The loaded file does not contain a geometry column.")

        self.source = "file"
        return self

    def from_urban_layer(
        self, urban_layer: UrbanLayerBase, **kwargs
    ) -> "CustomUrbanLayer":
        """Create a custom `urban layer` from an existing `urban layer`.

        This method creates a new custom `urban layer` by copying data from an
        existing `urban_layer`.

        !!! question "Why use this method?"
            This is useful in two scenarios:

            - [x] You do one urban analysis / pipeline workflow. The second one needs to have the results of the previous one (enriched urban layer).
            - [x] For transforming or extending standard `urban_layer`s with custom functionality.

        Args:
            urban_layer: An instance of `UrbanLayerBase` containing the data to copy.
            **kwargs: Additional parameters (not used).

        Returns:
            Self, for method chaining.

        Raises:
            ValueError: If the provided object is not a valid `urban_layer` or
                if the layer has no data.

        Examples:
            >>> # Get neighborhoods from standard layer
            >>> neighborhoods = UrbanMapper().urban_layer.region_neighborhoods().from_place("Chicago")
            >>> # Create custom layer from neighborhoods
            >>> custom = CustomUrbanLayer().from_urban_layer(neighborhoods)
            >>> # Now use custom layer with additional functionality / workflow
        """
        if not isinstance(urban_layer, UrbanLayerBase):
            raise ValueError(
                "The provided object is not an instance of UrbanLayerBase."
            )
        if urban_layer.layer is None:
            raise ValueError(
                "The provided urban layer has no data. Ensure it has been enriched or loaded."
            )

        self.layer = urban_layer.get_layer().copy()
        self.source = "urban_layer"
        return self

    def from_place(self, place_name: str, **kwargs) -> None:
        """Load custom data for a specific place.

        !!! danger "To Not Use – There For Consistency & Compatibility"
            This method is not currently implemented for `CustomUrbanLayer`, as custom
            layers require data to be loaded explicitly from files or other `urban_layer`s
            rather than from geocoded place names.
        """
        raise NotImplementedError(
            "Loading from place is not supported for CustomUrbanLayer."
        )

    @require_attributes_not_none(
        "layer",
        error_msg="Layer not loaded. Call from_file() or from_urban_layer() first.",
    )
    def _map_nearest_layer(
        self,
        data: gpd.GeoDataFrame,
        longitude_column: str,
        latitude_column: str,
        output_column: str = "nearest_feature",
        threshold_distance: float | None = None,
        _reset_layer_index: bool = True,
        **kwargs,
    ) -> Tuple[gpd.GeoDataFrame, gpd.GeoDataFrame]:
        """Map points to their `nearest features` in the `custom layer`.

        This internal method finds the `nearest feature` in the `custom layer` for each point
        in the input `GeoDataFrame` and adds a reference to that feature as a new column.

        It's primarily used by the `UrbanLayerBase.map_nearest_layer()` method to
        implement spatial joining between your dataset point data and custom urban layer's components.

        The method uses `GeoPandas`' spatial join with nearest match to find the
        closest feature for each point. If a threshold distance is specified,
        points beyond that distance will not be matched.

        Args:
            data: `GeoDataFrame` containing point data to map.
            longitude_column: Name of the column containing longitude values.
            latitude_column: Name of the column containing latitude values.
            output_column: Name of the column to store the indices of nearest features.
            threshold_distance: Maximum distance to consider a match, in the CRS units.
            _reset_layer_index: Whether to reset the index of the layer `GeoDataFrame`.
            **kwargs: Additional parameters (not used).

        Returns:
            A tuple containing:
                - The custom layer `GeoDataFrame` (possibly with reset index)
                - The input `GeoDataFrame` with the new output_column added
                  (filtered if threshold_distance was specified)

        !!! note "To Keep In Mind"

            - [x] The method automatically converts the input data to a projected CRS if
              it's not already projected, which is necessary for accurate distance
              calculations.
            - [x] Any duplicate indices in the result are removed to ensure a clean result.
        """
        dataframe = data.copy()

        if "geometry" not in dataframe.columns:
            dataframe = gpd.GeoDataFrame(
                dataframe,
                geometry=gpd.points_from_xy(
                    dataframe[longitude_column], dataframe[latitude_column]
                ),
                crs=self.coordinate_reference_system,
            )

        if not dataframe.crs.is_projected:
            utm_crs = dataframe.estimate_utm_crs()
            dataframe = dataframe.to_crs(utm_crs)
            layer_projected = self.layer.to_crs(utm_crs)
        else:
            layer_projected = self.layer

        mapped_data = gpd.sjoin_nearest(
            dataframe,
            layer_projected[["geometry"]],
            how="left",
            max_distance=threshold_distance,
            distance_col="distance_to_feature",
        )
        mapped_data[output_column] = mapped_data.index_right

        if mapped_data.index.duplicated().any():
            mapped_data = mapped_data.reset_index(drop=True)

        if _reset_layer_index:
            self.layer = self.layer.reset_index()

        return self.layer, mapped_data.drop(
            columns=["distance_to_feature", "index_right"], errors="ignore"
        )

    @require_attributes_not_none(
        "layer",
        error_msg="Layer not built. Call from_file() or from_urban_layer() first.",
    )
    def get_layer(self) -> gpd.GeoDataFrame:
        """Get the custom layer as a `GeoDataFrame`.

        This method returns the custom layer as a `GeoDataFrame`, which can be
        used for further analysis or visualisation purposes.

        Returns:
            `GeoDataFrame` containing the custom layer data.

        Raises:
            ValueError: If the layer has not been loaded yet.

        Examples:
            >>> custom_layer = CustomUrbanLayer().from_file("path/to/data.geojson")
            >>> custom_gdf = custom_layer.get_layer()
            >>> # Analyse the data
            >>> print(f"Layer has {len(custom_gdf)} features")
        """
        return self.layer

    @require_attributes_not_none(
        "layer",
        error_msg="Layer not built. Call from_file() or from_urban_layer() first.",
    )
    def get_layer_bounding_box(self) -> Tuple[float, float, float, float]:
        """Get the `bounding box` of the `custom layer`.

        This method returns the `bounding box` coordinates of the `custom layer`,
        which can be used for spatial queries, visualisation extents, or other
        geospatial operations.

        Returns:
            Tuple of (`left`, `bottom`, `right`, `top`) coordinates defining the bounding box.

        Raises:
            ValueError: If the layer has not been loaded yet.

        Examples:
            >>> custom_layer = CustomUrbanLayer().from_file("path/to/data.geojson")
            >>> bbox = custom_layer.get_layer_bounding_box()
            >>> print(f"Layer covers area: {bbox}")
        """
        return tuple(self.layer.total_bounds)  # type: ignore

    @require_attributes_not_none(
        "layer",
        error_msg="Layer not built. Call from_file() or from_urban_layer() first.",
    )
    def static_render(self, **plot_kwargs) -> None:
        """Render the `custom layer` as a `static plot`.

        This method creates a static visualisation of the custom layer using
        `GeoPandas`' plotting functionality. The plot is displayed immediately.

        Args:
            **plot_kwargs: Additional keyword arguments to pass to GeoDataFrame.plot().
                Common options include:

                - [x] figsize: Size of the figure as a tuple (width, height)
                - [x] column: Name of the column to use for coloring features
                - [x] cmap: Colormap to use for visualisation
                - [x] alpha: Transparency level
                - [x] edgecolor: Color for the edges of polygons

        Raises:
            ValueError: If no layer has been loaded yet.

        Examples:
            >>> custom_layer = CustomUrbanLayer().from_file("path/to/districts.geojson")
            >>> # Create a choropleth map by population
            >>> custom_layer.static_render(
            ...     figsize=(10, 8),
            ...     column="population",
            ...     cmap="viridis",
            ...     legend=True
            ... )
        """
        self.layer.plot(**plot_kwargs)

    def preview(self, format: str = "ascii") -> Any:
        """Generate a preview of this `urban_layer`.

        This method creates a textual or structured representation of the `CustomUrbanLayer`
        for quick inspection. It includes metadata about the layer such as its `source`,
        `coordinate reference system`, and `any mappings` that have been defined.

        Args:
            format: The output format for the preview (default: "ascii").

                - [x] "ascii": Text-based format for terminal display
                - [x] "json": JSON-formatted data for programmatic use

        Returns:
            A string (for `ASCII` format) or dictionary (for `JSON` format) representing
            the `custom layer`.

        Raises:
            ValueError: If an unsupported format is requested.

        Examples:
            >>> custom_layer = CustomUrbanLayer().from_file("path/to/data.geojson")
            >>> # ASCII preview
            >>> print(custom_layer.preview())
            >>> # JSON preview
            >>> import json
            >>> print(json.dumps(custom_layer.preview(format="json"), indent=2))
        """
        mappings_str = (
            "\n".join(
                f"    - lon={m.get('longitude_column', 'N/A')}, "
                f"lat={m.get('latitude_column', 'N/A')}, "
                f"output={m.get('output_column', 'N/A')}"
                for m in self.mappings
            )
            if self.mappings
            else "    No mappings"
        )
        if format == "ascii":
            return (
                f"Urban Layer: CustomUrbanLayer\n"
                f"  Source: {self.source or 'Not loaded'}\n"
                f"  CRS: {self.coordinate_reference_system}\n"
                f"  Mappings:\n{mappings_str}"
            )
        elif format == "json":
            return {
                "urban_layer": "CustomUrbanLayer",
                "source": self.source or "Not loaded",
                "coordinate_reference_system": self.coordinate_reference_system,
                "mappings": self.mappings,
            }
        else:
            raise ValueError(f"Unsupported format '{format}'")

from_file(file_path, **kwargs)

Load custom spatial data from a file.

This method reads spatial data from a shapefile (.shp) or GeoJSON (.geojson) file and prepares it for use as an urban_layer. The data is automatically converted to the default coordinate reference system, i.e EPSG:4326 (WGS 84),

Parameters:

Name Type Description Default
file_path str | Path

Path to the file containing spatial data. Must be a shapefile or GeoJSON file.

required
**kwargs

Additional parameters passed to gpd.read_file().

{}

Returns:

Type Description
CustomUrbanLayer

Self, for method chaining.

Raises:

Type Description
ValueError

If the file format is not supported or if the file doesn't contain a geometry column.

FileNotFoundError

If the specified file does not exist.

Examples:

>>> custom_layer = CustomUrbanLayer().from_file("path/to/districts.geojson")
>>> # Visualise the loaded data
>>> custom_layer.static_render(figsize=(10, 8), column="district_name")
Source code in src/urban_mapper/modules/urban_layer/urban_layers/custom_urban_layer.py
def from_file(self, file_path: str | Path, **kwargs) -> "CustomUrbanLayer":
    """Load custom spatial data from a file.

    This method reads spatial data from a `shapefile` (.shp) or `GeoJSON` (.geojson) file
    and prepares it for use as an `urban_layer`. The data is automatically converted
    to the default `coordinate reference system`, i.e `EPSG:4326` (WGS 84),

    Args:
        file_path: Path to the file containing spatial data. Must be a `shapefile`
            or `GeoJSON` file.
        **kwargs: Additional parameters passed to gpd.read_file().

    Returns:
        Self, for method chaining.

    Raises:
        ValueError: If the file format is not supported or if the file doesn't
            contain a geometry column.
        FileNotFoundError: If the specified file does not exist.

    Examples:
        >>> custom_layer = CustomUrbanLayer().from_file("path/to/districts.geojson")
        >>> # Visualise the loaded data
        >>> custom_layer.static_render(figsize=(10, 8), column="district_name")
    """
    if not (str(file_path).endswith(".shp") or str(file_path).endswith(".geojson")):
        raise ValueError(
            "Only shapefiles (.shp) and GeoJSON (.geojson) are supported for loading from file."
        )

    self.layer = gpd.read_file(file_path)
    if self.layer.crs is None:
        self.layer.set_crs(DEFAULT_CRS, inplace=True)
    else:
        self.layer = self.layer.to_crs(DEFAULT_CRS)

    if "geometry" not in self.layer.columns:
        raise ValueError("The loaded file does not contain a geometry column.")

    self.source = "file"
    return self

from_urban_layer(urban_layer, **kwargs)

Create a custom urban layer from an existing urban layer.

This method creates a new custom urban layer by copying data from an existing urban_layer.

Why use this method?

This is useful in two scenarios:

  • You do one urban analysis / pipeline workflow. The second one needs to have the results of the previous one (enriched urban layer).
  • For transforming or extending standard urban_layers with custom functionality.

Parameters:

Name Type Description Default
urban_layer UrbanLayerBase

An instance of UrbanLayerBase containing the data to copy.

required
**kwargs

Additional parameters (not used).

{}

Returns:

Type Description
CustomUrbanLayer

Self, for method chaining.

Raises:

Type Description
ValueError

If the provided object is not a valid urban_layer or if the layer has no data.

Examples:

>>> # Get neighborhoods from standard layer
>>> neighborhoods = UrbanMapper().urban_layer.region_neighborhoods().from_place("Chicago")
>>> # Create custom layer from neighborhoods
>>> custom = CustomUrbanLayer().from_urban_layer(neighborhoods)
>>> # Now use custom layer with additional functionality / workflow
Source code in src/urban_mapper/modules/urban_layer/urban_layers/custom_urban_layer.py
def from_urban_layer(
    self, urban_layer: UrbanLayerBase, **kwargs
) -> "CustomUrbanLayer":
    """Create a custom `urban layer` from an existing `urban layer`.

    This method creates a new custom `urban layer` by copying data from an
    existing `urban_layer`.

    !!! question "Why use this method?"
        This is useful in two scenarios:

        - [x] You do one urban analysis / pipeline workflow. The second one needs to have the results of the previous one (enriched urban layer).
        - [x] For transforming or extending standard `urban_layer`s with custom functionality.

    Args:
        urban_layer: An instance of `UrbanLayerBase` containing the data to copy.
        **kwargs: Additional parameters (not used).

    Returns:
        Self, for method chaining.

    Raises:
        ValueError: If the provided object is not a valid `urban_layer` or
            if the layer has no data.

    Examples:
        >>> # Get neighborhoods from standard layer
        >>> neighborhoods = UrbanMapper().urban_layer.region_neighborhoods().from_place("Chicago")
        >>> # Create custom layer from neighborhoods
        >>> custom = CustomUrbanLayer().from_urban_layer(neighborhoods)
        >>> # Now use custom layer with additional functionality / workflow
    """
    if not isinstance(urban_layer, UrbanLayerBase):
        raise ValueError(
            "The provided object is not an instance of UrbanLayerBase."
        )
    if urban_layer.layer is None:
        raise ValueError(
            "The provided urban layer has no data. Ensure it has been enriched or loaded."
        )

    self.layer = urban_layer.get_layer().copy()
    self.source = "urban_layer"
    return self

from_place(place_name, **kwargs)

Load custom data for a specific place.

To Not Use – There For Consistency & Compatibility

This method is not currently implemented for CustomUrbanLayer, as custom layers require data to be loaded explicitly from files or other urban_layers rather than from geocoded place names.

Source code in src/urban_mapper/modules/urban_layer/urban_layers/custom_urban_layer.py
def from_place(self, place_name: str, **kwargs) -> None:
    """Load custom data for a specific place.

    !!! danger "To Not Use – There For Consistency & Compatibility"
        This method is not currently implemented for `CustomUrbanLayer`, as custom
        layers require data to be loaded explicitly from files or other `urban_layer`s
        rather than from geocoded place names.
    """
    raise NotImplementedError(
        "Loading from place is not supported for CustomUrbanLayer."
    )

_map_nearest_layer(data, longitude_column, latitude_column, output_column='nearest_feature', threshold_distance=None, _reset_layer_index=True, **kwargs)

Map points to their nearest features in the custom layer.

This internal method finds the nearest feature in the custom layer for each point in the input GeoDataFrame and adds a reference to that feature as a new column.

It's primarily used by the UrbanLayerBase.map_nearest_layer() method to implement spatial joining between your dataset point data and custom urban layer's components.

The method uses GeoPandas' spatial join with nearest match to find the closest feature for each point. If a threshold distance is specified, points beyond that distance will not be matched.

Parameters:

Name Type Description Default
data GeoDataFrame

GeoDataFrame containing point data to map.

required
longitude_column str

Name of the column containing longitude values.

required
latitude_column str

Name of the column containing latitude values.

required
output_column str

Name of the column to store the indices of nearest features.

'nearest_feature'
threshold_distance float | None

Maximum distance to consider a match, in the CRS units.

None
_reset_layer_index bool

Whether to reset the index of the layer GeoDataFrame.

True
**kwargs

Additional parameters (not used).

{}

Returns:

Type Description
Tuple[GeoDataFrame, GeoDataFrame]

A tuple containing: - The custom layer GeoDataFrame (possibly with reset index) - The input GeoDataFrame with the new output_column added (filtered if threshold_distance was specified)

To Keep In Mind

  • The method automatically converts the input data to a projected CRS if it's not already projected, which is necessary for accurate distance calculations.
  • Any duplicate indices in the result are removed to ensure a clean result.
Source code in src/urban_mapper/modules/urban_layer/urban_layers/custom_urban_layer.py
@require_attributes_not_none(
    "layer",
    error_msg="Layer not loaded. Call from_file() or from_urban_layer() first.",
)
def _map_nearest_layer(
    self,
    data: gpd.GeoDataFrame,
    longitude_column: str,
    latitude_column: str,
    output_column: str = "nearest_feature",
    threshold_distance: float | None = None,
    _reset_layer_index: bool = True,
    **kwargs,
) -> Tuple[gpd.GeoDataFrame, gpd.GeoDataFrame]:
    """Map points to their `nearest features` in the `custom layer`.

    This internal method finds the `nearest feature` in the `custom layer` for each point
    in the input `GeoDataFrame` and adds a reference to that feature as a new column.

    It's primarily used by the `UrbanLayerBase.map_nearest_layer()` method to
    implement spatial joining between your dataset point data and custom urban layer's components.

    The method uses `GeoPandas`' spatial join with nearest match to find the
    closest feature for each point. If a threshold distance is specified,
    points beyond that distance will not be matched.

    Args:
        data: `GeoDataFrame` containing point data to map.
        longitude_column: Name of the column containing longitude values.
        latitude_column: Name of the column containing latitude values.
        output_column: Name of the column to store the indices of nearest features.
        threshold_distance: Maximum distance to consider a match, in the CRS units.
        _reset_layer_index: Whether to reset the index of the layer `GeoDataFrame`.
        **kwargs: Additional parameters (not used).

    Returns:
        A tuple containing:
            - The custom layer `GeoDataFrame` (possibly with reset index)
            - The input `GeoDataFrame` with the new output_column added
              (filtered if threshold_distance was specified)

    !!! note "To Keep In Mind"

        - [x] The method automatically converts the input data to a projected CRS if
          it's not already projected, which is necessary for accurate distance
          calculations.
        - [x] Any duplicate indices in the result are removed to ensure a clean result.
    """
    dataframe = data.copy()

    if "geometry" not in dataframe.columns:
        dataframe = gpd.GeoDataFrame(
            dataframe,
            geometry=gpd.points_from_xy(
                dataframe[longitude_column], dataframe[latitude_column]
            ),
            crs=self.coordinate_reference_system,
        )

    if not dataframe.crs.is_projected:
        utm_crs = dataframe.estimate_utm_crs()
        dataframe = dataframe.to_crs(utm_crs)
        layer_projected = self.layer.to_crs(utm_crs)
    else:
        layer_projected = self.layer

    mapped_data = gpd.sjoin_nearest(
        dataframe,
        layer_projected[["geometry"]],
        how="left",
        max_distance=threshold_distance,
        distance_col="distance_to_feature",
    )
    mapped_data[output_column] = mapped_data.index_right

    if mapped_data.index.duplicated().any():
        mapped_data = mapped_data.reset_index(drop=True)

    if _reset_layer_index:
        self.layer = self.layer.reset_index()

    return self.layer, mapped_data.drop(
        columns=["distance_to_feature", "index_right"], errors="ignore"
    )

get_layer()

Get the custom layer as a GeoDataFrame.

This method returns the custom layer as a GeoDataFrame, which can be used for further analysis or visualisation purposes.

Returns:

Type Description
GeoDataFrame

GeoDataFrame containing the custom layer data.

Raises:

Type Description
ValueError

If the layer has not been loaded yet.

Examples:

>>> custom_layer = CustomUrbanLayer().from_file("path/to/data.geojson")
>>> custom_gdf = custom_layer.get_layer()
>>> # Analyse the data
>>> print(f"Layer has {len(custom_gdf)} features")
Source code in src/urban_mapper/modules/urban_layer/urban_layers/custom_urban_layer.py
@require_attributes_not_none(
    "layer",
    error_msg="Layer not built. Call from_file() or from_urban_layer() first.",
)
def get_layer(self) -> gpd.GeoDataFrame:
    """Get the custom layer as a `GeoDataFrame`.

    This method returns the custom layer as a `GeoDataFrame`, which can be
    used for further analysis or visualisation purposes.

    Returns:
        `GeoDataFrame` containing the custom layer data.

    Raises:
        ValueError: If the layer has not been loaded yet.

    Examples:
        >>> custom_layer = CustomUrbanLayer().from_file("path/to/data.geojson")
        >>> custom_gdf = custom_layer.get_layer()
        >>> # Analyse the data
        >>> print(f"Layer has {len(custom_gdf)} features")
    """
    return self.layer

get_layer_bounding_box()

Get the bounding box of the custom layer.

This method returns the bounding box coordinates of the custom layer, which can be used for spatial queries, visualisation extents, or other geospatial operations.

Returns:

Type Description
Tuple[float, float, float, float]

Tuple of (left, bottom, right, top) coordinates defining the bounding box.

Raises:

Type Description
ValueError

If the layer has not been loaded yet.

Examples:

>>> custom_layer = CustomUrbanLayer().from_file("path/to/data.geojson")
>>> bbox = custom_layer.get_layer_bounding_box()
>>> print(f"Layer covers area: {bbox}")
Source code in src/urban_mapper/modules/urban_layer/urban_layers/custom_urban_layer.py
@require_attributes_not_none(
    "layer",
    error_msg="Layer not built. Call from_file() or from_urban_layer() first.",
)
def get_layer_bounding_box(self) -> Tuple[float, float, float, float]:
    """Get the `bounding box` of the `custom layer`.

    This method returns the `bounding box` coordinates of the `custom layer`,
    which can be used for spatial queries, visualisation extents, or other
    geospatial operations.

    Returns:
        Tuple of (`left`, `bottom`, `right`, `top`) coordinates defining the bounding box.

    Raises:
        ValueError: If the layer has not been loaded yet.

    Examples:
        >>> custom_layer = CustomUrbanLayer().from_file("path/to/data.geojson")
        >>> bbox = custom_layer.get_layer_bounding_box()
        >>> print(f"Layer covers area: {bbox}")
    """
    return tuple(self.layer.total_bounds)  # type: ignore

static_render(**plot_kwargs)

Render the custom layer as a static plot.

This method creates a static visualisation of the custom layer using GeoPandas' plotting functionality. The plot is displayed immediately.

Parameters:

Name Type Description Default
**plot_kwargs

Additional keyword arguments to pass to GeoDataFrame.plot(). Common options include:

  • figsize: Size of the figure as a tuple (width, height)
  • column: Name of the column to use for coloring features
  • cmap: Colormap to use for visualisation
  • alpha: Transparency level
  • edgecolor: Color for the edges of polygons
{}

Raises:

Type Description
ValueError

If no layer has been loaded yet.

Examples:

>>> custom_layer = CustomUrbanLayer().from_file("path/to/districts.geojson")
>>> # Create a choropleth map by population
>>> custom_layer.static_render(
...     figsize=(10, 8),
...     column="population",
...     cmap="viridis",
...     legend=True
... )
Source code in src/urban_mapper/modules/urban_layer/urban_layers/custom_urban_layer.py
@require_attributes_not_none(
    "layer",
    error_msg="Layer not built. Call from_file() or from_urban_layer() first.",
)
def static_render(self, **plot_kwargs) -> None:
    """Render the `custom layer` as a `static plot`.

    This method creates a static visualisation of the custom layer using
    `GeoPandas`' plotting functionality. The plot is displayed immediately.

    Args:
        **plot_kwargs: Additional keyword arguments to pass to GeoDataFrame.plot().
            Common options include:

            - [x] figsize: Size of the figure as a tuple (width, height)
            - [x] column: Name of the column to use for coloring features
            - [x] cmap: Colormap to use for visualisation
            - [x] alpha: Transparency level
            - [x] edgecolor: Color for the edges of polygons

    Raises:
        ValueError: If no layer has been loaded yet.

    Examples:
        >>> custom_layer = CustomUrbanLayer().from_file("path/to/districts.geojson")
        >>> # Create a choropleth map by population
        >>> custom_layer.static_render(
        ...     figsize=(10, 8),
        ...     column="population",
        ...     cmap="viridis",
        ...     legend=True
        ... )
    """
    self.layer.plot(**plot_kwargs)

preview(format='ascii')

Generate a preview of this urban_layer.

This method creates a textual or structured representation of the CustomUrbanLayer for quick inspection. It includes metadata about the layer such as its source, coordinate reference system, and any mappings that have been defined.

Parameters:

Name Type Description Default
format str

The output format for the preview (default: "ascii").

  • "ascii": Text-based format for terminal display
  • "json": JSON-formatted data for programmatic use
'ascii'

Returns:

Type Description
Any

A string (for ASCII format) or dictionary (for JSON format) representing

Any

the custom layer.

Raises:

Type Description
ValueError

If an unsupported format is requested.

Examples:

>>> custom_layer = CustomUrbanLayer().from_file("path/to/data.geojson")
>>> # ASCII preview
>>> print(custom_layer.preview())
>>> # JSON preview
>>> import json
>>> print(json.dumps(custom_layer.preview(format="json"), indent=2))
Source code in src/urban_mapper/modules/urban_layer/urban_layers/custom_urban_layer.py
def preview(self, format: str = "ascii") -> Any:
    """Generate a preview of this `urban_layer`.

    This method creates a textual or structured representation of the `CustomUrbanLayer`
    for quick inspection. It includes metadata about the layer such as its `source`,
    `coordinate reference system`, and `any mappings` that have been defined.

    Args:
        format: The output format for the preview (default: "ascii").

            - [x] "ascii": Text-based format for terminal display
            - [x] "json": JSON-formatted data for programmatic use

    Returns:
        A string (for `ASCII` format) or dictionary (for `JSON` format) representing
        the `custom layer`.

    Raises:
        ValueError: If an unsupported format is requested.

    Examples:
        >>> custom_layer = CustomUrbanLayer().from_file("path/to/data.geojson")
        >>> # ASCII preview
        >>> print(custom_layer.preview())
        >>> # JSON preview
        >>> import json
        >>> print(json.dumps(custom_layer.preview(format="json"), indent=2))
    """
    mappings_str = (
        "\n".join(
            f"    - lon={m.get('longitude_column', 'N/A')}, "
            f"lat={m.get('latitude_column', 'N/A')}, "
            f"output={m.get('output_column', 'N/A')}"
            for m in self.mappings
        )
        if self.mappings
        else "    No mappings"
    )
    if format == "ascii":
        return (
            f"Urban Layer: CustomUrbanLayer\n"
            f"  Source: {self.source or 'Not loaded'}\n"
            f"  CRS: {self.coordinate_reference_system}\n"
            f"  Mappings:\n{mappings_str}"
        )
    elif format == "json":
        return {
            "urban_layer": "CustomUrbanLayer",
            "source": self.source or "Not loaded",
            "coordinate_reference_system": self.coordinate_reference_system,
            "mappings": self.mappings,
        }
    else:
        raise ValueError(f"Unsupported format '{format}'")

UrbanLayerFactory

Factory for creating urban layer instances with a fluent chaining-method-based interface.

This factory class provides a chainable API for creating and configuring urban layer instances. It supports setting the layer type, loading method, mappings, and preview options before building the final urban layer instance.

The factory uses method chaining for a fluent, expressive API:

- [x] Set the layer type with `with_type()`
- [x] Set the loading method with `from_*()` methods
- [x] Add mappings with `with_mapping()`
- [x] Build the layer instance with `build()`

What are the various type available?

The available types are defined in the URBAN_LAYER_FACTORY dictionary. As follows:

  • streets_roads: OSMNXStreets
  • streets_intersections: OSMNXIntersections
  • streets_sidewalks: Tile2NetSidewalks
  • streets_crosswalks: Tile2NetCrosswalks
  • streets_features: OSMFeatures
  • region_cities: RegionCities
  • region_neighborhoods: RegionNeighborhoods
  • region_states: RegionStates
  • region_countries: RegionCountries
  • custom_urban_layer: CustomUrbanLayer

Attributes:

Name Type Description
layer_class Type[UrbanLayerBase] | None

The class of the urban layer to create.

loading_method str | None

The method to call to load the urban layer.

loading_args Tuple[object, ...]

Positional arguments for the loading method.

loading_kwargs Dict[str, object]

Keyword arguments for the loading method.

mappings List[Dict[str, object]]

List of mapping configurations for this layer.

Examples:

>>> from urban_mapper as um
>>> streets = um.UrbanMapper().urban_layer            ...     .with_type("streets_roads")            ...     .from_place("Manhattan, New York")            ...     .with_mapping(
...         longitude_column="pickup_lng",
...         latitude_column="pickup_lat",
...         output_column="nearest_street"
...     )            ...     .with_preview("ascii")            ...     .build()
Source code in src/urban_mapper/modules/urban_layer/urban_layer_factory.py
@beartype
class UrbanLayerFactory:
    """Factory for creating `urban layer` instances with a fluent chaining-method-based interface.

        This factory class provides a `chainable API` for creating and configuring
        `urban layer` instances. It supports setting the `layer type`, `loading method`,
        `mappings`, and `preview` options before `building` the final `urban layer` instance.

        The factory uses method chaining for a fluent, expressive API:

            - [x] Set the layer type with `with_type()`
            - [x] Set the loading method with `from_*()` methods
            - [x] Add mappings with `with_mapping()`
            - [x] Build the layer instance with `build()`

        !!! tip "What are the various type available?"
            The available types are defined in the `URBAN_LAYER_FACTORY` dictionary.
            As follows:

            - [x] `streets_roads`: OSMNXStreets
            - [x] `streets_intersections`: OSMNXIntersections
            - [x] `streets_sidewalks`: Tile2NetSidewalks
            - [x] `streets_crosswalks`: Tile2NetCrosswalks
            - [x] `streets_features`: OSMFeatures
            - [x] `region_cities`: RegionCities
            - [x] `region_neighborhoods`: RegionNeighborhoods
            - [x] `region_states`: RegionStates
            - [x] `region_countries`: RegionCountries
            - [x] `custom_urban_layer`: CustomUrbanLayer

        Attributes:
            layer_class: The class of the `urban layer` to create.
            loading_method: The method to call to load the `urban layer`.
            loading_args: Positional arguments for the loading method.
            loading_kwargs: Keyword arguments for the loading method.
            mappings: List of mapping configurations for this layer.

        Examples:
            >>> from urban_mapper as um
            >>> streets = um.UrbanMapper().urban_layer\
            ...     .with_type("streets_roads")\
            ...     .from_place("Manhattan, New York")\
            ...     .with_mapping(
            ...         longitude_column="pickup_lng",
            ...         latitude_column="pickup_lat",
            ...         output_column="nearest_street"
            ...     )\
            ...     .with_preview("ascii")\
            ...     .build()
        """

    def __init__(self):
        self.layer_class: Type[UrbanLayerBase] | None = None
        self.loading_method: str | None = None
        self.loading_args: Tuple[object, ...] = ()
        self.loading_kwargs: Dict[str, object] = {}
        self.mappings: List[Dict[str, object]] = []
        self._layer_recently_reset: bool = False
        self._instance: Optional[UrbanLayerBase] = None
        self._preview: Optional[dict] = None

    def with_type(self, primitive_type: str) -> "UrbanLayerFactory":
        """Set the type of `urban layer` to create.

        Args:
            primitive_type: String identifier for the `urban layer` type.
                Must be one of the registered layer types in `URBAN_LAYER_FACTORY`.

        Returns:
            Self, for method chaining.

        Raises:
            ValueError: If the provided type is not registered in `URBAN_LAYER_FACTORY`.
                Includes a suggestion if a similar type is available.

        Examples:
            >>> factory = UrbanLayerFactory().with_type("streets_roads")
        """
        if self.layer_class is not None:
            logger.log(
                "DEBUG_MID",
                f"Attribute 'layer_class' is being overwritten from {self.layer_class} to None. "
                f"Prior to most probably being set again by the method you are calling.",
            )
            self.layer_class = None
            self._layer_recently_reset = True
        if self.loading_method is not None:
            logger.log(
                "DEBUG_MID",
                f"Attribute 'loading_method' is being overwritten from {self.loading_method} to None. "
                f"Prior to most probably being set again by the method you are calling.",
            )
            self.loading_method = None

        from urban_mapper.modules.urban_layer import URBAN_LAYER_FACTORY

        if primitive_type not in URBAN_LAYER_FACTORY:
            available = list(URBAN_LAYER_FACTORY.keys())
            match, score = process.extractOne(primitive_type, available)
            if score > 80:
                suggestion = f" Maybe you meant '{match}'?"
            else:
                suggestion = ""
            raise ValueError(
                f"Unsupported layer type: {primitive_type}. Supported types: {', '.join(available)}.{suggestion}"
            )
        self.layer_class = URBAN_LAYER_FACTORY[primitive_type]
        logger.log(
            "DEBUG_LOW",
            f"WITH_TYPE: Initialised UrbanLayerFactory with layer_class={self.layer_class}",
        )
        return self

    def with_mapping(
        self,
        longitude_column: str | None = None,
        latitude_column: str | None = None,
        output_column: str | None = None,
        **mapping_kwargs,
    ) -> "UrbanLayerFactory":
        """Add a mapping configuration to the `urban layer`.

        Mappings define how the `urban layer` should be joined or related to other data.
        Each mapping specifies which columns contain the coordinates to map, and
        what the output column should be named.

        Args:
            longitude_column: Name of the column containing longitude values in the data
                to be mapped to this `urban layer`.
            latitude_column: Name of the column containing latitude values in the data
                to be mapped to this `urban layer`.
            output_column: Name of the column that will contain the mapping results.
                Must be unique across all mappings for this layer.
            **mapping_kwargs: Additional parameters specific to the mapping operation.
                Common parameters include `threshold_distance`, `max_distance`, etc.

        Returns:
            Self, for method chaining.

        Raises:
            ValueError: If the `output_column` is already used in another mapping.

        Examples:
            >>> factory = um.UrbanMapper().urban_layer\
            ...     .with_type("streets_roads")\
            ...     .from_place("Manhattan, New York")\
            ...     .with_mapping(
            ...         longitude_column="pickup_lng",
            ...         latitude_column="pickup_lat",
            ...         output_column="nearest_street",
            ...         threshold_distance=100
            ...     )
        """
        if self._layer_recently_reset:
            logger.log(
                "DEBUG_MID",
                f"Attribute 'mappings' is being overwritten from {self.mappings} to []. "
                f"Prior to most probably being set again by the method you are calling.",
            )
            self.mappings = []
            self._layer_recently_reset = False

        if output_column in [m.get("output_column") for m in self.mappings]:
            raise ValueError(
                f"Output column '{output_column}' is already used in another mapping."
            )

        mapping = {}
        if longitude_column:
            mapping["longitude_column"] = longitude_column
        if latitude_column:
            mapping["latitude_column"] = latitude_column
        if output_column:
            mapping["output_column"] = output_column
        mapping["kwargs"] = mapping_kwargs

        self.mappings.append(mapping)
        logger.log(
            "DEBUG_LOW",
            f"WITH_MAPPING: Added mapping with output_column={output_column}",
        )
        return self

    def __getattr__(self, name: str):
        if name.startswith("from_"):

            def wrapper(*args, **kwargs):
                self.loading_method = name
                self.loading_args = args
                self.loading_kwargs = kwargs
                logger.log(
                    "DEBUG_LOW",
                    f"{name}: Initialised UrbanLayerFactory with args={args} and kwargs={kwargs}",
                )
                return self

            return wrapper
        raise AttributeError(f"'{self.__class__.__name__}' has no attribute '{name}'")

    @require_attributes_not_none(
        "layer_class",
        error_msg="Layer type must be set using with_type() before building.",
    )
    @require_attributes_not_none(
        "loading_method",
        error_msg="A loading method must be specified before building the layer.",
    )
    def build(self) -> UrbanLayerBase:
        """Build and return the configured `urban layer` instance.

        This method creates an instance of the specified `urban layer` class,
        calls the loading method with the specified arguments, and attaches
        any mappings that were added.

        Returns:
            An initialised `urban layer` instance of the specified type,
            loaded with the specified data and configured with the specified mappings.

        Raises:
            ValueError: If `layer_class` or `loading_method` is not set, or if the
                loading method is not available for the specified layer class.

        Examples:
            >>> streets = um.UrbanMapper().urban_layer\
            ...     .with_type("osmnx_streets")\
            ...     .from_place("Manhattan, New York")\
            ...     .with_mapping(
            ...         longitude_column="pickup_lng",
            ...         latitude_column="pickup_lat",
            ...         output_column="nearest_street"
            ...     )\
            ...     .build()
        """
        layer = self.layer_class()
        if not hasattr(layer, self.loading_method):
            raise ValueError(
                f"'{self.loading_method}' is not available for {self.layer_class.__name__}"
            )
        loading_func = getattr(layer, self.loading_method)
        loading_func(*self.loading_args, **self.loading_kwargs)
        layer.mappings = self.mappings
        self._instance = layer
        if self._preview is not None:
            self.preview(format=self._preview["format"])
        return layer

    def preview(self, format: str = "ascii") -> None:
        """Display a preview of the built `urban layer`.

        This method generates and displays a preview of the `urban layer` instance
        that was created by the `build()` method.

        Args:
            format: The output format for the preview ("ascii" or "json").

        Raises:
            ValueError: If an unsupported format is requested.

        Examples:
            >>> streets = um.UrbanMapper().urban_layer\
            ...     .with_type("osmnx_streets")\
            ...     .from_place("Manhattan, New York")\
            ...     .build()
            >>> streets.preview()
        """
        if self._instance is None:
            print("No urban layer instance available to preview. Call build() first.")
            return

        if hasattr(self._instance, "preview"):
            preview_data = self._instance.preview(format=format)
            if format == "ascii":
                print(preview_data)
            elif format == "json":
                print(json.dumps(preview_data, indent=2))
            else:
                raise ValueError(f"Unsupported format '{format}'.")
        else:
            print("Preview not supported for this urban layer instance.")

    def with_preview(self, format: str = "ascii") -> "UrbanLayerFactory":
        """Enable automatic preview after building the `urban layer`.

        This method sets up the factory to automatically display a preview
        of the `urban layer` instance after it is built with `build()`.

        Args:
            format: The output format for the preview ("ascii" or "json").

        Returns:
            Self, for method chaining.

        Examples:
            >>> streets = UrbanLayerFactory()\
            ...     .with_type("osmnx_streets")\
            ...     .from_place("Manhattan, New York")\
            ...     .with_preview("json")\
            ...     .build()
        """
        self._preview = {"format": format}
        return self

with_type(primitive_type)

Set the type of urban layer to create.

Parameters:

Name Type Description Default
primitive_type str

String identifier for the urban layer type. Must be one of the registered layer types in URBAN_LAYER_FACTORY.

required

Returns:

Type Description
UrbanLayerFactory

Self, for method chaining.

Raises:

Type Description
ValueError

If the provided type is not registered in URBAN_LAYER_FACTORY. Includes a suggestion if a similar type is available.

Examples:

>>> factory = UrbanLayerFactory().with_type("streets_roads")
Source code in src/urban_mapper/modules/urban_layer/urban_layer_factory.py
def with_type(self, primitive_type: str) -> "UrbanLayerFactory":
    """Set the type of `urban layer` to create.

    Args:
        primitive_type: String identifier for the `urban layer` type.
            Must be one of the registered layer types in `URBAN_LAYER_FACTORY`.

    Returns:
        Self, for method chaining.

    Raises:
        ValueError: If the provided type is not registered in `URBAN_LAYER_FACTORY`.
            Includes a suggestion if a similar type is available.

    Examples:
        >>> factory = UrbanLayerFactory().with_type("streets_roads")
    """
    if self.layer_class is not None:
        logger.log(
            "DEBUG_MID",
            f"Attribute 'layer_class' is being overwritten from {self.layer_class} to None. "
            f"Prior to most probably being set again by the method you are calling.",
        )
        self.layer_class = None
        self._layer_recently_reset = True
    if self.loading_method is not None:
        logger.log(
            "DEBUG_MID",
            f"Attribute 'loading_method' is being overwritten from {self.loading_method} to None. "
            f"Prior to most probably being set again by the method you are calling.",
        )
        self.loading_method = None

    from urban_mapper.modules.urban_layer import URBAN_LAYER_FACTORY

    if primitive_type not in URBAN_LAYER_FACTORY:
        available = list(URBAN_LAYER_FACTORY.keys())
        match, score = process.extractOne(primitive_type, available)
        if score > 80:
            suggestion = f" Maybe you meant '{match}'?"
        else:
            suggestion = ""
        raise ValueError(
            f"Unsupported layer type: {primitive_type}. Supported types: {', '.join(available)}.{suggestion}"
        )
    self.layer_class = URBAN_LAYER_FACTORY[primitive_type]
    logger.log(
        "DEBUG_LOW",
        f"WITH_TYPE: Initialised UrbanLayerFactory with layer_class={self.layer_class}",
    )
    return self

with_mapping(longitude_column=None, latitude_column=None, output_column=None, **mapping_kwargs)

Add a mapping configuration to the urban layer.

Mappings define how the urban layer should be joined or related to other data. Each mapping specifies which columns contain the coordinates to map, and what the output column should be named.

Parameters:

Name Type Description Default
longitude_column str | None

Name of the column containing longitude values in the data to be mapped to this urban layer.

None
latitude_column str | None

Name of the column containing latitude values in the data to be mapped to this urban layer.

None
output_column str | None

Name of the column that will contain the mapping results. Must be unique across all mappings for this layer.

None
**mapping_kwargs

Additional parameters specific to the mapping operation. Common parameters include threshold_distance, max_distance, etc.

{}

Returns:

Type Description
UrbanLayerFactory

Self, for method chaining.

Raises:

Type Description
ValueError

If the output_column is already used in another mapping.

Examples:

>>> factory = um.UrbanMapper().urban_layer            ...     .with_type("streets_roads")            ...     .from_place("Manhattan, New York")            ...     .with_mapping(
...         longitude_column="pickup_lng",
...         latitude_column="pickup_lat",
...         output_column="nearest_street",
...         threshold_distance=100
...     )
Source code in src/urban_mapper/modules/urban_layer/urban_layer_factory.py
def with_mapping(
    self,
    longitude_column: str | None = None,
    latitude_column: str | None = None,
    output_column: str | None = None,
    **mapping_kwargs,
) -> "UrbanLayerFactory":
    """Add a mapping configuration to the `urban layer`.

    Mappings define how the `urban layer` should be joined or related to other data.
    Each mapping specifies which columns contain the coordinates to map, and
    what the output column should be named.

    Args:
        longitude_column: Name of the column containing longitude values in the data
            to be mapped to this `urban layer`.
        latitude_column: Name of the column containing latitude values in the data
            to be mapped to this `urban layer`.
        output_column: Name of the column that will contain the mapping results.
            Must be unique across all mappings for this layer.
        **mapping_kwargs: Additional parameters specific to the mapping operation.
            Common parameters include `threshold_distance`, `max_distance`, etc.

    Returns:
        Self, for method chaining.

    Raises:
        ValueError: If the `output_column` is already used in another mapping.

    Examples:
        >>> factory = um.UrbanMapper().urban_layer\
        ...     .with_type("streets_roads")\
        ...     .from_place("Manhattan, New York")\
        ...     .with_mapping(
        ...         longitude_column="pickup_lng",
        ...         latitude_column="pickup_lat",
        ...         output_column="nearest_street",
        ...         threshold_distance=100
        ...     )
    """
    if self._layer_recently_reset:
        logger.log(
            "DEBUG_MID",
            f"Attribute 'mappings' is being overwritten from {self.mappings} to []. "
            f"Prior to most probably being set again by the method you are calling.",
        )
        self.mappings = []
        self._layer_recently_reset = False

    if output_column in [m.get("output_column") for m in self.mappings]:
        raise ValueError(
            f"Output column '{output_column}' is already used in another mapping."
        )

    mapping = {}
    if longitude_column:
        mapping["longitude_column"] = longitude_column
    if latitude_column:
        mapping["latitude_column"] = latitude_column
    if output_column:
        mapping["output_column"] = output_column
    mapping["kwargs"] = mapping_kwargs

    self.mappings.append(mapping)
    logger.log(
        "DEBUG_LOW",
        f"WITH_MAPPING: Added mapping with output_column={output_column}",
    )
    return self

build()

Build and return the configured urban layer instance.

This method creates an instance of the specified urban layer class, calls the loading method with the specified arguments, and attaches any mappings that were added.

Returns:

Type Description
UrbanLayerBase

An initialised urban layer instance of the specified type,

UrbanLayerBase

loaded with the specified data and configured with the specified mappings.

Raises:

Type Description
ValueError

If layer_class or loading_method is not set, or if the loading method is not available for the specified layer class.

Examples:

>>> streets = um.UrbanMapper().urban_layer            ...     .with_type("osmnx_streets")            ...     .from_place("Manhattan, New York")            ...     .with_mapping(
...         longitude_column="pickup_lng",
...         latitude_column="pickup_lat",
...         output_column="nearest_street"
...     )            ...     .build()
Source code in src/urban_mapper/modules/urban_layer/urban_layer_factory.py
@require_attributes_not_none(
    "layer_class",
    error_msg="Layer type must be set using with_type() before building.",
)
@require_attributes_not_none(
    "loading_method",
    error_msg="A loading method must be specified before building the layer.",
)
def build(self) -> UrbanLayerBase:
    """Build and return the configured `urban layer` instance.

    This method creates an instance of the specified `urban layer` class,
    calls the loading method with the specified arguments, and attaches
    any mappings that were added.

    Returns:
        An initialised `urban layer` instance of the specified type,
        loaded with the specified data and configured with the specified mappings.

    Raises:
        ValueError: If `layer_class` or `loading_method` is not set, or if the
            loading method is not available for the specified layer class.

    Examples:
        >>> streets = um.UrbanMapper().urban_layer\
        ...     .with_type("osmnx_streets")\
        ...     .from_place("Manhattan, New York")\
        ...     .with_mapping(
        ...         longitude_column="pickup_lng",
        ...         latitude_column="pickup_lat",
        ...         output_column="nearest_street"
        ...     )\
        ...     .build()
    """
    layer = self.layer_class()
    if not hasattr(layer, self.loading_method):
        raise ValueError(
            f"'{self.loading_method}' is not available for {self.layer_class.__name__}"
        )
    loading_func = getattr(layer, self.loading_method)
    loading_func(*self.loading_args, **self.loading_kwargs)
    layer.mappings = self.mappings
    self._instance = layer
    if self._preview is not None:
        self.preview(format=self._preview["format"])
    return layer

preview(format='ascii')

Display a preview of the built urban layer.

This method generates and displays a preview of the urban layer instance that was created by the build() method.

Parameters:

Name Type Description Default
format str

The output format for the preview ("ascii" or "json").

'ascii'

Raises:

Type Description
ValueError

If an unsupported format is requested.

Examples:

>>> streets = um.UrbanMapper().urban_layer            ...     .with_type("osmnx_streets")            ...     .from_place("Manhattan, New York")            ...     .build()
>>> streets.preview()
Source code in src/urban_mapper/modules/urban_layer/urban_layer_factory.py
def preview(self, format: str = "ascii") -> None:
    """Display a preview of the built `urban layer`.

    This method generates and displays a preview of the `urban layer` instance
    that was created by the `build()` method.

    Args:
        format: The output format for the preview ("ascii" or "json").

    Raises:
        ValueError: If an unsupported format is requested.

    Examples:
        >>> streets = um.UrbanMapper().urban_layer\
        ...     .with_type("osmnx_streets")\
        ...     .from_place("Manhattan, New York")\
        ...     .build()
        >>> streets.preview()
    """
    if self._instance is None:
        print("No urban layer instance available to preview. Call build() first.")
        return

    if hasattr(self._instance, "preview"):
        preview_data = self._instance.preview(format=format)
        if format == "ascii":
            print(preview_data)
        elif format == "json":
            print(json.dumps(preview_data, indent=2))
        else:
            raise ValueError(f"Unsupported format '{format}'.")
    else:
        print("Preview not supported for this urban layer instance.")

with_preview(format='ascii')

Enable automatic preview after building the urban layer.

This method sets up the factory to automatically display a preview of the urban layer instance after it is built with build().

Parameters:

Name Type Description Default
format str

The output format for the preview ("ascii" or "json").

'ascii'

Returns:

Type Description
UrbanLayerFactory

Self, for method chaining.

Examples:

>>> streets = UrbanLayerFactory()            ...     .with_type("osmnx_streets")            ...     .from_place("Manhattan, New York")            ...     .with_preview("json")            ...     .build()
Source code in src/urban_mapper/modules/urban_layer/urban_layer_factory.py
def with_preview(self, format: str = "ascii") -> "UrbanLayerFactory":
    """Enable automatic preview after building the `urban layer`.

    This method sets up the factory to automatically display a preview
    of the `urban layer` instance after it is built with `build()`.

    Args:
        format: The output format for the preview ("ascii" or "json").

    Returns:
        Self, for method chaining.

    Examples:
        >>> streets = UrbanLayerFactory()\
        ...     .with_type("osmnx_streets")\
        ...     .from_place("Manhattan, New York")\
        ...     .with_preview("json")\
        ...     .build()
    """
    self._preview = {"format": format}
    return self

AdminFeatures

Helper class for dealing with OpenStreetMap features.

What to understand from this class?

In a nutshell? You barely will be using this out at all, unless you create a new UrbanLayer that needs to load OpenStreetMap features. If not, you can skip reading.

This class provides methods for loading various types of features from OpenStreetMap using different spatial queries. Features can include amenities (restaurants, hospitals), buildings, infrastructure, natural features, and more, specified through OSM tags.

More can be found at: Map Features.

The class uses OSMnx's features_from_* methods to retrieve the data and store it in a GeoDataFrame. It supports loading features by place name, address, bounding box, point with radius, or custom polygon.

Attributes:

Name Type Description
_features GeoDataFrame | None

Internal GeoDataFrame containing the loaded OSM features. None until load() is called.

Examples:

>>> admin_features = AdminFeatures()
>>> # Load all restaurants in Manhattan
>>> tags = {"amenity": "restaurant"}
>>> admin_features.load("place", tags, query="Manhattan, New York")
>>> restaurants = admin_features.features
Source code in src/urban_mapper/modules/urban_layer/urban_layers/admin_features_.py
@beartype
class AdminFeatures:
    """Helper class for dealing with `OpenStreetMap features`.

    !!! warning "What to understand from this class?"
        In a nutshell? You barely will be using this out at all, unless you create a new
        `UrbanLayer` that needs to load `OpenStreetMap` features. If not, you can skip reading.

    This class provides methods for loading various types of features from `OpenStreetMap`
    using different spatial queries. `Features` can include `amenities` (`restaurants`, `hospitals`),
    `buildings`, `infrastructure`, `natural features`, and more, specified through `OSM tags`.

    More can be found at: [Map Features](https://wiki.openstreetmap.org/wiki/Map_features).

    The class uses `OSMnx`'s `features_from_*` methods to retrieve the data and store it in
    a `GeoDataFrame`. It supports loading features by `place name`, `address`, `bounding box`,
    point with radius, or custom polygon.

    Attributes:
        _features: Internal GeoDataFrame containing the loaded OSM features.
            None until load() is called.

    Examples:
        >>> admin_features = AdminFeatures()
        >>> # Load all restaurants in Manhattan
        >>> tags = {"amenity": "restaurant"}
        >>> admin_features.load("place", tags, query="Manhattan, New York")
        >>> restaurants = admin_features.features
    """

    def __init__(self) -> None:
        self._features: gpd.GeoDataFrame | None = None

    def load(
        self, method: str, tags: Dict[str, str | bool | dict | list], **kwargs
    ) -> None:
        """Load `OpenStreetMap` features using the specified method and tags.

        This method retrieves features from `OpenStreetMap` that match the provided
        tags, using one of several spatial query methods (`address`, `bbox`, `place`, `point`,
        or `polygon`). The specific parameters required depend on the method chosen.e

        Args:
            method: The spatial query method to use. One of:
                - "address": Load features around an address
                - "bbox": Load features within a bounding box
                - "place": Load features for a named place (city, neighborhood, etc.)
                - "point": Load features around a specific point
                - "polygon": Load features within a polygon
            tags: Dictionary specifying the OpenStreetMap tags to filter features.
                Examples:
                - {"amenity": "restaurant"} - All restaurants
                - {"building": True} - All buildings
                - {"leisure": ["park", "garden"]} - Parks and gardens
                - {"landuse": "residential"} - Residential areas
            **kwargs: Additional arguments specific to the chosen method:
                - address: Requires "address" (str) and "dist" (float)
                - bbox: Requires "bbox" (tuple of left, bottom, right, top)
                - place: Requires "query" (str)
                - point: Requires "center_point" (tuple of lat, lon) and "dist" (float)
                - polygon: Requires "polygon" (Shapely Polygon/MultiPolygon)
                - All methods: Optional "timeout" (int) for Overpass API timeout in seconds

        Raises:
            ValueError: If an invalid method is specified or required parameters are missing

        Examples:
            >>> # Load all parks in Brooklyn
            >>> admin_features = AdminFeatures()
            >>> admin_features.load(
            ...     "place",
            ...     {"leisure": "park"},
            ...     query="Brooklyn, New York"
            ... )

            >>> # Load all hospitals within 5km of a point
            >>> admin_features.load(
            ...     "point",
            ...     {"amenity": "hospital"},
            ...     center_point=(40.7128, -74.0060),  # New York City coordinates
            ...     dist=5000  # 5km radius
            ... )
        """
        method = method.lower()
        valid_methods = {"address", "bbox", "place", "point", "polygon"}
        if method not in valid_methods:
            raise ValueError(f"Invalid method. Choose from {valid_methods}")

        if "timeout" in kwargs:
            ox.settings.overpass_settings = f"[out:json][timeout:{kwargs['timeout']}]"

        if method == "address":
            if "address" not in kwargs or "dist" not in kwargs:
                raise ValueError("Method 'address' requires 'address' and 'dist'")
            self._features = ox.features_from_address(
                kwargs["address"], tags, kwargs["dist"]
            )
        elif method == "bbox":
            if "bbox" not in kwargs:
                raise ValueError("Method 'bbox' requires 'bbox'")
            bbox = kwargs["bbox"]
            if not isinstance(bbox, tuple) or len(bbox) != 4:
                raise ValueError("'bbox' must be a tuple of (left, bottom, right, top)")
            self._features = ox.features_from_bbox(bbox, tags)
        elif method == "place":
            if "query" not in kwargs:
                raise ValueError("Method 'place' requires 'query'")
            self._features = ox.features_from_place(kwargs["query"], tags)
        elif method == "point":
            if "center_point" not in kwargs or "dist" not in kwargs:
                raise ValueError("Method 'point' requires 'center_point' and 'dist'")
            self._features = ox.features_from_point(
                kwargs["center_point"], tags, kwargs["dist"]
            )
        elif method == "polygon":
            if "polygon" not in kwargs:
                raise ValueError("Method 'polygon' requires 'polygon'")
            polygon = kwargs["polygon"]
            if not isinstance(polygon, (Polygon, MultiPolygon)):
                raise ValueError("'polygon' must be a shapely Polygon or MultiPolygon")
            self._features = ox.features_from_polygon(polygon, tags)

    @property
    def features(self) -> gpd.GeoDataFrame:
        """Get the loaded `OpenStreetMap` features as a `GeoDataFrame`.

        This property provides access to the `OpenStreetMap` features that were
        loaded using the `load()` method. The returned `GeoDataFrame` contains
        geometries and attributes for all features that matched the specified tags.

        Returns:
            GeoDataFrame containing the loaded OpenStreetMap features.

        Raises:
            ValueError: If features have not been loaded yet.

        Examples:
            >>> admin_features = AdminFeatures()
            >>> admin_features.load("place", {"amenity": "school"}, query="Boston, MA")
            >>> schools = admin_features.features
            >>> print(f"Found {len(schools)} schools in Boston")
        """
        if self._features is None:
            raise ValueError("Features not loaded. Call load() first.")
        return self._features

features property

Get the loaded OpenStreetMap features as a GeoDataFrame.

This property provides access to the OpenStreetMap features that were loaded using the load() method. The returned GeoDataFrame contains geometries and attributes for all features that matched the specified tags.

Returns:

Type Description
GeoDataFrame

GeoDataFrame containing the loaded OpenStreetMap features.

Raises:

Type Description
ValueError

If features have not been loaded yet.

Examples:

>>> admin_features = AdminFeatures()
>>> admin_features.load("place", {"amenity": "school"}, query="Boston, MA")
>>> schools = admin_features.features
>>> print(f"Found {len(schools)} schools in Boston")

load(method, tags, **kwargs)

Load OpenStreetMap features using the specified method and tags.

This method retrieves features from OpenStreetMap that match the provided tags, using one of several spatial query methods (address, bbox, place, point, or polygon). The specific parameters required depend on the method chosen.e

Parameters:

Name Type Description Default
method str

The spatial query method to use. One of: - "address": Load features around an address - "bbox": Load features within a bounding box - "place": Load features for a named place (city, neighborhood, etc.) - "point": Load features around a specific point - "polygon": Load features within a polygon

required
tags Dict[str, str | bool | dict | list]

Dictionary specifying the OpenStreetMap tags to filter features. Examples: - {"amenity": "restaurant"} - All restaurants - {"building": True} - All buildings - {"leisure": ["park", "garden"]} - Parks and gardens - {"landuse": "residential"} - Residential areas

required
**kwargs

Additional arguments specific to the chosen method: - address: Requires "address" (str) and "dist" (float) - bbox: Requires "bbox" (tuple of left, bottom, right, top) - place: Requires "query" (str) - point: Requires "center_point" (tuple of lat, lon) and "dist" (float) - polygon: Requires "polygon" (Shapely Polygon/MultiPolygon) - All methods: Optional "timeout" (int) for Overpass API timeout in seconds

{}

Raises:

Type Description
ValueError

If an invalid method is specified or required parameters are missing

Examples:

>>> # Load all parks in Brooklyn
>>> admin_features = AdminFeatures()
>>> admin_features.load(
...     "place",
...     {"leisure": "park"},
...     query="Brooklyn, New York"
... )
>>> # Load all hospitals within 5km of a point
>>> admin_features.load(
...     "point",
...     {"amenity": "hospital"},
...     center_point=(40.7128, -74.0060),  # New York City coordinates
...     dist=5000  # 5km radius
... )
Source code in src/urban_mapper/modules/urban_layer/urban_layers/admin_features_.py
def load(
    self, method: str, tags: Dict[str, str | bool | dict | list], **kwargs
) -> None:
    """Load `OpenStreetMap` features using the specified method and tags.

    This method retrieves features from `OpenStreetMap` that match the provided
    tags, using one of several spatial query methods (`address`, `bbox`, `place`, `point`,
    or `polygon`). The specific parameters required depend on the method chosen.e

    Args:
        method: The spatial query method to use. One of:
            - "address": Load features around an address
            - "bbox": Load features within a bounding box
            - "place": Load features for a named place (city, neighborhood, etc.)
            - "point": Load features around a specific point
            - "polygon": Load features within a polygon
        tags: Dictionary specifying the OpenStreetMap tags to filter features.
            Examples:
            - {"amenity": "restaurant"} - All restaurants
            - {"building": True} - All buildings
            - {"leisure": ["park", "garden"]} - Parks and gardens
            - {"landuse": "residential"} - Residential areas
        **kwargs: Additional arguments specific to the chosen method:
            - address: Requires "address" (str) and "dist" (float)
            - bbox: Requires "bbox" (tuple of left, bottom, right, top)
            - place: Requires "query" (str)
            - point: Requires "center_point" (tuple of lat, lon) and "dist" (float)
            - polygon: Requires "polygon" (Shapely Polygon/MultiPolygon)
            - All methods: Optional "timeout" (int) for Overpass API timeout in seconds

    Raises:
        ValueError: If an invalid method is specified or required parameters are missing

    Examples:
        >>> # Load all parks in Brooklyn
        >>> admin_features = AdminFeatures()
        >>> admin_features.load(
        ...     "place",
        ...     {"leisure": "park"},
        ...     query="Brooklyn, New York"
        ... )

        >>> # Load all hospitals within 5km of a point
        >>> admin_features.load(
        ...     "point",
        ...     {"amenity": "hospital"},
        ...     center_point=(40.7128, -74.0060),  # New York City coordinates
        ...     dist=5000  # 5km radius
        ... )
    """
    method = method.lower()
    valid_methods = {"address", "bbox", "place", "point", "polygon"}
    if method not in valid_methods:
        raise ValueError(f"Invalid method. Choose from {valid_methods}")

    if "timeout" in kwargs:
        ox.settings.overpass_settings = f"[out:json][timeout:{kwargs['timeout']}]"

    if method == "address":
        if "address" not in kwargs or "dist" not in kwargs:
            raise ValueError("Method 'address' requires 'address' and 'dist'")
        self._features = ox.features_from_address(
            kwargs["address"], tags, kwargs["dist"]
        )
    elif method == "bbox":
        if "bbox" not in kwargs:
            raise ValueError("Method 'bbox' requires 'bbox'")
        bbox = kwargs["bbox"]
        if not isinstance(bbox, tuple) or len(bbox) != 4:
            raise ValueError("'bbox' must be a tuple of (left, bottom, right, top)")
        self._features = ox.features_from_bbox(bbox, tags)
    elif method == "place":
        if "query" not in kwargs:
            raise ValueError("Method 'place' requires 'query'")
        self._features = ox.features_from_place(kwargs["query"], tags)
    elif method == "point":
        if "center_point" not in kwargs or "dist" not in kwargs:
            raise ValueError("Method 'point' requires 'center_point' and 'dist'")
        self._features = ox.features_from_point(
            kwargs["center_point"], tags, kwargs["dist"]
        )
    elif method == "polygon":
        if "polygon" not in kwargs:
            raise ValueError("Method 'polygon' requires 'polygon'")
        polygon = kwargs["polygon"]
        if not isinstance(polygon, (Polygon, MultiPolygon)):
            raise ValueError("'polygon' must be a shapely Polygon or MultiPolygon")
        self._features = ox.features_from_polygon(polygon, tags)

AdminRegions

Bases: OSMFeatures

Base class for administrative regions at various levels.

What to understand from this class?

In a nutshell? You barely will be using this out at all, unless you create a new UrbanLayer that needs to load OpenStreetMap features. If not, you can skip reading.

This abstract class provides shared functionality for loading and processing administrative boundaries from OpenStreetMap. It's designed to be subclassed for specific types of administrative regions (neighborhoods, cities, states, countries).

The class intelligently handles the complexities of OpenStreetMap's administrative levels, which vary across different countries and regions. It attempts to infer the appropriate level based on the type of administrative division requested, but also allows manual overriding of this inference.

Further can be read at: OpenStreetMap Wiki to understand why it is complex to infer the right admin_level.

Attributes:

Name Type Description
division_type str | None

The type of administrative division this layer represents (e.g., "neighborhood", "city", "state", "country").

tags Dict[str, str] | None

OpenStreetMap tags used to filter boundary features.

layer GeoDataFrame | None

The GeoDataFrame containing the administrative boundary data (set after loading).

Examples:

>>> # This is an abstract class - use concrete implementations like:
>>> from urban_mapper import UrbanMapper
>>> mapper = UrbanMapper()
>>> # For neighborhoods:
>>> neighborhoods = mapper.urban_layer.region_neighborhoods().from_place("Paris, France")
>>> # For cities:
>>> cities = mapper.urban_layer.region_cities().from_place("HΓ©rault, France")
Source code in src/urban_mapper/modules/urban_layer/urban_layers/admin_regions_.py
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
@beartype
class AdminRegions(OSMFeatures):
    """Base class for `administrative regions` at `various levels`.

    !!! warning "What to understand from this class?"
        In a nutshell? You barely will be using this out at all, unless you create a new
        `UrbanLayer` that needs to load `OpenStreetMap` features. If not, you can skip reading.

    This abstract class provides shared functionality for `loading` and `processing`
    `administrative boundaries` from `OpenStreetMap`. It's designed to be subclassed
    for specific types of administrative regions (`neighborhoods`, `cities`, `states`, `countries`).

    The class _intelligently_ handles the complexities of `OpenStreetMap's administrative
    levels`, which vary across different `countries` and `regions`. It attempts to infer
    the appropriate level based on the type of administrative division requested,
    but also allows manual overriding of this inference.

    Further can be read at: [OpenStreetMap Wiki](https://wiki.openstreetmap.org/wiki/Tag:boundary%3Dadministrative)
    to understand why it is complex to infer the right `admin_level`.

    Attributes:
        division_type: The type of administrative division this layer represents
            (e.g., "neighborhood", "city", "state", "country").
        tags: OpenStreetMap tags used to filter boundary features.
        layer: The GeoDataFrame containing the administrative boundary data (set after loading).

    Examples:
        >>> # This is an abstract class - use concrete implementations like:
        >>> from urban_mapper import UrbanMapper
        >>> mapper = UrbanMapper()
        >>> # For neighborhoods:
        >>> neighborhoods = mapper.urban_layer.region_neighborhoods().from_place("Paris, France")
        >>> # For cities:
        >>> cities = mapper.urban_layer.region_cities().from_place("HΓ©rault, France")
    """

    def __init__(self) -> None:
        super().__init__()
        self.division_type: str | None = None
        self.tags: Dict[str, str] | None = None

    def from_place(
        self, place_name: str, overwrite_admin_level: str | None = None, **kwargs
    ) -> None:
        """Load `administrative regions` for a named place.

        This method retrieves administrative boundaries for a specified place
        name from `OpenStreetMap`. It filters for the appropriate `administrative
        level` based on the division_type set for this layer, and can be manually
        overridden if needed.

        Args:
            place_name: Name of the place to load administrative regions for
                (e.g., "New York City", "Bavaria, Germany").
            overwrite_admin_level: Manually specify the OpenStreetMap admin_level
                to use instead of inferring it. Admin levels differ by region but
                typically follow patterns like:

                - [x] 2: Country
                - [x] 4: State/Province
                - [x] 6: County
                - [x] 8: City/Municipality
                - [x] 10: Neighborhood/Borough

                Feel free to look into [OSM Wiki](https://wiki.openstreetmap.org/wiki/Tag:boundary%3Dadministrative).

            **kwargs: Additional parameters passed to OSMnx's features_from_place.

        Returns:
            Self, for method chaining.

        Raises:
            ValueError: If division_type is not set or if no administrative
                boundaries are found for the specified place.

        Examples:
            >>> # Get neighborhoods in Manhattan
            >>> neighborhoods = AdminRegions()
            >>> neighborhoods.division_type = "neighborhood"
            >>> neighborhoods.from_place("Manhattan, New York")

            >>> # Override admin level for more control
            >>> cities = AdminRegions()
            >>> cities.division_type = "city"
            >>> cities.from_place("France", overwrite_admin_level="6")
        """
        if self.division_type is None:
            raise ValueError("Division type not set for this layer.")
        warnings.warn(
            "Administrative levels vary across regions. The system will infer the most appropriate admin_level "
            "based on the data and division type, but you can (and is recommended to) override it "
            "with 'overwrite_admin_level'."
        )
        geolocator = Nominatim(user_agent="urban_mapper")
        place_polygon = None
        try:
            location = geolocator.geocode(place_name, geometry="wkt")
            if location and "geotext" in location.raw:
                place_polygon = loads(location.raw["geotext"])
            else:
                logger.log(
                    "DEBUG_LOW", f"Geocoding for {place_name} did not return a polygon."
                )
        except Exception as e:
            logger.log(
                "DEBUG_LOW",
                f"Geocoding failed for {place_name}: {e}. Proceeding without polygon filtering.",
            )
        self.tags = {"boundary": "administrative"}
        self.feature_network = AdminFeatures()
        self.feature_network.load("place", self.tags, query=place_name, **kwargs)
        all_boundaries = self.feature_network.features.to_crs(
            self.coordinate_reference_system
        )
        if place_polygon:
            all_boundaries = all_boundaries[
                all_boundaries.geometry.within(place_polygon)
            ]
            if all_boundaries.empty:
                logger.log(
                    "DEBUG_LOW",
                    "No boundaries found within the geocoded polygon. Using all loaded boundaries.",
                )
                all_boundaries = self.feature_network.features.to_crs(
                    self.coordinate_reference_system
                )
        all_boundaries.reset_index(inplace=True)
        if (
            "element" in all_boundaries.columns
            and "relation" in all_boundaries["element"].unique()
        ):
            all_boundaries = all_boundaries[all_boundaries["element"] == "relation"]
        else:
            logger.log(
                "DEBUG_LOW",
                "No 'relation' found in 'element' column. Using all loaded boundaries.",
            )
        available_levels = all_boundaries["admin_level"].dropna().unique()
        if not available_levels.size:
            raise ValueError(f"No administrative boundaries found for {place_name}.")
        if overwrite_admin_level is not None:
            logger.log(
                "DEBUG_LOW", f"Admin level overridden to {overwrite_admin_level}."
            )
            if overwrite_admin_level not in available_levels:
                raise ValueError(
                    f"Overridden admin level {overwrite_admin_level} not found in available levels: {available_levels}."
                )
            admin_level = overwrite_admin_level
        else:
            inferred_level = self.infer_best_admin_level(
                all_boundaries.copy(), self.division_type
            )
            warnings.warn(
                f"Inferred admin_level for {self.division_type}: {inferred_level}. "
                f"Other available levels: {sorted(available_levels)}. "
                "You can override this with 'overwrite_admin_level' if desired."
            )
            admin_level = inferred_level
        self.layer = all_boundaries[
            all_boundaries["admin_level"] == admin_level
        ].to_crs(self.coordinate_reference_system)

    def from_address(
        self,
        address: str,
        dist: float,
        overwrite_admin_level: str | None = None,
        **kwargs,
    ) -> None:
        """Load `administrative regions` for a specific address.

        This method retrieves administrative boundaries for a specified address
        from `OpenStreetMap`. It filters for the appropriate `administrative
        level` based on the division_type set for this layer, and can be manually
        overridden if needed.

        Args:
            address: Address to load administrative regions for (e.g., "1600 Amphitheatre Parkway, Mountain View, CA").
            dist: Distance in meters to search around the address. Consider this a radius.
            overwrite_admin_level: Manually specify the OpenStreetMap admin_level
                to use instead of inferring it. Admin levels differ by region but
                typically follow patterns like:

                - [x] 2: Country
                - [x] 4: State/Province
                - [x] 6: County
                - [x] 8: City/Municipality
                - [x] 10: Neighborhood/Borough

                Feel free to look into [OSM Wiki](https://wiki.openstreetmap.org/wiki/Tag:boundary%3Dadministrative).

            **kwargs: Additional parameters passed to OSMnx's features_from_address.

        Returns:
            Self, for method chaining.

        Raises:
            ValueError: If division_type is not set or if no administrative
                boundaries are found for the specified address.

        Examples:
            >>> # Get neighborhoods around a specific address
            >>> neighborhoods = AdminRegions()
            >>> neighborhoods.division_type = "neighborhood"
            >>> neighborhoods.from_address("1600 Amphitheatre Parkway, Mountain View, CA", dist=500)

            >>> # Override admin level for more control
            >>> cities = AdminRegions()
            >>> cities.division_type = "city"
            >>> cities.from_address("1600 Amphitheatre Parkway, Mountain View, CA", dist=500, overwrite_admin_level="6")
        """

        if self.division_type is None:
            raise ValueError("Division type not set for this layer.")
        warnings.warn(
            "Administrative levels vary across regions. The system will infer the most appropriate admin_level "
            "based on the data and division type, but you can (and is recommended to) override it "
            "with 'overwrite_admin_level'."
        )
        geolocator = Nominatim(user_agent="urban_mapper")
        place_polygon = None
        try:
            location = geolocator.geocode(address, geometry="wkt")
            if location and "geotext" in location.raw:
                place_polygon = loads(location.raw["geotext"])
            else:
                logger.log(
                    "DEBUG_LOW", f"Geocoding for {address} did not return a polygon."
                )
        except Exception as e:
            logger.log(
                "DEBUG_LOW",
                f"Geocoding failed for {address}: {e}. Proceeding without polygon filtering.",
            )
        self.tags = {"boundary": "administrative"}
        self.feature_network = AdminFeatures()
        self.feature_network.load(
            "address", self.tags, address=address, dist=dist, **kwargs
        )
        all_boundaries = self.feature_network.features.to_crs(
            self.coordinate_reference_system
        )
        if place_polygon:
            all_boundaries = all_boundaries[
                all_boundaries.geometry.within(place_polygon)
            ]
            if all_boundaries.empty:
                logger.log(
                    "DEBUG_LOW",
                    "No boundaries found within the geocoded polygon. Using all loaded boundaries.",
                )
                all_boundaries = self.feature_network.features.to_crs(
                    self.coordinate_reference_system
                )
        all_boundaries.reset_index(inplace=True)
        if (
            "element" in all_boundaries.columns
            and "relation" in all_boundaries["element"].unique()
        ):
            all_boundaries = all_boundaries[all_boundaries["element"] == "relation"]
        else:
            logger.log(
                "DEBUG_LOW",
                "No 'relation' found in 'element' column. Using all loaded boundaries.",
            )
        available_levels = all_boundaries["admin_level"].dropna().unique()
        if not available_levels.size:
            raise ValueError(
                f"No administrative boundaries found for address {address}."
            )
        if overwrite_admin_level is not None:
            logger.log(
                "DEBUG_LOW", f"Admin level overridden to {overwrite_admin_level}."
            )
            if overwrite_admin_level not in available_levels:
                raise ValueError(
                    f"Overridden admin level {overwrite_admin_level} not found in available levels: {available_levels}."
                )
            admin_level = overwrite_admin_level
        else:
            inferred_level = self.infer_best_admin_level(
                all_boundaries.copy(), self.division_type
            )
            warnings.warn(
                f"Inferred admin_level for {self.division_type}: {inferred_level}. "
                f"Other available levels: {sorted(available_levels)}. "
                "You can override this with 'overwrite_admin_level' if desired."
            )
            admin_level = inferred_level
        self.layer = all_boundaries[
            all_boundaries["admin_level"] == admin_level
        ].to_crs(self.coordinate_reference_system)

    def from_polygon(
        self,
        polygon: Polygon | MultiPolygon,
        overwrite_admin_level: str | None = None,
        **kwargs,
    ) -> None:
        """Load `administrative regions` for a specific polygon.
        This method retrieves administrative boundaries for a specified polygon
        from `OpenStreetMap`. It filters for the appropriate `administrative
        level` based on the division_type set for this layer, and can be manually
        overridden if needed.

        Args:
            polygon: Shapely Polygon or MultiPolygon to load administrative regions for.
            overwrite_admin_level: Manually specify the OpenStreetMap admin_level
                to use instead of inferring it. Admin levels differ by region but
                typically follow patterns like:

                - [x] 2: Country
                - [x] 4: State/Province
                - [x] 6: County
                - [x] 8: City/Municipality
                - [x] 10: Neighborhood/Borough

                Feel free to look into [OSM Wiki](https://wiki.openstreetmap.org/wiki/Tag:boundary%3Dadministrative).

            **kwargs: Additional parameters passed to OSMnx's features_from_polygon.

        Returns:
            Self, for method chaining.

        Raises:
            ValueError: If division_type is not set or if no administrative
                boundaries are found for the specified polygon.

        Examples:
            >>> # Create a polygon out of an address for instance, with the help of geopy
            >>> from geopy.geocoders import Nominatim
            >>> geolocator = Nominatim(user_agent="urban_mapper")
            >>> location = geolocator.geocode("1600 Amphitheatre Parkway, Mountain View, CA", geometry="wkt")
            >>> polygon = loads(location.raw["geotext"])

            >>> # Get neighborhoods within a specific polygon
            >>> neighborhoods = AdminRegions()
            >>> neighborhoods.division_type = "neighborhood"
            >>> neighborhoods.from_polygon(polygon)

            >>> # Override admin level for more control
            >>> cities = AdminRegions()
            >>> cities.division_type = "neighborhood"
            >>> cities.from_polygon(polygon, overwrite_admin_level="8")
        """

        if self.division_type is None:
            raise ValueError("Division type not set for this layer.")
        warnings.warn(
            "Administrative levels vary across regions. The system will infer the most appropriate admin_level "
            "based on the data and division type, but you can (and is recommended to) override it "
            "with 'overwrite_admin_level'."
        )
        self.tags = {"boundary": "administrative"}
        self.feature_network = AdminFeatures()
        self.feature_network.load("polygon", self.tags, polygon=polygon, **kwargs)
        all_boundaries = self.feature_network.features.to_crs(
            self.coordinate_reference_system
        )
        all_boundaries = all_boundaries[all_boundaries.geometry.within(polygon)]
        if all_boundaries.empty:
            logger.log(
                "DEBUG_LOW",
                "No boundaries found within the provided polygon. Using all loaded boundaries.",
            )
            all_boundaries = self.feature_network.features.to_crs(
                self.coordinate_reference_system
            )
        all_boundaries.reset_index(inplace=True)
        if (
            "element" in all_boundaries.columns
            and "relation" in all_boundaries["element"].unique()
        ):
            all_boundaries = all_boundaries[all_boundaries["element"] == "relation"]
        else:
            logger.log(
                "DEBUG_LOW",
                "No 'relation' found in 'element' column. Using all loaded boundaries.",
            )
        available_levels = all_boundaries["admin_level"].dropna().unique()
        if not available_levels.size:
            raise ValueError(
                "No administrative boundaries found within the provided polygon."
            )
        if overwrite_admin_level is not None:
            logger.log(
                "DEBUG_LOW", f"Admin level overridden to {overwrite_admin_level}."
            )
            if overwrite_admin_level not in available_levels:
                raise ValueError(
                    f"Overridden admin level {overwrite_admin_level} not found in available levels: {available_levels}."
                )
            admin_level = overwrite_admin_level
        else:
            inferred_level = self.infer_best_admin_level(
                all_boundaries.copy(), self.division_type
            )
            warnings.warn(
                f"Inferred admin_level for {self.division_type}: {inferred_level}. "
                f"Other available levels: {sorted(available_levels)}. "
                "You can override this with 'overwrite_admin_level' if desired."
            )
            admin_level = inferred_level
        self.layer = all_boundaries[
            all_boundaries["admin_level"] == admin_level
        ].to_crs(self.coordinate_reference_system)

    def from_bbox(
        self,
        bbox: Tuple[float, float, float, float],
        overwrite_admin_level: str | None = None,
        **kwargs,
    ) -> None:
        """Load `administrative regions` for a specific bounding box.
        This method retrieves administrative boundaries for a specified bounding
        box from `OpenStreetMap`. It filters for the appropriate `administrative
        level` based on the division_type set for this layer, and can be manually
        overridden if needed.

        Args:
            bbox: Tuple of (left, bottom, right, top) coordinates defining the bounding box.
            overwrite_admin_level: Manually specify the OpenStreetMap admin_level
                to use instead of inferring it. Admin levels differ by region but
                typically follow patterns like:

                - [x] 2: Country
                - [x] 4: State/Province
                - [x] 6: County
                - [x] 8: City/Municipality
                - [x] 10: Neighborhood/Borough

                Feel free to look into [OSM Wiki](https://wiki.openstreetmap.org/wiki/Tag:boundary%3Dadministrative).

            **kwargs: Additional parameters passed to OSMnx's features_from_bbox.

        Returns:
            Self, for method chaining.

        Raises:
            ValueError: If division_type is not set or if no administrative
                boundaries are found for the specified bounding box.

        Examples:
            >>> # Get neighborhoods within a specific bounding box
            >>> bbox = (-73.935242, 40.730610, -73.925242, 40.740610)  # Example coordinates
            >>> neighborhoods = AdminRegions()
            >>> neighborhoods.division_type = "neighborhood"
            >>> neighborhoods.from_bbox(bbox)

            >>> # Override admin level for more control
            >>> cities = AdminRegions()
            >>> cities.division_type = "city"
            >>> cities.from_bbox(bbox, overwrite_admin_level="8")
        """
        if self.division_type is None:
            raise ValueError("Division type not set for this layer.")
        warnings.warn(
            "Administrative levels vary across regions. The system will infer the most appropriate admin_level "
            "based on the data and division type, but you can (and is recommended to) override it "
            "with 'overwrite_admin_level'."
        )
        self.tags = {"boundary": "administrative"}
        self.feature_network = AdminFeatures()
        self.feature_network.load("bbox", self.tags, bbox=bbox, **kwargs)
        all_boundaries = self.feature_network.features.to_crs(
            self.coordinate_reference_system
        )
        all_boundaries.reset_index(inplace=True)
        if (
            "element" in all_boundaries.columns
            and "relation" in all_boundaries["element"].unique()
        ):
            all_boundaries = all_boundaries[all_boundaries["element"] == "relation"]
        else:
            logger.log(
                "DEBUG_LOW",
                "No 'relation' found in 'element' column. Using all loaded boundaries.",
            )
        available_levels = all_boundaries["admin_level"].dropna().unique()
        if not available_levels.size:
            raise ValueError(
                "No administrative boundaries found within the provided bounding box."
            )
        if overwrite_admin_level is not None:
            logger.log(
                "DEBUG_LOW", f"Admin level overridden to {overwrite_admin_level}."
            )
            if overwrite_admin_level not in available_levels:
                raise ValueError(
                    f"Overridden admin level {overwrite_admin_level} not found in available levels: {available_levels}."
                )
            admin_level = overwrite_admin_level
        else:
            inferred_level = self.infer_best_admin_level(
                all_boundaries.copy(), self.division_type
            )
            warnings.warn(
                f"Inferred admin_level for {self.division_type}: {inferred_level}. "
                f"Other available levels: {sorted(available_levels)}. "
                "You can override this with 'overwrite_admin_level' if desired."
            )
            admin_level = inferred_level
        self.layer = all_boundaries[
            all_boundaries["admin_level"] == admin_level
        ].to_crs(self.coordinate_reference_system)

    def from_point(
        self,
        lat: float,
        lon: float,
        dist: float,
        overwrite_admin_level: str | None = None,
        **kwargs,
    ) -> None:
        """Load `administrative regions` for a specific point.
        This method retrieves administrative boundaries for a specified point
        from `OpenStreetMap`. It filters for the appropriate `administrative
        level` based on the division_type set for this layer, and can be manually
        overridden if needed.

        Args:
            lat: Latitude of the point to load administrative regions for.
            lon: Longitude of the point to load administrative regions for.
            dist: Distance in meters to search around the point. Consider this a radius.
            overwrite_admin_level: Manually specify the OpenStreetMap admin_level
                to use instead of inferring it. Admin levels differ by region but
                typically follow patterns like:

                - [x] 2: Country
                - [x] 4: State/Province
                - [x] 6: County
                - [x] 8: City/Municipality
                - [x] 10: Neighborhood/Borough

                Feel free to look into [OSM Wiki](https://wiki.openstreetmap.org/wiki/Tag:boundary%3Dadministrative).

            **kwargs: Additional parameters passed to OSMnx's features_from_point.

        Returns:
            Self, for method chaining.

        Raises:
            ValueError: If division_type is not set or if no administrative
                boundaries are found for the specified point.

        Examples:
            >>> # Get neighborhoods around a specific point
            >>> neighborhoods = AdminRegions()
            >>> neighborhoods.division_type = "neighborhood"
            >>> neighborhoods.from_point(40.730610, -73.935242, dist=500)

            >>> # Override admin level for more control
            >>> cities = AdminRegions()
            >>> cities.division_type = "city"
            >>> cities.from_point(40.730610, -73.935242, dist=500, overwrite_admin_level="8")
        """
        if self.division_type is None:
            raise ValueError("Division type not set for this layer.")
        warnings.warn(
            "Administrative levels vary across regions. The system will infer the most appropriate admin_level "
            "based on the data and division type, but you can (and is recommended to) override it "
            "with 'overwrite_admin_level'."
        )
        self.tags = {"boundary": "administrative"}
        self.feature_network = AdminFeatures()
        self.feature_network.load(
            "point", self.tags, lat=lat, lon=lon, dist=dist, **kwargs
        )
        all_boundaries = self.feature_network.features.to_crs(
            self.coordinate_reference_system
        )
        all_boundaries.reset_index(inplace=True)
        if (
            "element" in all_boundaries.columns
            and "relation" in all_boundaries["element"].unique()
        ):
            all_boundaries = all_boundaries[all_boundaries["element"] == "relation"]
        else:
            logger.log(
                "DEBUG_LOW",
                "No 'relation' found in 'element' column. Using all loaded boundaries.",
            )
        available_levels = all_boundaries["admin_level"].dropna().unique()
        if not available_levels.size:
            raise ValueError(
                "No administrative boundaries found around the provided point."
            )
        if overwrite_admin_level is not None:
            logger.log(
                "DEBUG_LOW", f"Admin level overridden to {overwrite_admin_level}."
            )
            if overwrite_admin_level not in available_levels:
                raise ValueError(
                    f"Overridden admin level {overwrite_admin_level} not found in available levels: {available_levels}."
                )
            admin_level = overwrite_admin_level
        else:
            inferred_level = self.infer_best_admin_level(
                all_boundaries.copy(), self.division_type
            )
            warnings.warn(
                f"Inferred admin_level for {self.division_type}: {inferred_level}. "
                f"Other available levels: {sorted(available_levels)}. "
                "You can override this with 'overwrite_admin_level' if desired."
            )
            admin_level = inferred_level
        self.layer = all_boundaries[
            all_boundaries["admin_level"] == admin_level
        ].to_crs(self.coordinate_reference_system)

    def infer_best_admin_level(
        self, boundaries: gpd.GeoDataFrame, division_type: str
    ) -> str:
        """Infer the most appropriate `OpenStreetMap admin_level` for a division type.

        This method uses heuristics to determine which `administrative level` in
        `OpenStreetMap` best matches the requested division type (`neighborhood`, `city`,
        `state`, or `country`). It accounts for both the number of regions at each level
        and their spatial connectivity patterns.

        The method calculates a score for each available admin_level based on:

        - [x] The number of regions (higher for `neighborhoods`, lower for countries)
        - [x] The connectivity between regions (how many `share boundaries`)
        - [x] The specific division type requested, i.e., `neighborhood`, `city`, `state`, or `country`.

        !!! note "Why is this heuristic?"
            This method is intentionally heuristic because `OSM admin_levels` vary
            across `regions` and `countries`. The scoring system prioritises different
            factors based on the division type, as follows:

            - [x] `Neighborhoods`: High region count, moderate connectivity
            - [x] `Cities`: Moderate region count, high connectivity
            - [x] `States`: Low region count, high connectivity
            - [x] `Countries`: Very low region count, very high connectivity

        Args:
            boundaries: `GeoDataFrame` containing administrative boundaries with
                an "admin_level" column.
            division_type: The type of division to find the best level for
                (`neighborhood`, `city`, `state`, or `country`).

        Returns:
            The `admin_level` string that best matches the requested division type.

        Raises:
            ValueError: If the division_type is not recogniseed.
        """
        levels = boundaries["admin_level"].unique()
        metrics = {}
        for level in levels:
            level_gdf = boundaries[boundaries["admin_level"] == level]
            connectivity = self._calculate_connectivity(level_gdf)
            count = len(level_gdf)
            if division_type == "neighborhood":
                score = (count / boundaries.shape[0]) * 100 + connectivity * 0.5
            elif division_type == "city":
                score = (count / boundaries.shape[0]) * 50 + connectivity * 0.75
            elif division_type == "state":
                score = connectivity * 1.0 - (count / boundaries.shape[0]) * 20
            elif division_type == "country":
                score = connectivity * 1.5 - (count / boundaries.shape[0]) * 10
            else:
                raise ValueError(f"Unknown division_type: {division_type}")
            metrics[level] = score
            logger.log(
                "DEBUG_LOW",
                f"Admin level {level}: count={count}, "
                f"connectivity={connectivity:.2f}%, "
                f"score={score:.2f}",
            )
        return max(metrics, key=metrics.get)

    def from_file(
        self, file_path: str | Path, overwrite_admin_level: str | None = None, **kwargs
    ) -> None:
        """Load `administrative regions` from a file.

        !!! warning "Not implemented"
            This method is not implemented for this class. It raises a `NotImplementedError`
            to indicate that loading administrative regions from a file is not supported.

        Args:
            file_path: Path to the file containing administrative regions data.
            overwrite_admin_level: (Optional) Manually specify the OpenStreetMap admin_level
                to use instead of inferring it. Admin levels differ by region but
                typically follow patterns like:

                - [x] 2: Country
                - [x] 4: State/Province
                - [x] 6: County
                - [x] 8: City/Municipality
                - [x] 10: Neighborhood/Borough

            **kwargs: Additional parameters passed to OSMnx's features_from_file.


        Raises:
            NotImplementedError: This method is not implemented for this class.

        Examples:
            >>> # Load administrative regions from a file (not implemented)
            >>> admin_regions = AdminRegions()
            >>> admin_regions.from_file("path/to/file.geojson")
        """
        raise NotImplementedError(
            "Loading administrative regions from file is not supported."
        )

    def preview(self, format: str = "ascii") -> Any:
        """Preview the `urban layer` in a human-readable format.

        This method provides a summary of the `urban layer` attributes, including
        the division type, tags, coordinate reference system, and mappings.
        It can return the preview in either ASCII or JSON format.

        Args:
            format: The format for the preview. Can be "ascii" or "json". Default is "ascii".

        Returns:
            A string or dictionary containing the preview of the urban layer.
            If format is "ascii", returns a formatted string. If format is "json",
            returns a dictionary.

        Raises:
            ValueError: If the specified format is not supported.
        """
        mappings_str = (
            "\n".join(
                "Mapping:\n"
                f"    - lon={m.get('longitude_column', 'N/A')}, "
                f"lat={m.get('latitude_column', 'N/A')}, "
                f"output={m.get('output_column', 'N/A')}"
                for m in self.mappings
            )
            if self.mappings
            else "    No mappings"
        )
        if format == "ascii":
            return (
                f"Urban Layer: Region_{self.division_type}\n"
                f"  Focussing tags: {self.tags}\n"
                f"  CRS: {self.coordinate_reference_system}\n"
                f"  Mappings:\n{mappings_str}"
            )
        elif format == "json":
            return {
                "urban_layer": f"Region_{self.division_type}",
                "tags": self.tags,
                "coordinate_reference_system": self.coordinate_reference_system,
                "mappings": self.mappings,
            }
        else:
            raise ValueError(f"Unsupported format '{format}'")

    def _calculate_connectivity(self, gdf: gpd.GeoDataFrame) -> float:
        """Calculate the `spatial connectivity` percentage for a set of polygons.

        !!! note "What is spatial connectivity?"
            Spatial connectivity refers to the degree to which polygons in a
            geographic dataset are adjacent or overlapping with each other.

            In the context of administrative boundaries, it indicates how
            well-defined and interconnected the regions are. A high connectivity
            percentage suggests that the polygons are closely related and
            form a coherent administrative structure, while a low percentage
            may indicate isolated or poorly defined regions.

            Note that this method is not a strict measure of connectivity but rather
            an approximation based on the number of polygons that share boundaries.

            Lastly, note that this is also a `static` method, consider this as an helper to only
            use within the class.

        Args:
            gdf: `GeoDataFrame` containing polygon geometries to analyze.

        Returns:
            Percentage (0-100) of polygons that touch or overlap with at least
            one other polygon in the dataset.
        """
        if len(gdf) < 2:
            return 0.0
        sindex = gdf.sindex
        touching_count = 0
        for idx, geom in gdf.iterrows():
            possible_matches_index = list(sindex.intersection(geom.geometry.bounds))
            possible_matches = gdf.iloc[possible_matches_index]
            possible_matches = possible_matches[possible_matches.index != idx]
            if any(
                geom.geometry.touches(match.geometry)
                or geom.geometry.overlaps(match.geometry)
                for _, match in possible_matches.iterrows()
            ):
                touching_count += 1
        return (touching_count / len(gdf)) * 100

from_place(place_name, overwrite_admin_level=None, **kwargs)

Load administrative regions for a named place.

This method retrieves administrative boundaries for a specified place name from OpenStreetMap. It filters for the appropriate administrative level based on the division_type set for this layer, and can be manually overridden if needed.

Parameters:

Name Type Description Default
place_name str

Name of the place to load administrative regions for (e.g., "New York City", "Bavaria, Germany").

required
overwrite_admin_level str | None

Manually specify the OpenStreetMap admin_level to use instead of inferring it. Admin levels differ by region but typically follow patterns like:

  • 2: Country
  • 4: State/Province
  • 6: County
  • 8: City/Municipality
  • 10: Neighborhood/Borough

Feel free to look into OSM Wiki.

None
**kwargs

Additional parameters passed to OSMnx's features_from_place.

{}

Returns:

Type Description
None

Self, for method chaining.

Raises:

Type Description
ValueError

If division_type is not set or if no administrative boundaries are found for the specified place.

Examples:

>>> # Get neighborhoods in Manhattan
>>> neighborhoods = AdminRegions()
>>> neighborhoods.division_type = "neighborhood"
>>> neighborhoods.from_place("Manhattan, New York")
>>> # Override admin level for more control
>>> cities = AdminRegions()
>>> cities.division_type = "city"
>>> cities.from_place("France", overwrite_admin_level="6")
Source code in src/urban_mapper/modules/urban_layer/urban_layers/admin_regions_.py
def from_place(
    self, place_name: str, overwrite_admin_level: str | None = None, **kwargs
) -> None:
    """Load `administrative regions` for a named place.

    This method retrieves administrative boundaries for a specified place
    name from `OpenStreetMap`. It filters for the appropriate `administrative
    level` based on the division_type set for this layer, and can be manually
    overridden if needed.

    Args:
        place_name: Name of the place to load administrative regions for
            (e.g., "New York City", "Bavaria, Germany").
        overwrite_admin_level: Manually specify the OpenStreetMap admin_level
            to use instead of inferring it. Admin levels differ by region but
            typically follow patterns like:

            - [x] 2: Country
            - [x] 4: State/Province
            - [x] 6: County
            - [x] 8: City/Municipality
            - [x] 10: Neighborhood/Borough

            Feel free to look into [OSM Wiki](https://wiki.openstreetmap.org/wiki/Tag:boundary%3Dadministrative).

        **kwargs: Additional parameters passed to OSMnx's features_from_place.

    Returns:
        Self, for method chaining.

    Raises:
        ValueError: If division_type is not set or if no administrative
            boundaries are found for the specified place.

    Examples:
        >>> # Get neighborhoods in Manhattan
        >>> neighborhoods = AdminRegions()
        >>> neighborhoods.division_type = "neighborhood"
        >>> neighborhoods.from_place("Manhattan, New York")

        >>> # Override admin level for more control
        >>> cities = AdminRegions()
        >>> cities.division_type = "city"
        >>> cities.from_place("France", overwrite_admin_level="6")
    """
    if self.division_type is None:
        raise ValueError("Division type not set for this layer.")
    warnings.warn(
        "Administrative levels vary across regions. The system will infer the most appropriate admin_level "
        "based on the data and division type, but you can (and is recommended to) override it "
        "with 'overwrite_admin_level'."
    )
    geolocator = Nominatim(user_agent="urban_mapper")
    place_polygon = None
    try:
        location = geolocator.geocode(place_name, geometry="wkt")
        if location and "geotext" in location.raw:
            place_polygon = loads(location.raw["geotext"])
        else:
            logger.log(
                "DEBUG_LOW", f"Geocoding for {place_name} did not return a polygon."
            )
    except Exception as e:
        logger.log(
            "DEBUG_LOW",
            f"Geocoding failed for {place_name}: {e}. Proceeding without polygon filtering.",
        )
    self.tags = {"boundary": "administrative"}
    self.feature_network = AdminFeatures()
    self.feature_network.load("place", self.tags, query=place_name, **kwargs)
    all_boundaries = self.feature_network.features.to_crs(
        self.coordinate_reference_system
    )
    if place_polygon:
        all_boundaries = all_boundaries[
            all_boundaries.geometry.within(place_polygon)
        ]
        if all_boundaries.empty:
            logger.log(
                "DEBUG_LOW",
                "No boundaries found within the geocoded polygon. Using all loaded boundaries.",
            )
            all_boundaries = self.feature_network.features.to_crs(
                self.coordinate_reference_system
            )
    all_boundaries.reset_index(inplace=True)
    if (
        "element" in all_boundaries.columns
        and "relation" in all_boundaries["element"].unique()
    ):
        all_boundaries = all_boundaries[all_boundaries["element"] == "relation"]
    else:
        logger.log(
            "DEBUG_LOW",
            "No 'relation' found in 'element' column. Using all loaded boundaries.",
        )
    available_levels = all_boundaries["admin_level"].dropna().unique()
    if not available_levels.size:
        raise ValueError(f"No administrative boundaries found for {place_name}.")
    if overwrite_admin_level is not None:
        logger.log(
            "DEBUG_LOW", f"Admin level overridden to {overwrite_admin_level}."
        )
        if overwrite_admin_level not in available_levels:
            raise ValueError(
                f"Overridden admin level {overwrite_admin_level} not found in available levels: {available_levels}."
            )
        admin_level = overwrite_admin_level
    else:
        inferred_level = self.infer_best_admin_level(
            all_boundaries.copy(), self.division_type
        )
        warnings.warn(
            f"Inferred admin_level for {self.division_type}: {inferred_level}. "
            f"Other available levels: {sorted(available_levels)}. "
            "You can override this with 'overwrite_admin_level' if desired."
        )
        admin_level = inferred_level
    self.layer = all_boundaries[
        all_boundaries["admin_level"] == admin_level
    ].to_crs(self.coordinate_reference_system)

from_address(address, dist, overwrite_admin_level=None, **kwargs)

Load administrative regions for a specific address.

This method retrieves administrative boundaries for a specified address from OpenStreetMap. It filters for the appropriate administrative level based on the division_type set for this layer, and can be manually overridden if needed.

Parameters:

Name Type Description Default
address str

Address to load administrative regions for (e.g., "1600 Amphitheatre Parkway, Mountain View, CA").

required
dist float

Distance in meters to search around the address. Consider this a radius.

required
overwrite_admin_level str | None

Manually specify the OpenStreetMap admin_level to use instead of inferring it. Admin levels differ by region but typically follow patterns like:

  • 2: Country
  • 4: State/Province
  • 6: County
  • 8: City/Municipality
  • 10: Neighborhood/Borough

Feel free to look into OSM Wiki.

None
**kwargs

Additional parameters passed to OSMnx's features_from_address.

{}

Returns:

Type Description
None

Self, for method chaining.

Raises:

Type Description
ValueError

If division_type is not set or if no administrative boundaries are found for the specified address.

Examples:

>>> # Get neighborhoods around a specific address
>>> neighborhoods = AdminRegions()
>>> neighborhoods.division_type = "neighborhood"
>>> neighborhoods.from_address("1600 Amphitheatre Parkway, Mountain View, CA", dist=500)
>>> # Override admin level for more control
>>> cities = AdminRegions()
>>> cities.division_type = "city"
>>> cities.from_address("1600 Amphitheatre Parkway, Mountain View, CA", dist=500, overwrite_admin_level="6")
Source code in src/urban_mapper/modules/urban_layer/urban_layers/admin_regions_.py
def from_address(
    self,
    address: str,
    dist: float,
    overwrite_admin_level: str | None = None,
    **kwargs,
) -> None:
    """Load `administrative regions` for a specific address.

    This method retrieves administrative boundaries for a specified address
    from `OpenStreetMap`. It filters for the appropriate `administrative
    level` based on the division_type set for this layer, and can be manually
    overridden if needed.

    Args:
        address: Address to load administrative regions for (e.g., "1600 Amphitheatre Parkway, Mountain View, CA").
        dist: Distance in meters to search around the address. Consider this a radius.
        overwrite_admin_level: Manually specify the OpenStreetMap admin_level
            to use instead of inferring it. Admin levels differ by region but
            typically follow patterns like:

            - [x] 2: Country
            - [x] 4: State/Province
            - [x] 6: County
            - [x] 8: City/Municipality
            - [x] 10: Neighborhood/Borough

            Feel free to look into [OSM Wiki](https://wiki.openstreetmap.org/wiki/Tag:boundary%3Dadministrative).

        **kwargs: Additional parameters passed to OSMnx's features_from_address.

    Returns:
        Self, for method chaining.

    Raises:
        ValueError: If division_type is not set or if no administrative
            boundaries are found for the specified address.

    Examples:
        >>> # Get neighborhoods around a specific address
        >>> neighborhoods = AdminRegions()
        >>> neighborhoods.division_type = "neighborhood"
        >>> neighborhoods.from_address("1600 Amphitheatre Parkway, Mountain View, CA", dist=500)

        >>> # Override admin level for more control
        >>> cities = AdminRegions()
        >>> cities.division_type = "city"
        >>> cities.from_address("1600 Amphitheatre Parkway, Mountain View, CA", dist=500, overwrite_admin_level="6")
    """

    if self.division_type is None:
        raise ValueError("Division type not set for this layer.")
    warnings.warn(
        "Administrative levels vary across regions. The system will infer the most appropriate admin_level "
        "based on the data and division type, but you can (and is recommended to) override it "
        "with 'overwrite_admin_level'."
    )
    geolocator = Nominatim(user_agent="urban_mapper")
    place_polygon = None
    try:
        location = geolocator.geocode(address, geometry="wkt")
        if location and "geotext" in location.raw:
            place_polygon = loads(location.raw["geotext"])
        else:
            logger.log(
                "DEBUG_LOW", f"Geocoding for {address} did not return a polygon."
            )
    except Exception as e:
        logger.log(
            "DEBUG_LOW",
            f"Geocoding failed for {address}: {e}. Proceeding without polygon filtering.",
        )
    self.tags = {"boundary": "administrative"}
    self.feature_network = AdminFeatures()
    self.feature_network.load(
        "address", self.tags, address=address, dist=dist, **kwargs
    )
    all_boundaries = self.feature_network.features.to_crs(
        self.coordinate_reference_system
    )
    if place_polygon:
        all_boundaries = all_boundaries[
            all_boundaries.geometry.within(place_polygon)
        ]
        if all_boundaries.empty:
            logger.log(
                "DEBUG_LOW",
                "No boundaries found within the geocoded polygon. Using all loaded boundaries.",
            )
            all_boundaries = self.feature_network.features.to_crs(
                self.coordinate_reference_system
            )
    all_boundaries.reset_index(inplace=True)
    if (
        "element" in all_boundaries.columns
        and "relation" in all_boundaries["element"].unique()
    ):
        all_boundaries = all_boundaries[all_boundaries["element"] == "relation"]
    else:
        logger.log(
            "DEBUG_LOW",
            "No 'relation' found in 'element' column. Using all loaded boundaries.",
        )
    available_levels = all_boundaries["admin_level"].dropna().unique()
    if not available_levels.size:
        raise ValueError(
            f"No administrative boundaries found for address {address}."
        )
    if overwrite_admin_level is not None:
        logger.log(
            "DEBUG_LOW", f"Admin level overridden to {overwrite_admin_level}."
        )
        if overwrite_admin_level not in available_levels:
            raise ValueError(
                f"Overridden admin level {overwrite_admin_level} not found in available levels: {available_levels}."
            )
        admin_level = overwrite_admin_level
    else:
        inferred_level = self.infer_best_admin_level(
            all_boundaries.copy(), self.division_type
        )
        warnings.warn(
            f"Inferred admin_level for {self.division_type}: {inferred_level}. "
            f"Other available levels: {sorted(available_levels)}. "
            "You can override this with 'overwrite_admin_level' if desired."
        )
        admin_level = inferred_level
    self.layer = all_boundaries[
        all_boundaries["admin_level"] == admin_level
    ].to_crs(self.coordinate_reference_system)

from_bbox(bbox, overwrite_admin_level=None, **kwargs)

Load administrative regions for a specific bounding box. This method retrieves administrative boundaries for a specified bounding box from OpenStreetMap. It filters for the appropriate administrative level based on the division_type set for this layer, and can be manually overridden if needed.

Parameters:

Name Type Description Default
bbox Tuple[float, float, float, float]

Tuple of (left, bottom, right, top) coordinates defining the bounding box.

required
overwrite_admin_level str | None

Manually specify the OpenStreetMap admin_level to use instead of inferring it. Admin levels differ by region but typically follow patterns like:

  • 2: Country
  • 4: State/Province
  • 6: County
  • 8: City/Municipality
  • 10: Neighborhood/Borough

Feel free to look into OSM Wiki.

None
**kwargs

Additional parameters passed to OSMnx's features_from_bbox.

{}

Returns:

Type Description
None

Self, for method chaining.

Raises:

Type Description
ValueError

If division_type is not set or if no administrative boundaries are found for the specified bounding box.

Examples:

>>> # Get neighborhoods within a specific bounding box
>>> bbox = (-73.935242, 40.730610, -73.925242, 40.740610)  # Example coordinates
>>> neighborhoods = AdminRegions()
>>> neighborhoods.division_type = "neighborhood"
>>> neighborhoods.from_bbox(bbox)
>>> # Override admin level for more control
>>> cities = AdminRegions()
>>> cities.division_type = "city"
>>> cities.from_bbox(bbox, overwrite_admin_level="8")
Source code in src/urban_mapper/modules/urban_layer/urban_layers/admin_regions_.py
def from_bbox(
    self,
    bbox: Tuple[float, float, float, float],
    overwrite_admin_level: str | None = None,
    **kwargs,
) -> None:
    """Load `administrative regions` for a specific bounding box.
    This method retrieves administrative boundaries for a specified bounding
    box from `OpenStreetMap`. It filters for the appropriate `administrative
    level` based on the division_type set for this layer, and can be manually
    overridden if needed.

    Args:
        bbox: Tuple of (left, bottom, right, top) coordinates defining the bounding box.
        overwrite_admin_level: Manually specify the OpenStreetMap admin_level
            to use instead of inferring it. Admin levels differ by region but
            typically follow patterns like:

            - [x] 2: Country
            - [x] 4: State/Province
            - [x] 6: County
            - [x] 8: City/Municipality
            - [x] 10: Neighborhood/Borough

            Feel free to look into [OSM Wiki](https://wiki.openstreetmap.org/wiki/Tag:boundary%3Dadministrative).

        **kwargs: Additional parameters passed to OSMnx's features_from_bbox.

    Returns:
        Self, for method chaining.

    Raises:
        ValueError: If division_type is not set or if no administrative
            boundaries are found for the specified bounding box.

    Examples:
        >>> # Get neighborhoods within a specific bounding box
        >>> bbox = (-73.935242, 40.730610, -73.925242, 40.740610)  # Example coordinates
        >>> neighborhoods = AdminRegions()
        >>> neighborhoods.division_type = "neighborhood"
        >>> neighborhoods.from_bbox(bbox)

        >>> # Override admin level for more control
        >>> cities = AdminRegions()
        >>> cities.division_type = "city"
        >>> cities.from_bbox(bbox, overwrite_admin_level="8")
    """
    if self.division_type is None:
        raise ValueError("Division type not set for this layer.")
    warnings.warn(
        "Administrative levels vary across regions. The system will infer the most appropriate admin_level "
        "based on the data and division type, but you can (and is recommended to) override it "
        "with 'overwrite_admin_level'."
    )
    self.tags = {"boundary": "administrative"}
    self.feature_network = AdminFeatures()
    self.feature_network.load("bbox", self.tags, bbox=bbox, **kwargs)
    all_boundaries = self.feature_network.features.to_crs(
        self.coordinate_reference_system
    )
    all_boundaries.reset_index(inplace=True)
    if (
        "element" in all_boundaries.columns
        and "relation" in all_boundaries["element"].unique()
    ):
        all_boundaries = all_boundaries[all_boundaries["element"] == "relation"]
    else:
        logger.log(
            "DEBUG_LOW",
            "No 'relation' found in 'element' column. Using all loaded boundaries.",
        )
    available_levels = all_boundaries["admin_level"].dropna().unique()
    if not available_levels.size:
        raise ValueError(
            "No administrative boundaries found within the provided bounding box."
        )
    if overwrite_admin_level is not None:
        logger.log(
            "DEBUG_LOW", f"Admin level overridden to {overwrite_admin_level}."
        )
        if overwrite_admin_level not in available_levels:
            raise ValueError(
                f"Overridden admin level {overwrite_admin_level} not found in available levels: {available_levels}."
            )
        admin_level = overwrite_admin_level
    else:
        inferred_level = self.infer_best_admin_level(
            all_boundaries.copy(), self.division_type
        )
        warnings.warn(
            f"Inferred admin_level for {self.division_type}: {inferred_level}. "
            f"Other available levels: {sorted(available_levels)}. "
            "You can override this with 'overwrite_admin_level' if desired."
        )
        admin_level = inferred_level
    self.layer = all_boundaries[
        all_boundaries["admin_level"] == admin_level
    ].to_crs(self.coordinate_reference_system)

from_point(lat, lon, dist, overwrite_admin_level=None, **kwargs)

Load administrative regions for a specific point. This method retrieves administrative boundaries for a specified point from OpenStreetMap. It filters for the appropriate administrative level based on the division_type set for this layer, and can be manually overridden if needed.

Parameters:

Name Type Description Default
lat float

Latitude of the point to load administrative regions for.

required
lon float

Longitude of the point to load administrative regions for.

required
dist float

Distance in meters to search around the point. Consider this a radius.

required
overwrite_admin_level str | None

Manually specify the OpenStreetMap admin_level to use instead of inferring it. Admin levels differ by region but typically follow patterns like:

  • 2: Country
  • 4: State/Province
  • 6: County
  • 8: City/Municipality
  • 10: Neighborhood/Borough

Feel free to look into OSM Wiki.

None
**kwargs

Additional parameters passed to OSMnx's features_from_point.

{}

Returns:

Type Description
None

Self, for method chaining.

Raises:

Type Description
ValueError

If division_type is not set or if no administrative boundaries are found for the specified point.

Examples:

>>> # Get neighborhoods around a specific point
>>> neighborhoods = AdminRegions()
>>> neighborhoods.division_type = "neighborhood"
>>> neighborhoods.from_point(40.730610, -73.935242, dist=500)
>>> # Override admin level for more control
>>> cities = AdminRegions()
>>> cities.division_type = "city"
>>> cities.from_point(40.730610, -73.935242, dist=500, overwrite_admin_level="8")
Source code in src/urban_mapper/modules/urban_layer/urban_layers/admin_regions_.py
def from_point(
    self,
    lat: float,
    lon: float,
    dist: float,
    overwrite_admin_level: str | None = None,
    **kwargs,
) -> None:
    """Load `administrative regions` for a specific point.
    This method retrieves administrative boundaries for a specified point
    from `OpenStreetMap`. It filters for the appropriate `administrative
    level` based on the division_type set for this layer, and can be manually
    overridden if needed.

    Args:
        lat: Latitude of the point to load administrative regions for.
        lon: Longitude of the point to load administrative regions for.
        dist: Distance in meters to search around the point. Consider this a radius.
        overwrite_admin_level: Manually specify the OpenStreetMap admin_level
            to use instead of inferring it. Admin levels differ by region but
            typically follow patterns like:

            - [x] 2: Country
            - [x] 4: State/Province
            - [x] 6: County
            - [x] 8: City/Municipality
            - [x] 10: Neighborhood/Borough

            Feel free to look into [OSM Wiki](https://wiki.openstreetmap.org/wiki/Tag:boundary%3Dadministrative).

        **kwargs: Additional parameters passed to OSMnx's features_from_point.

    Returns:
        Self, for method chaining.

    Raises:
        ValueError: If division_type is not set or if no administrative
            boundaries are found for the specified point.

    Examples:
        >>> # Get neighborhoods around a specific point
        >>> neighborhoods = AdminRegions()
        >>> neighborhoods.division_type = "neighborhood"
        >>> neighborhoods.from_point(40.730610, -73.935242, dist=500)

        >>> # Override admin level for more control
        >>> cities = AdminRegions()
        >>> cities.division_type = "city"
        >>> cities.from_point(40.730610, -73.935242, dist=500, overwrite_admin_level="8")
    """
    if self.division_type is None:
        raise ValueError("Division type not set for this layer.")
    warnings.warn(
        "Administrative levels vary across regions. The system will infer the most appropriate admin_level "
        "based on the data and division type, but you can (and is recommended to) override it "
        "with 'overwrite_admin_level'."
    )
    self.tags = {"boundary": "administrative"}
    self.feature_network = AdminFeatures()
    self.feature_network.load(
        "point", self.tags, lat=lat, lon=lon, dist=dist, **kwargs
    )
    all_boundaries = self.feature_network.features.to_crs(
        self.coordinate_reference_system
    )
    all_boundaries.reset_index(inplace=True)
    if (
        "element" in all_boundaries.columns
        and "relation" in all_boundaries["element"].unique()
    ):
        all_boundaries = all_boundaries[all_boundaries["element"] == "relation"]
    else:
        logger.log(
            "DEBUG_LOW",
            "No 'relation' found in 'element' column. Using all loaded boundaries.",
        )
    available_levels = all_boundaries["admin_level"].dropna().unique()
    if not available_levels.size:
        raise ValueError(
            "No administrative boundaries found around the provided point."
        )
    if overwrite_admin_level is not None:
        logger.log(
            "DEBUG_LOW", f"Admin level overridden to {overwrite_admin_level}."
        )
        if overwrite_admin_level not in available_levels:
            raise ValueError(
                f"Overridden admin level {overwrite_admin_level} not found in available levels: {available_levels}."
            )
        admin_level = overwrite_admin_level
    else:
        inferred_level = self.infer_best_admin_level(
            all_boundaries.copy(), self.division_type
        )
        warnings.warn(
            f"Inferred admin_level for {self.division_type}: {inferred_level}. "
            f"Other available levels: {sorted(available_levels)}. "
            "You can override this with 'overwrite_admin_level' if desired."
        )
        admin_level = inferred_level
    self.layer = all_boundaries[
        all_boundaries["admin_level"] == admin_level
    ].to_crs(self.coordinate_reference_system)

from_polygon(polygon, overwrite_admin_level=None, **kwargs)

Load administrative regions for a specific polygon. This method retrieves administrative boundaries for a specified polygon from OpenStreetMap. It filters for the appropriate administrative level based on the division_type set for this layer, and can be manually overridden if needed.

Parameters:

Name Type Description Default
polygon Polygon | MultiPolygon

Shapely Polygon or MultiPolygon to load administrative regions for.

required
overwrite_admin_level str | None

Manually specify the OpenStreetMap admin_level to use instead of inferring it. Admin levels differ by region but typically follow patterns like:

  • 2: Country
  • 4: State/Province
  • 6: County
  • 8: City/Municipality
  • 10: Neighborhood/Borough

Feel free to look into OSM Wiki.

None
**kwargs

Additional parameters passed to OSMnx's features_from_polygon.

{}

Returns:

Type Description
None

Self, for method chaining.

Raises:

Type Description
ValueError

If division_type is not set or if no administrative boundaries are found for the specified polygon.

Examples:

>>> # Create a polygon out of an address for instance, with the help of geopy
>>> from geopy.geocoders import Nominatim
>>> geolocator = Nominatim(user_agent="urban_mapper")
>>> location = geolocator.geocode("1600 Amphitheatre Parkway, Mountain View, CA", geometry="wkt")
>>> polygon = loads(location.raw["geotext"])
>>> # Get neighborhoods within a specific polygon
>>> neighborhoods = AdminRegions()
>>> neighborhoods.division_type = "neighborhood"
>>> neighborhoods.from_polygon(polygon)
>>> # Override admin level for more control
>>> cities = AdminRegions()
>>> cities.division_type = "neighborhood"
>>> cities.from_polygon(polygon, overwrite_admin_level="8")
Source code in src/urban_mapper/modules/urban_layer/urban_layers/admin_regions_.py
def from_polygon(
    self,
    polygon: Polygon | MultiPolygon,
    overwrite_admin_level: str | None = None,
    **kwargs,
) -> None:
    """Load `administrative regions` for a specific polygon.
    This method retrieves administrative boundaries for a specified polygon
    from `OpenStreetMap`. It filters for the appropriate `administrative
    level` based on the division_type set for this layer, and can be manually
    overridden if needed.

    Args:
        polygon: Shapely Polygon or MultiPolygon to load administrative regions for.
        overwrite_admin_level: Manually specify the OpenStreetMap admin_level
            to use instead of inferring it. Admin levels differ by region but
            typically follow patterns like:

            - [x] 2: Country
            - [x] 4: State/Province
            - [x] 6: County
            - [x] 8: City/Municipality
            - [x] 10: Neighborhood/Borough

            Feel free to look into [OSM Wiki](https://wiki.openstreetmap.org/wiki/Tag:boundary%3Dadministrative).

        **kwargs: Additional parameters passed to OSMnx's features_from_polygon.

    Returns:
        Self, for method chaining.

    Raises:
        ValueError: If division_type is not set or if no administrative
            boundaries are found for the specified polygon.

    Examples:
        >>> # Create a polygon out of an address for instance, with the help of geopy
        >>> from geopy.geocoders import Nominatim
        >>> geolocator = Nominatim(user_agent="urban_mapper")
        >>> location = geolocator.geocode("1600 Amphitheatre Parkway, Mountain View, CA", geometry="wkt")
        >>> polygon = loads(location.raw["geotext"])

        >>> # Get neighborhoods within a specific polygon
        >>> neighborhoods = AdminRegions()
        >>> neighborhoods.division_type = "neighborhood"
        >>> neighborhoods.from_polygon(polygon)

        >>> # Override admin level for more control
        >>> cities = AdminRegions()
        >>> cities.division_type = "neighborhood"
        >>> cities.from_polygon(polygon, overwrite_admin_level="8")
    """

    if self.division_type is None:
        raise ValueError("Division type not set for this layer.")
    warnings.warn(
        "Administrative levels vary across regions. The system will infer the most appropriate admin_level "
        "based on the data and division type, but you can (and is recommended to) override it "
        "with 'overwrite_admin_level'."
    )
    self.tags = {"boundary": "administrative"}
    self.feature_network = AdminFeatures()
    self.feature_network.load("polygon", self.tags, polygon=polygon, **kwargs)
    all_boundaries = self.feature_network.features.to_crs(
        self.coordinate_reference_system
    )
    all_boundaries = all_boundaries[all_boundaries.geometry.within(polygon)]
    if all_boundaries.empty:
        logger.log(
            "DEBUG_LOW",
            "No boundaries found within the provided polygon. Using all loaded boundaries.",
        )
        all_boundaries = self.feature_network.features.to_crs(
            self.coordinate_reference_system
        )
    all_boundaries.reset_index(inplace=True)
    if (
        "element" in all_boundaries.columns
        and "relation" in all_boundaries["element"].unique()
    ):
        all_boundaries = all_boundaries[all_boundaries["element"] == "relation"]
    else:
        logger.log(
            "DEBUG_LOW",
            "No 'relation' found in 'element' column. Using all loaded boundaries.",
        )
    available_levels = all_boundaries["admin_level"].dropna().unique()
    if not available_levels.size:
        raise ValueError(
            "No administrative boundaries found within the provided polygon."
        )
    if overwrite_admin_level is not None:
        logger.log(
            "DEBUG_LOW", f"Admin level overridden to {overwrite_admin_level}."
        )
        if overwrite_admin_level not in available_levels:
            raise ValueError(
                f"Overridden admin level {overwrite_admin_level} not found in available levels: {available_levels}."
            )
        admin_level = overwrite_admin_level
    else:
        inferred_level = self.infer_best_admin_level(
            all_boundaries.copy(), self.division_type
        )
        warnings.warn(
            f"Inferred admin_level for {self.division_type}: {inferred_level}. "
            f"Other available levels: {sorted(available_levels)}. "
            "You can override this with 'overwrite_admin_level' if desired."
        )
        admin_level = inferred_level
    self.layer = all_boundaries[
        all_boundaries["admin_level"] == admin_level
    ].to_crs(self.coordinate_reference_system)

from_file(file_path, overwrite_admin_level=None, **kwargs)

Load administrative regions from a file.

Not implemented

This method is not implemented for this class. It raises a NotImplementedError to indicate that loading administrative regions from a file is not supported.

Parameters:

Name Type Description Default
file_path str | Path

Path to the file containing administrative regions data.

required
overwrite_admin_level str | None

(Optional) Manually specify the OpenStreetMap admin_level to use instead of inferring it. Admin levels differ by region but typically follow patterns like:

  • 2: Country
  • 4: State/Province
  • 6: County
  • 8: City/Municipality
  • 10: Neighborhood/Borough
None
**kwargs

Additional parameters passed to OSMnx's features_from_file.

{}

Raises:

Type Description
NotImplementedError

This method is not implemented for this class.

Examples:

>>> # Load administrative regions from a file (not implemented)
>>> admin_regions = AdminRegions()
>>> admin_regions.from_file("path/to/file.geojson")
Source code in src/urban_mapper/modules/urban_layer/urban_layers/admin_regions_.py
def from_file(
    self, file_path: str | Path, overwrite_admin_level: str | None = None, **kwargs
) -> None:
    """Load `administrative regions` from a file.

    !!! warning "Not implemented"
        This method is not implemented for this class. It raises a `NotImplementedError`
        to indicate that loading administrative regions from a file is not supported.

    Args:
        file_path: Path to the file containing administrative regions data.
        overwrite_admin_level: (Optional) Manually specify the OpenStreetMap admin_level
            to use instead of inferring it. Admin levels differ by region but
            typically follow patterns like:

            - [x] 2: Country
            - [x] 4: State/Province
            - [x] 6: County
            - [x] 8: City/Municipality
            - [x] 10: Neighborhood/Borough

        **kwargs: Additional parameters passed to OSMnx's features_from_file.


    Raises:
        NotImplementedError: This method is not implemented for this class.

    Examples:
        >>> # Load administrative regions from a file (not implemented)
        >>> admin_regions = AdminRegions()
        >>> admin_regions.from_file("path/to/file.geojson")
    """
    raise NotImplementedError(
        "Loading administrative regions from file is not supported."
    )

_calculate_connectivity(gdf)

Calculate the spatial connectivity percentage for a set of polygons.

What is spatial connectivity?

Spatial connectivity refers to the degree to which polygons in a geographic dataset are adjacent or overlapping with each other.

In the context of administrative boundaries, it indicates how well-defined and interconnected the regions are. A high connectivity percentage suggests that the polygons are closely related and form a coherent administrative structure, while a low percentage may indicate isolated or poorly defined regions.

Note that this method is not a strict measure of connectivity but rather an approximation based on the number of polygons that share boundaries.

Lastly, note that this is also a static method, consider this as an helper to only use within the class.

Parameters:

Name Type Description Default
gdf GeoDataFrame

GeoDataFrame containing polygon geometries to analyze.

required

Returns:

Type Description
float

Percentage (0-100) of polygons that touch or overlap with at least

float

one other polygon in the dataset.

Source code in src/urban_mapper/modules/urban_layer/urban_layers/admin_regions_.py
def _calculate_connectivity(self, gdf: gpd.GeoDataFrame) -> float:
    """Calculate the `spatial connectivity` percentage for a set of polygons.

    !!! note "What is spatial connectivity?"
        Spatial connectivity refers to the degree to which polygons in a
        geographic dataset are adjacent or overlapping with each other.

        In the context of administrative boundaries, it indicates how
        well-defined and interconnected the regions are. A high connectivity
        percentage suggests that the polygons are closely related and
        form a coherent administrative structure, while a low percentage
        may indicate isolated or poorly defined regions.

        Note that this method is not a strict measure of connectivity but rather
        an approximation based on the number of polygons that share boundaries.

        Lastly, note that this is also a `static` method, consider this as an helper to only
        use within the class.

    Args:
        gdf: `GeoDataFrame` containing polygon geometries to analyze.

    Returns:
        Percentage (0-100) of polygons that touch or overlap with at least
        one other polygon in the dataset.
    """
    if len(gdf) < 2:
        return 0.0
    sindex = gdf.sindex
    touching_count = 0
    for idx, geom in gdf.iterrows():
        possible_matches_index = list(sindex.intersection(geom.geometry.bounds))
        possible_matches = gdf.iloc[possible_matches_index]
        possible_matches = possible_matches[possible_matches.index != idx]
        if any(
            geom.geometry.touches(match.geometry)
            or geom.geometry.overlaps(match.geometry)
            for _, match in possible_matches.iterrows()
        ):
            touching_count += 1
    return (touching_count / len(gdf)) * 100

preview(format='ascii')

Preview the urban layer in a human-readable format.

This method provides a summary of the urban layer attributes, including the division type, tags, coordinate reference system, and mappings. It can return the preview in either ASCII or JSON format.

Parameters:

Name Type Description Default
format str

The format for the preview. Can be "ascii" or "json". Default is "ascii".

'ascii'

Returns:

Type Description
Any

A string or dictionary containing the preview of the urban layer.

Any

If format is "ascii", returns a formatted string. If format is "json",

Any

returns a dictionary.

Raises:

Type Description
ValueError

If the specified format is not supported.

Source code in src/urban_mapper/modules/urban_layer/urban_layers/admin_regions_.py
def preview(self, format: str = "ascii") -> Any:
    """Preview the `urban layer` in a human-readable format.

    This method provides a summary of the `urban layer` attributes, including
    the division type, tags, coordinate reference system, and mappings.
    It can return the preview in either ASCII or JSON format.

    Args:
        format: The format for the preview. Can be "ascii" or "json". Default is "ascii".

    Returns:
        A string or dictionary containing the preview of the urban layer.
        If format is "ascii", returns a formatted string. If format is "json",
        returns a dictionary.

    Raises:
        ValueError: If the specified format is not supported.
    """
    mappings_str = (
        "\n".join(
            "Mapping:\n"
            f"    - lon={m.get('longitude_column', 'N/A')}, "
            f"lat={m.get('latitude_column', 'N/A')}, "
            f"output={m.get('output_column', 'N/A')}"
            for m in self.mappings
        )
        if self.mappings
        else "    No mappings"
    )
    if format == "ascii":
        return (
            f"Urban Layer: Region_{self.division_type}\n"
            f"  Focussing tags: {self.tags}\n"
            f"  CRS: {self.coordinate_reference_system}\n"
            f"  Mappings:\n{mappings_str}"
        )
    elif format == "json":
        return {
            "urban_layer": f"Region_{self.division_type}",
            "tags": self.tags,
            "coordinate_reference_system": self.coordinate_reference_system,
            "mappings": self.mappings,
        }
    else:
        raise ValueError(f"Unsupported format '{format}'")
Fabio, Provost Simon