Skip to content

OSM Data Structures#

This module provides classes for working with OpenStreetMap (OSM) data elements.

Base Class#

osmdiff.osm.OSMObject #

Base class for OpenStreetMap objects.

Parameters:

Name Type Description Default
tags dict

OSM tags (key-value pairs)

{}
attribs dict

XML attributes

{}
bounds list

Bounding box [minlon, minlat, maxlon, maxlat]

None

Attributes:

Name Type Description
tags dict

OSM tags (key-value pairs)

attribs dict

XML attributes

bounds list

Bounding box [minlon, minlat, maxlon, maxlat]

Methods:

Name Description
from_xml

Create object from XML element

_parse_tags

Parse tags from XML

_parse_bounds

Parse bounds from XML

Raises:

Type Description
ValueError

If XML element is invalid

TypeError

If element type is unknown

Example:

node = Node()
node.attribs = {"lon": "0.0", "lat": "51.5"}
Source code in src/osmdiff/osm/osm.py
class OSMObject:
    """
    Base class for OpenStreetMap objects.

    Parameters:
        tags (dict): OSM tags (key-value pairs)
        attribs (dict): XML attributes
        bounds (list): Bounding box [minlon, minlat, maxlon, maxlat]

    Attributes:
        tags (dict): OSM tags (key-value pairs)
        attribs (dict): XML attributes
        bounds (list): Bounding box [minlon, minlat, maxlon, maxlat]

    Methods:
        from_xml: Create object from XML element
        _parse_tags: Parse tags from XML
        _parse_bounds: Parse bounds from XML

    Raises:
        ValueError: If XML element is invalid
        TypeError: If element type is unknown

    Example:
    ```python
    node = Node()
    node.attribs = {"lon": "0.0", "lat": "51.5"}
    ```
    """

    def __init__(
        self,
        tags: Dict[str, str] = {},
        attribs: Dict[str, str] = {},
        bounds: List[float] = None,
    ) -> None:
        """Initialize an empty OSM object."""
        self.tags = tags or {}
        self.attribs = attribs or {}
        self.bounds = bounds or None

    def __repr__(self) -> str:
        """
        String representation of the OSM object.

        Returns:
            str: Object type and ID, with additional info for ways/relations
        """
        out = "{type} {id}".format(type=type(self).__name__, id=self.attribs.get("id"))
        if type(self) == Way:
            out += " ({ways} nodes)".format(ways=len(self.nodes))
        if type(self) == Relation:
            out += " ({mem} members)".format(mem=len(self.members))
        return out

    def _parse_tags(self, elem: Element) -> None:
        """
        Parse tags from XML element.

        Args:
            elem: XML element containing tag elements
        """
        for tagelem in elem.findall("tag"):
            self.tags[tagelem.attrib["k"]] = tagelem.attrib["v"]

    def _parse_bounds(self, elem: Element) -> None:
        """
        Parse bounds from XML element.

        Args:
            elem: XML element containing bounds element
        """
        be = elem.find("bounds")
        if be is not None:
            self.bounds = [
                be.attrib["minlon"],
                be.attrib["minlat"],
                be.attrib["maxlon"],
                be.attrib["maxlat"],
            ]

    @classmethod
    def from_xml(cls, elem: Element) -> "OSMObject":
        """
        Create OSM object from XML element.

        Args:
            elem: XML element representing an OSM object

        Returns:
            OSMObject: Appropriate subclass instance

        Raises:
            ValueError: If XML element is invalid
            TypeError: If element type is unknown
        """
        if elem is None:
            raise ValueError("XML element cannot be None")

        osmtype = ""
        if elem.tag == "member":
            osmtype = elem.attrib.get("type")
            if not osmtype:
                raise ValueError("Member element missing type attribute")
        else:
            osmtype = elem.tag

        if osmtype not in ("node", "nd", "way", "relation"):
            raise TypeError(f"Unknown OSM element type: {osmtype}")

        if osmtype in ("node", "nd"):
            o = Node()
        elif osmtype == "way":
            o = Way()
            o._parse_nodes(elem)
        elif osmtype == "relation":
            o = Relation()
            o._parse_members(elem)
        else:
            pass
        o.attribs = elem.attrib
        o._parse_tags(elem)
        o._parse_bounds(elem)
        return o

    def to_dict(self) -> Dict[str, Any]:
        """
        Convert object to dictionary.

        Returns:
            Dict[str, Any]: Dictionary representation
        """
        return {
            "type": self.__class__.__name__,
            "id": self.attribs.get("id"),
            "tags": self.tags,
            "bounds": self.bounds,
        }

    def to_json(self) -> str:
        """
        Convert object to JSON string.

        Returns:
            str: JSON representation
        """
        return json.dumps(self.to_dict())

    @classmethod
    def from_file(cls, filename: str) -> "OSMObject":
        """
        Create object from XML file.

        Args:
            filename: Path to XML file

        Returns:
            OSMObject: Parsed object
        """
        with open(filename, "r") as f:
            tree = ElementTree.parse(f)
            return cls.from_xml(tree.getroot())

