Skip to content

Way

Represents a single OSM feature with geometry and tags.

A Way is the fundamental unit of map data. It wraps an OSM way or relation with its parsed geometry and tag dictionary, and provides helpers to classify the feature and convert it to a point cloud.

Attributes:

Name Type Description
id any

OSM way ID (positive integer) or a negative integer / string for manually annotated ways.

is_area bool

True if the geometry is a closed polygon (area), False for a linestring.

nodes list

Ordered list of node IDs that make up this way.

tags dict

OSM key-value tags (e.g. {"highway": "footway", "surface": "asphalt"}).

line BaseGeometry or None

Shapely geometry representing the way in UTM coordinates.

in_out str

Direction hint used by some planners ("in", "out", or "").

pcd_points ndarray or None

Cached point cloud generated by :meth:to_pcd_points. None until first computed.

Attributes

Way objects are created by MapData.run_parse() and are not typically constructed manually.

Attribute Type Description
id int OSM way or relation ID
nodes list Ordered list of OSM node IDs defining the geometry
tags dict[str, str] OSM tags (e.g. {"highway": "footway", "surface": "asphalt"})
line shapely.Geometry \| None Geometry in UTM metres. LineString for roads and footways; Polygon for area-type barriers. None if unparsed.
in_out str \| None Direction hint set by GraphPlanner: "in", "out", or None for bidirectional
pcd_points np.ndarray \| None Array of equidistant 3D points along the way, populated by to_pcd_points()

Note

The line geometry uses the same UTM coordinate system (metres) as the parent MapData object. To convert a point to WGS84, use utm.to_latlon(x, y, md.zone_number, md.zone_letter).

Methods

map_data.utils.way.Way.is_road()

Return True if this way is a vehicle road (any highway value not in footway types).

Source code in map_data/utils/way.py
def is_road(self) -> bool:
    """
    Return ``True`` if this way is a vehicle road (any ``highway`` value not in footway types).
    """
    hw = self.tags.get("highway")
    return bool(hw and hw not in FOOTWAY_VALUES)

map_data.utils.way.Way.is_footway()

Return True if this way is a pedestrian footway (highway in the footway value set).

Source code in map_data/utils/way.py
def is_footway(self) -> bool:
    """
    Return ``True`` if this way is a pedestrian footway (``highway`` in the footway value set).
    """
    hw = self.tags.get("highway")
    return bool(hw and hw in FOOTWAY_VALUES)

map_data.utils.way.Way.is_barrier(yes_tags, not_tags, anti_tags)

Return True if this way should be classified as an untraversable barrier.

Parameters:

Name Type Description Default
yes_tags dict

Mapping of OSM tag key → list of values that indicate a barrier. A "*" value matches any tag value not in not_tags.

required
not_tags dict

Exceptions to "*" wildcard matches in yes_tags.

required
anti_tags dict

Tags that, if present, override a barrier match and return False (e.g. a gate that makes a fence passable).

required
Source code in map_data/utils/way.py
def is_barrier(
    self,
    yes_tags: dict[str, list[str]],
    not_tags: dict[str, list[str]],
    anti_tags: dict[str, list[str]],
) -> bool:
    """
    Return ``True`` if this way should be classified as an untraversable barrier.

    Parameters
    ----------
    yes_tags : dict
        Mapping of OSM tag key → list of values that indicate a barrier.
        A ``"*"`` value matches any tag value not in *not_tags*.
    not_tags : dict
        Exceptions to ``"*"`` wildcard matches in *yes_tags*.
    anti_tags : dict
        Tags that, if present, override a barrier match and return
        ``False`` (e.g. a gate that makes a fence passable).

    """
    has_barrier_tag = any(
        key in yes_tags
        and (
            self.tags[key] in yes_tags[key]
            or ("*" in yes_tags[key] and self.tags[key] not in not_tags.get(key, []))
        )
        for key in self.tags
    )
    has_anti_tag = any(
        key in anti_tags and self.tags[key] in anti_tags[key] for key in self.tags
    )
    return has_barrier_tag and not has_anti_tag

map_data.utils.way.Way.to_pcd_points(density=2.0, *, filled=True)

Convert the way's geometry to a dense 2-D point cloud.

For closed polygon areas the interior is filled with a regular grid of points. For linestrings (or unfilled polygons) points are sampled along the edges at the given density. The result is cached in :attr:pcd_points after the first call.

Parameters:

Name Type Description Default
density float

Points per metre (default 2.0). Higher values produce denser clouds at the cost of more memory.

2.0
filled bool

If True (default), fill polygon interiors. If False, sample points only along the boundary edges.

True

Returns:

Type Description
ndarray

Array of shape (N, 2) containing [x, y] UTM coordinates, or an empty (0, 2) array if the way has no geometry.

Source code in map_data/utils/way.py
def to_pcd_points(self, density: float = 2.0, *, filled: bool = True) -> np.ndarray:
    """
    Convert the way's geometry to a dense 2-D point cloud.

    For closed polygon areas the interior is filled with a regular grid
    of points. For linestrings (or unfilled polygons) points are sampled
    along the edges at the given density. The result is cached in
    :attr:`pcd_points` after the first call.

    Parameters
    ----------
    density : float
        Points per metre (default ``2.0``). Higher values produce denser
        clouds at the cost of more memory.
    filled : bool
        If ``True`` (default), fill polygon interiors. If ``False``,
        sample points only along the boundary edges.

    Returns
    -------
    np.ndarray
        Array of shape ``(N, 2)`` containing ``[x, y]`` UTM coordinates,
        or an empty ``(0, 2)`` array if the way has no geometry.

    """
    if self.pcd_points is not None:
        return self.pcd_points

    if not self.line:
        return np.empty((0, 2))

    if filled and isinstance(self.line, Polygon):
        xmin, ymin, xmax, ymax = self.line.bounds
        x = np.arange(
            np.floor(xmin * density) / density,
            np.ceil(xmax * density) / density,
            1 / density,
        )
        y = np.arange(
            np.floor(ymin * density) / density,
            np.ceil(ymax * density) / density,
            1 / density,
        )
        xv, yv = np.meshgrid(x, y)
        candidates = MultiPoint(np.column_stack([xv.ravel(), yv.ravel()])).geoms
        pts = list(self._mask_points(candidates, self.line))
        if not pts:
            self.pcd_points = np.empty((0, 2))
        else:
            self.pcd_points = np.array([(p.x, p.y) for p in pts])
    else:
        # For LineString or unfilled Polygon
        geom = self.line
        coords = list(geom.exterior.coords if hasattr(geom, "exterior") else geom.coords)

        pcd_points = np.empty((0, 2))
        for i in range(len(coords) - 1):
            p1 = Point(coords[i])
            p2 = Point(coords[i + 1])
            _, line, _ = get_point_line(p1, p2, 0.5)
            pcd_points = np.concatenate([pcd_points, line])
        self.pcd_points = pcd_points

    return self.pcd_points