__init__(tags={}, attribs={}, bounds=None) #

Initialize an empty OSM object.

Source code in src/osmdiff/osm/osm.py
def __init__(
    self,
    tags: Dict[str, str] = {},
    attribs: Dict[str, str] = {},
    bounds: List[float] = None,
) -> None:
    """Initialize an empty OSM object."""
    self.tags = tags or {}
    self.attribs = attribs or {}
    self.bounds = bounds or None

__repr__() #

String representation of the OSM object.

Returns:

Name Type Description
str str

Object type and ID, with additional info for ways/relations

Source code in src/osmdiff/osm/osm.py
def __repr__(self) -> str:
    """
    String representation of the OSM object.

    Returns:
        str: Object type and ID, with additional info for ways/relations
    """
    out = "{type} {id}".format(type=type(self).__name__, id=self.attribs.get("id"))
    if type(self) == Way:
        out += " ({ways} nodes)".format(ways=len(self.nodes))
    if type(self) == Relation:
        out += " ({mem} members)".format(mem=len(self.members))
    return out

from_file(filename) classmethod #

Create object from XML file.

Parameters:

Name Type Description Default
filename str

Path to XML file

required

Returns:

Name Type Description
OSMObject OSMObject

Parsed object

Source code in src/osmdiff/osm/osm.py
@classmethod
def from_file(cls, filename: str) -> "OSMObject":
    """
    Create object from XML file.

    Args:
        filename: Path to XML file

    Returns:
        OSMObject: Parsed object
    """
    with open(filename, "r") as f:
        tree = ElementTree.parse(f)
        return cls.from_xml(tree.getroot())

from_xml(elem) classmethod #

Create OSM object from XML element.

Parameters:

Name Type Description Default
elem Element

XML element representing an OSM object

required

Returns:

Name Type Description
OSMObject OSMObject

Appropriate subclass instance

Raises:

Type Description
ValueError

If XML element is invalid

TypeError

If element type is unknown

Source code in src/osmdiff/osm/osm.py
@classmethod
def from_xml(cls, elem: Element) -> "OSMObject":
    """
    Create OSM object from XML element.

    Args:
        elem: XML element representing an OSM object

    Returns:
        OSMObject: Appropriate subclass instance

    Raises:
        ValueError: If XML element is invalid
        TypeError: If element type is unknown
    """
    if elem is None:
        raise ValueError("XML element cannot be None")

    osmtype = ""
    if elem.tag == "member":
        osmtype = elem.attrib.get("type")
        if not osmtype:
            raise ValueError("Member element missing type attribute")
    else:
        osmtype = elem.tag

    if osmtype not in ("node", "nd", "way", "relation"):
        raise TypeError(f"Unknown OSM element type: {osmtype}")

    if osmtype in ("node", "nd"):
        o = Node()
    elif osmtype == "way":
        o = Way()
        o._parse_nodes(elem)
    elif osmtype == "relation":
        o = Relation()
        o._parse_members(elem)
    else:
        pass
    o.attribs = elem.attrib
    o._parse_tags(elem)
    o._parse_bounds(elem)
    return o

to_dict() #

Convert object to dictionary.

Returns:

Type Description
Dict[str, Any]

Dict[str, Any]: Dictionary representation

Source code in src/osmdiff/osm/osm.py
def to_dict(self) -> Dict[str, Any]:
    """
    Convert object to dictionary.

    Returns:
        Dict[str, Any]: Dictionary representation
    """
    return {
        "type": self.__class__.__name__,
        "id": self.attribs.get("id"),
        "tags": self.tags,
        "bounds": self.bounds,
    }

to_json() #

Convert object to JSON string.

Returns:

Name Type Description
str str

JSON representation

Source code in src/osmdiff/osm/osm.py
def to_json(self) -> str:
    """
    Convert object to JSON string.

    Returns:
        str: JSON representation
    """
    return json.dumps(self.to_dict())

OSM Elements#

Node#

osmdiff.osm.Node #

Bases: OSMObject

Represents an OSM node (point feature).

Attributes#

lon (float): Longitude
lat (float): Latitude
__geo_interface__ (dict): GeoJSON-compatible interface, see https://gist.github.com/sgillies/2217756 for more details.

Example#

node = Node()
node.attribs = {"lon": "0.0", "lat": "51.5"}
print(node.lon, node.lat)  # 0.0, 51.5
Source code in src/osmdiff/osm/osm.py
class Node(OSMObject):
    """
    Represents an OSM node (point feature).

    ## Attributes
        lon (float): Longitude
        lat (float): Latitude
        __geo_interface__ (dict): GeoJSON-compatible interface, see https://gist.github.com/sgillies/2217756 for more details.

    ## Example
    ```python
    node = Node()
    node.attribs = {"lon": "0.0", "lat": "51.5"}
    print(node.lon, node.lat)  # 0.0, 51.5
    ```
    """

    def __init__(
        self,
        tags: Dict[str, str] = {},
        attribs: Dict[str, str] = {},
        bounds: List[float] = None,
    ):
        super().__init__(tags, attribs, bounds)

    def _validate_coords(self) -> None:
        """Validate node coordinates."""
        lon = float(self.attribs.get("lon", 0))
        lat = float(self.attribs.get("lat", 0))
        if not -90 <= lat <= 90:
            raise ValueError(f"Invalid latitude: {lat}")
        if not -180 <= lon <= 180:
            raise ValueError(f"Invalid longitude: {lon}")

    @property
    def lon(self) -> float:
        """Get longitude value."""
        self._validate_coords()
        return float(self.attribs.get("lon", 0))

    @property
    def lat(self) -> float:
        """Get latitude value."""
        self._validate_coords()
        return float(self.attribs.get("lat", 0))

    def _geo_interface(self):
        """
        GeoJSON-compatible interface.

        Returns:
            dict: GeoJSON Point geometry
        """
        return {"type": "Point", "coordinates": [self.lon, self.lat]}

    __geo_interface__ = property(_geo_interface)

    def __eq__(self, other) -> bool:
        """
        Check if two nodes are equal.

        Args:
            other (OSMObject): Another OSMObject object

        Returns:
            bool: True if nodes have same coordinates
        """
        if not isinstance(other, Node):
            return False
        return self.lon == other.lon and self.lat == other.lat

lat property #

Get latitude value.

lon property #

Get longitude value.

__eq__(other) #

Check if two nodes are equal.

Parameters:

Name Type Description Default
other OSMObject

Another OSMObject object

required

Returns:

Name Type Description
bool bool

True if nodes have same coordinates

Source code in src/osmdiff/osm/osm.py
def __eq__(self, other) -> bool:
    """
    Check if two nodes are equal.

    Args:
        other (OSMObject): Another OSMObject object

    Returns:
        bool: True if nodes have same coordinates
    """
    if not isinstance(other, Node):
        return False
    return self.lon == other.lon and self.lat == other.lat

Way#

osmdiff.osm.Way #

Bases: OSMObject

Represents an OSM way (linear feature).

Attributes#

nodes (list): List of Node objects
__geo_interface__ (dict): GeoJSON-compatible interface, see https://gist.github.com/sgillies/2217756 for more details.

Example#

way = Way()
way.nodes = [Node(), Node()]  # Add nodes
print(way.__geo_interface__["type"])  # "LineString" or "Polygon"
Source code in src/osmdiff/osm/osm.py
class Way(OSMObject):
    """
    Represents an OSM way (linear feature).

    ## Attributes
        nodes (list): List of Node objects
        __geo_interface__ (dict): GeoJSON-compatible interface, see https://gist.github.com/sgillies/2217756 for more details.
    ## Example
    ```python
    way = Way()
    way.nodes = [Node(), Node()]  # Add nodes
    print(way.__geo_interface__["type"])  # "LineString" or "Polygon"
    ```
    """

    def __init__(
        self,
        tags: Dict[str, str] = {},
        attribs: Dict[str, str] = {},
        bounds: List[float] = None,
    ):
        super().__init__(tags, attribs, bounds)
        self.nodes = []

    def is_closed(self) -> bool:
        """
        Check if the way forms a closed loop.

        Returns:
            bool: True if first and last nodes are identical
        """
        return bool(self.nodes and self.nodes[0] == self.nodes[-1])

    def length(self) -> float:
        """
        Calculate approximate length in meters.

        Returns:
            float: Length of way in meters
        """
        # Implementation using haversine formula
        pass

    def _parse_nodes(self, elem: Element):
        """
        Parse nodes from XML element.

        Args:
            elem: XML element containing nd elements
        """
        for node in elem.findall("nd"):
            self.nodes.append(OSMObject.from_xml(node))

    def _geo_interface(self):
        """
        GeoJSON-compatible interface.

        Returns:
            dict: GeoJSON LineString or Polygon geometry
        """
        geom_type = "Polygon" if self.is_closed() else "LineString"
        coordinates = [[n.lon, n.lat] for n in self.nodes]

        # For Polygon, we need to wrap coordinates in an extra list
        if geom_type == "Polygon":
            coordinates = [coordinates]

        return {"type": geom_type, "coordinates": coordinates}

    __geo_interface__ = property(_geo_interface)

is_closed() #

Check if the way forms a closed loop.

Returns:

Name Type Description
bool bool

True if first and last nodes are identical

Source code in src/osmdiff/osm/osm.py
def is_closed(self) -> bool:
    """
    Check if the way forms a closed loop.

    Returns:
        bool: True if first and last nodes are identical
    """
    return bool(self.nodes and self.nodes[0] == self.nodes[-1])

length() #

Calculate approximate length in meters.

Returns:

Name Type Description
float float

Length of way in meters

Source code in src/osmdiff/osm/osm.py
def length(self) -> float:
    """
    Calculate approximate length in meters.

    Returns:
        float: Length of way in meters
    """
    # Implementation using haversine formula
    pass

Relation#

osmdiff.osm.Relation #

Bases: OSMObject

Represents an OSM relation (collection of features).

Attributes#

members (list): List of member objects
__geo_interface__ (dict): GeoJSON-compatible interface, see https://gist.github.com/sgillies/2217756 for more details.

Example#

relation = Relation()
relation.members = [Way(), Node()]  # Add members
print(relation.__geo_interface__["type"])  # "FeatureCollection"
Source code in src/osmdiff/osm/osm.py
class Relation(OSMObject):
    """
    Represents an OSM relation (collection of features).

    ## Attributes
        members (list): List of member objects
        __geo_interface__ (dict): GeoJSON-compatible interface, see https://gist.github.com/sgillies/2217756 for more details.

    ## Example
    ```python
    relation = Relation()
    relation.members = [Way(), Node()]  # Add members
    print(relation.__geo_interface__["type"])  # "FeatureCollection"
    ```
    """

    def __init__(
        self,
        tags: Dict[str, str] = {},
        attribs: Dict[str, str] = {},
        bounds: List[float] = None,
    ):
        super().__init__(tags, attribs, bounds)
        self.members = []

    def _parse_members(self, elem: Element):
        """
        Parse members from XML element.

        Args:
            elem: XML element containing member elements
        """
        for member in elem.findall("member"):
            self.members.append(OSMObject.from_xml(member))

    def _geo_interface(self):
        """
        GeoJSON-compatible interface.

        Returns:
            dict: GeoJSON GeometryCollection
        """
        return {
            "type": "GeometryCollection",
            "geometries": [m.__geo_interface__ for m in self.members],
        }

    __geo_interface__ = property(_geo_interface)