aio_overpass.element

Typed result set members.

  1"""Typed result set members."""
  2
  3import math
  4import re
  5from collections import defaultdict
  6from collections.abc import Iterable, Iterator
  7from dataclasses import dataclass
  8from typing import Any, Final, Generic, TypeAlias, TypeVar
  9
 10from aio_overpass import Query
 11from aio_overpass.spatial import GeoJsonDict, Spatial
 12
 13import shapely.geometry
 14import shapely.ops
 15from shapely.geometry import LinearRing, LineString, MultiPolygon, Point, Polygon
 16from shapely.geometry.base import BaseGeometry, BaseMultipartGeometry
 17
 18
 19__docformat__ = "google"
 20__all__ = (
 21    "collect_elements",
 22    "Element",
 23    "Node",
 24    "Way",
 25    "Relation",
 26    "Relationship",
 27    "Bbox",
 28    "GeometryDetails",
 29    "Metadata",
 30)
 31
 32
 33Bbox: TypeAlias = tuple[float, float, float, float]
 34"""
 35The bounding box of a spatial object.
 36
 37This tuple can be understood as any of
 38    - ``(s, w, n, e)``
 39    - ``(minlat, minlon, maxlat, maxlon)``
 40    - ``(minx, miny, maxx, maxy)``
 41"""
 42
 43
 44@dataclass(kw_only=True, slots=True)
 45class Metadata:
 46    """
 47    Metadata concerning the most recent edit of an OSM element.
 48
 49    Attributes:
 50        version: The version number of the element
 51        timestamp: Timestamp (ISO 8601) of the most recent change of this element
 52        changeset: The changeset in which the element was most recently changed
 53        user_name: Name of the user that made the most recent change to the element
 54        user_id: ID of the user that made the most recent change to the element
 55    """
 56
 57    version: int
 58    timestamp: str
 59    changeset: int
 60    user_name: str
 61    user_id: int
 62
 63
 64G = TypeVar("G", bound=BaseGeometry)
 65
 66
 67@dataclass(kw_only=True, slots=True)
 68class GeometryDetails(Generic[G]):
 69    """
 70    Element geometry with more info on its validity.
 71
 72    Shapely validity is based on an [OGC standard](https://www.ogc.org/standard/sfa/).
 73
 74    For MultiPolygons, one assertion is that its elements may only touch at a finite number
 75    of Points, which means they may not share an edge on their exteriors. In terms of
 76    OSM multipolygons, it makes sense to lift this requirement, and such geometries
 77    end up in the ``accepted`` field.
 78
 79    For invalid MultiPolygon and Polygons, we use Shapely's ``make_valid()``. If and only if
 80    the amount of polygons stays the same before and after making them valid,
 81    they will end up in the ``valid`` field.
 82
 83    Attributes:
 84        valid: if set, this is the original valid geometry
 85        accepted: if set, this is the original geometry that is invalid by Shapely standards,
 86                  but accepted by us
 87        fixed: if set, this is the geometry fixed by ``make_valid()``
 88        invalid: if set, this is the original invalid geometry
 89        invalid_reason: if the original geometry is invalid by Shapely standards,
 90                        this message states why
 91    """
 92
 93    valid: G | None = None
 94    accepted: G | None = None
 95    fixed: G | None = None
 96    invalid: G | None = None
 97    invalid_reason: str | None = None
 98
 99    @property
100    def best(self) -> G | None:
101        """The "best" geometry, prioritizing ``fixed`` over ``invalid``."""
102        return self.valid or self.accepted or self.fixed or self.invalid
103
104
105@dataclass(kw_only=True, repr=False, eq=False)
106class Element(Spatial):
107    """
108    Elements are the basic components of OpenStreetMap's data.
109
110    A query's result set is made up of these elements.
111
112    Objects of this class do not necessarily describe OSM elements in their entirety.
113    The degrees of detail are decided by the ``out`` statements used in an Overpass query:
114    using ``out ids`` f.e. would only include an element's ID, but not its tags, geometry, etc.
115
116    Element geometries have coordinates in the EPSG:4326 coordinate reference system,
117    meaning that the coordinates are (latitude, longitude) tuples on the WGS 84 reference ellipsoid.
118    The geometries are Shapely objects, where the x/y coordinates refer to lat/lon.
119    Since Shapely works on the Cartesian plane, not all operations are useful: distances between
120    Shapely objects are Euclidean distances for instance - not geodetic distances.
121
122    Tags provide the semantics of elements. There are *classifying* tags, where for a certain key
123    there is a limited number of agreed upon values: the ``highway`` tag f.e. is used to identify
124    any kind of road, street or path, and to classify its importance within the road network.
125    Deviating values are perceived as erroneous. Other tags are *describing*, where any value
126    is acceptable for a certain key: the ``name`` tag is a prominent example for this.
127
128    Objects of this class are not meant to represent "derived elements" of the Overpass API,
129    that can have an entirely different data structure compared to traditional elements.
130    An example of this is the structure produced by ``out count``, but also special statements like
131    ``make`` and ``convert``.
132
133    Attributes:
134        id: A number that uniquely identifies an element of a certain type
135            (nodes, ways and relations each have their own ID space).
136            Note that this ID cannot safely be used to link to a specific object on OSM.
137            OSM IDs may change at any time, e.g. if an object is deleted and re-added.
138        tags: A list of key-value pairs that describe the element, or ``None`` if the element's
139              tags not included in the query's result set.
140        meta: Metadata of this element, or ``None`` when not using ``out meta``
141        relations: All relations that are **also in the query's result set**, and that
142                   **are known** to contain this element as a member.
143
144    References:
145        - https://wiki.openstreetmap.org/wiki/Elements
146        - https://wiki.openstreetmap.org/wiki/Map_features
147        - https://wiki.openstreetmap.org/wiki/Overpass_API/Overpass_QL#out
148        - https://wiki.openstreetmap.org/wiki/Overpass_API/Permanent_ID
149    """
150
151    __slots__ = ()
152
153    id: int
154    tags: dict[str, str] | None
155    meta: Metadata | None
156    relations: list["Relationship"]
157
158    @property
159    def base_geometry(self) -> BaseGeometry | None:
160        """
161        The element's geometry, if available.
162
163        For the specifics, refer to the documentation of the ``geometry`` property in each subclass.
164        """
165        match self:
166            case Node() | Way() | Relation():
167                return self.geometry
168            case _:
169                raise NotImplementedError
170
171    @property
172    def base_geometry_details(self) -> GeometryDetails | None:
173        """More info on the validity of ``base_geometry``."""
174        match self:
175            case Way() | Relation():
176                return self.geometry_details
177            case Node():
178                return GeometryDetails(valid=self.geometry)
179            case _:
180                raise NotImplementedError
181
182    def tag(self, key: str, default: str | None = None) -> str | None:
183        """
184        Get the tag value for the given key.
185
186        Returns ``default`` if there is no ``key`` tag.
187
188        References:
189            - https://wiki.openstreetmap.org/wiki/Tags
190        """
191        if not self.tags:
192            return default
193        return self.tags.get(key, default)
194
195    @property
196    def type(self) -> str:
197        """The element's type: "node", "way", or "relation"."""
198        match self:
199            case Node():
200                return "node"
201            case Way():
202                return "way"
203            case Relation():
204                return "relation"
205            case _:
206                raise AssertionError
207
208    @property
209    def link(self) -> str:
210        """This element on openstreetmap.org."""
211        return f"https://www.openstreetmap.org/{self.type}/{self.id}"
212
213    @property
214    def wikidata_id(self) -> str | None:
215        """
216        [Wikidata](https://www.wikidata.org) item ID of this element.
217
218        This is "perhaps, the most stable and reliable manner to obtain Permanent ID
219        of relevant spatial features".
220
221        References:
222            - https://wiki.openstreetmap.org/wiki/Permanent_ID
223            - https://wiki.openstreetmap.org/wiki/Overpass_API/Permanent_ID
224            - https://wiki.openstreetmap.org/wiki/Key:wikidata
225            - https://www.wikidata.org/wiki/Wikidata:Notability
226            - Nodes on Wikidata: https://www.wikidata.org/wiki/Property:P11693
227            - Ways on Wikidata: https://www.wikidata.org/wiki/Property:P10689
228            - Relations on Wikidata: https://www.wikidata.org/wiki/Property:P402
229        """
230        # since tag values are not enforced, use a regex to filter out bad IDs
231        if self.tags and "wikidata" in self.tags and _WIKIDATA_Q_ID.match(self.tags["wikidata"]):
232            return self.tags["wikidata"]
233        return None
234
235    @property
236    def wikidata_link(self) -> str | None:
237        """This element on wikidata.org."""
238        if self.wikidata_id:
239            return f"https://www.wikidata.org/wiki/{self.wikidata_id}"
240        return None
241
242    @property
243    def geojson(self) -> GeoJsonDict:
244        """
245        A mapping of this object, using the GeoJSON format.
246
247        Objects are mapped as the following:
248         - ``Node`` -> ``Feature`` with optional ``Point`` geometry
249         - ``Way`` -> ``Feature`` with optional ``LineString`` or ``Polygon`` geometry
250         - ``Relation`` with geometry -> ``Feature`` with ``Polygon`` or ``MultiPolygon`` geometry
251         - ``Relation`` -> ``FeatureCollection`` (nested ``Relations`` are mapped to unlocated
252           ``Features``)
253
254        ``Feature`` properties contain all the following keys if they are present for the element:
255        ``id``, ``type``, ``role``, ``tags``, ``nodes``, ``bounds``, ``center``, ``timestamp``,
256        ``version``, ``changeset``, ``user``, ``uid``.
257        The JSON object in ``properties`` is therefore very similar to the original JSON object
258        returned by Overpass, skipping only ``geometry``, ``lat`` and ``lon``.
259
260        Additionally, features inside ``FeatureCollections`` receive the special ``__rel__``
261        property. Its value is an object containing all properties of the relation the collection
262        represents. This works around the fact that ``FeatureCollections`` have no ``properties``
263        member. The benefit is that you can take relation tags into consideration when styling
264        and rendering their members on a map (f.e. with Leaflet). The downside is that these
265        properties are duplicated for every feature.
266        """
267        if isinstance(self, Relation) and not self.geometry:
268            return {
269                "type": "FeatureCollection",
270                "features": [_geojson_feature(relship) for relship in self.members],
271            }
272
273        return _geojson_feature(self)
274
275    def __repr__(self) -> str:
276        return f"{type(self).__name__}({self.id})"
277
278
279@dataclass(kw_only=True, slots=True, repr=False, eq=False)
280class Node(Element):
281    """
282    A point in space, at a specific coordinate.
283
284    Nodes are used to define standalone point features (e.g. a bench),
285    or to define the shape or "path" of a way.
286
287    Attributes:
288        geometry: A Point or ``None`` if the coordinate is not included in the query's result set.
289
290    References:
291        - https://wiki.openstreetmap.org/wiki/Node
292    """
293
294    geometry: Point | None
295
296
297@dataclass(kw_only=True, slots=True, repr=False, eq=False)
298class Way(Element):
299    """
300    A way is an ordered list of nodes.
301
302    An open way is a way whose first node is not its last node (e.g. a railway line).
303    A closed way is a way whose first node is also its last node, and may be interpreted either
304    as a closed polyline (e.g. a roundabout), an area (e.g. a patch of grass), or both
305    (e.g. a roundabout surrounding a grassy area).
306
307    Attributes:
308        node_ids: The IDs of the nodes that make up this way, or ``None`` if they are not included
309                  in the query's result set.
310        bounds: The enclosing bounding box of all nodes, or ``None`` when not using ``out bb``.
311                The ``bounds`` property of Shapely geometries can be used as an alternative.
312        center: The center of ``bounds``, or ``None`` when not using ``out center``.
313                If you need a coordinate that is inside the element's geometry, consider Shapely's
314                ``representative_point()`` and ``centroid``.
315        geometry: A Linestring if the way is open, a LinearRing if the way is closed,
316                  a Polygon if the way is closed and its tags indicate that it represents an area,
317                  or ``None`` if the geometry is not included in the query's result set.
318        geometry_details: More info on the validity of ``geometry``.
319
320    References:
321        - https://wiki.openstreetmap.org/wiki/Way
322    """
323
324    node_ids: list[int] | None
325    bounds: Bbox | None
326    center: Point | None
327    geometry: LineString | LinearRing | Polygon | None
328    geometry_details: GeometryDetails[LineString | LinearRing | Polygon] | None
329
330
331@dataclass(kw_only=True, slots=True, repr=False, eq=False)
332class Relation(Element):
333    """
334    A relation is a group of nodes and ways that have a logical or geographic relationship.
335
336    This relationship is described through its tags.
337
338    A relation may define an area geometry, which may have boundaries made up of several
339    unclosed ways. Relations of ``type=multipolygon`` may have boundaries ("outer" role) and
340    holes ("inner" role) made up of several unclosed ways.
341
342    Tags describing the multipolygon always go on the relation. The inner and outer ways are tagged
343    if they describe something in their own right. For example,
344     - a multipolygon relation may be tagged as landuse=forest if it outlines a forest,
345     - its outer ways may be tagged as barrier=fence if the forest is fenced,
346     - and its inner ways may be tagged as natural=water if there is a lake within the forest
347       boundaries.
348
349    Attributes:
350        bounds: The bounding box of this element, or ``None`` when not using ``out bb``.
351                It encloses all node and way members; relations as members have no effect.
352                The ``bounds`` property of Shapely geometries can be used as an alternative.
353        center: The center of ``bounds``, or ``None`` when not using ``out center``.
354                If you need a coordinate that is inside the element's geometry, consider Shapely's
355                ``representative_point()`` and ``centroid``.
356        members: Ordered member elements of this relation, with an optional role
357        geometry: If this relation is deemed to represent an area, these are the complex polygons
358                  whose boundaries and holes are made up of the ways inside the relation. Members
359                  that are not ways, or are not part of any polygon boundary, are not part of the
360                  result geometry. This is ``None`` if the geometry of the relation members is not
361                  included in the query's result set, or if the relation is not deemed to represent
362                  an area.
363        geometry_details: More info on the validity of ``geometry``.
364
365    References:
366        - https://wiki.openstreetmap.org/wiki/Relation
367        - https://wiki.openstreetmap.org/wiki/Relation:multipolygon
368        - https://wiki.openstreetmap.org/wiki/Relation:boundary
369    """
370
371    bounds: Bbox | None
372    center: Point | None
373    members: list["Relationship"]
374    geometry: Polygon | MultiPolygon | None
375    geometry_details: GeometryDetails[Polygon | MultiPolygon] | None
376
377    def __iter__(self) -> Iterator[tuple[str | None, Element]]:
378        """Iterates over all members in the form of ``(role, element)``."""
379        for relship in self.members:
380            yield relship.role, relship.member
381
382
383@dataclass(kw_only=True, slots=True, repr=False)
384class Relationship(Spatial):
385    """
386    The relationship of an element that is part of a relation, with an optional role.
387
388    Attributes:
389        member:     any element
390        relation:   a relation that the member is a part of
391        role:       describes the function of the member in the context of the relation
392
393    References:
394        - https://wiki.openstreetmap.org/wiki/Relation#Roles
395    """
396
397    member: Element
398    relation: Relation
399    role: str | None
400
401    @property
402    def geojson(self) -> GeoJsonDict:
403        """
404        A mapping of ``member``.
405
406        This is ``member.geojson``, with the added properties ``role`` and ``__rel__``.
407        """
408        return _geojson_feature(self)
409
410    def __repr__(self) -> str:
411        role = f" as '{self.role}'" if self.role else " "
412        return f"{type(self).__name__}({self.member}{role} in {self.relation})"
413
414
415_KNOWN_ELEMENTS: Final[set[str]] = {"node", "way", "relation"}
416
417
418_ElementKey: TypeAlias = tuple[str, int]
419"""Elements are uniquely identified by the tuple (type, id)."""
420
421_MemberKey: TypeAlias = tuple[_ElementKey, str]
422"""Relation members are identified by their element key and role."""
423
424_OverpassDict: TypeAlias = dict[str, Any]
425"""A dictionary representing a JSON object returned by the Overpass API."""
426
427
428class _ElementCollector:
429    __slots__ = (
430        "member_dict",
431        "result_set",
432        "typed_dict",
433        "untyped_dict",
434    )
435
436    def __init__(self) -> None:
437        self.result_set: list[_ElementKey] = []
438        self.typed_dict: dict[_ElementKey, Element] = {}
439        self.untyped_dict: dict[_ElementKey, _OverpassDict] = defaultdict(dict)
440        self.member_dict: dict[int, list[_MemberKey]] = defaultdict(list)
441
442
443def collect_elements(query: Query) -> list[Element]:
444    """
445    Produce typed elements from the result set of a query.
446
447    This function exclusively collects elements that are of type "node", "way", or "relation".
448
449    Element data is "conflated", which means that if elements appear more than once in a
450    result set, their data is merged. This is useful f.e. when querying tags for relation members:
451    using ``rel(...); out tags;`` will only print tags for relation itself, not its members.
452    For those you will have to recurse down from the relation, which means members will show
453    up twice in the result set: once untagged as a member of the relation, and once tagged at
454    the top level. This function will have these two occurrences point to the same, single object.
455
456    The order of elements and relation members is retained.
457
458    If you want to query Overpass' "area" elements, you should use the `way(pivot)` or `rel(pivot)`
459    filter to select the element of the chosen type that defines the outline of the given area.
460    Otherwise, they will be ignored.
461
462    Derived elements with other types - f.e. produced by ``make`` and ``convert`` statements
463    or when using ``out count`` - are ignored. If you need to work with other derived elements,
464    you should not use this function.
465
466    Args:
467        query: a finished query
468
469    Returns:
470        the result set as a list of typed elements
471
472    Raises:
473        ValueError: If the input query is unfinished/has no result set.
474        KeyError: The only times there should be missing keys is when either using ``out noids``,
475                  or when building derived elements that are missing common keys.
476    """
477    if not query.done:
478        msg = "query has no result set"
479        raise ValueError(msg)
480
481    collector = _ElementCollector()
482    _collect_untyped(query, collector)
483    _collect_typed(collector)
484    _collect_relationships(collector)
485    return [collector.typed_dict[elem_key] for elem_key in collector.result_set]
486
487
488def _collect_untyped(query: Query, collector: _ElementCollector) -> None:
489    if query.result_set is None:
490        raise AssertionError
491
492    # Here we populate 'untyped_dict' with both top level elements, and
493    # relation members, while conflating their data if they appear as both.
494    # We also populate 'member_dict'.
495    for elem_dict in query.result_set:
496        if elem_dict.get("type") not in _KNOWN_ELEMENTS:
497            continue
498
499        key: _ElementKey = (elem_dict["type"], elem_dict["id"])
500
501        collector.result_set.append(key)
502        collector.untyped_dict[key].update(elem_dict)
503
504        if elem_dict["type"] != "relation":
505            continue
506
507        for mem in elem_dict["members"]:
508            key = (mem["type"], mem["ref"])
509            collector.untyped_dict[key].update(mem)
510            collector.member_dict[elem_dict["id"]].append((key, mem.get("role")))
511
512
513def _collect_typed(collector: _ElementCollector) -> None:
514    for elem_key, elem_dict in collector.untyped_dict.items():
515        elem_type, elem_id = elem_key
516
517        geometry = _geometry(elem_dict)
518
519        center = Point(elem_dict["center"].values()) if "center" in elem_dict else None
520        bounds = tuple(elem_dict["bounds"].values()) if "bounds" in elem_dict else None
521        assert bounds is None or len(bounds) == 4
522
523        meta: Metadata | None = None
524        if "timestamp" in elem_dict:
525            meta = Metadata(
526                timestamp=elem_dict["timestamp"],
527                version=elem_dict["version"],
528                changeset=elem_dict["changeset"],
529                user_name=elem_dict["user"],
530                user_id=elem_dict["uid"],
531            )
532
533        elem: Element
534
535        match elem_type:
536            case "node":
537                assert isinstance(geometry, Point | None)
538                assert geometry is None or geometry.is_valid
539                elem = Node(
540                    id=elem_id,
541                    tags=elem_dict.get("tags"),
542                    geometry=geometry,
543                    meta=meta,
544                    relations=[],  # add later
545                )
546            case "way":
547                assert isinstance(geometry, LineString | LinearRing | Polygon | None)
548                geometry_details = _try_validate_geometry(geometry) if geometry else None
549                elem = Way(
550                    id=elem_id,
551                    tags=elem_dict.get("tags"),
552                    geometry=geometry if geometry_details is None else geometry_details.best,
553                    geometry_details=geometry_details,
554                    meta=meta,
555                    node_ids=elem_dict.get("nodes"),
556                    bounds=bounds,
557                    center=center,
558                    relations=[],  # add later
559                )
560            case "relation":
561                assert isinstance(geometry, Polygon | MultiPolygon | None)
562                geometry_details = _try_validate_geometry(geometry) if geometry else None
563                elem = Relation(
564                    id=elem_id,
565                    tags=elem_dict.get("tags"),
566                    geometry=geometry if geometry_details is None else geometry_details.best,
567                    geometry_details=geometry_details,
568                    meta=meta,
569                    bounds=bounds,
570                    center=center,
571                    relations=[],  # add later
572                    members=[],  # add later
573                )
574            case _:
575                raise AssertionError
576
577        collector.typed_dict[elem_key] = elem
578
579
580def _collect_relationships(collector: _ElementCollector) -> None:
581    for rel_id, mem_roles in collector.member_dict.items():
582        rel = collector.typed_dict[("relation", rel_id)]
583        assert isinstance(rel, Relation)
584
585        for mem_key, mem_role in mem_roles:
586            mem = collector.typed_dict[mem_key]
587            relship = Relationship(member=mem, relation=rel, role=mem_role or None)
588            mem.relations.append(relship)
589            rel.members.append(relship)
590
591
592def _try_validate_geometry(geom: G) -> GeometryDetails[G]:
593    if geom.is_valid:
594        return GeometryDetails(valid=geom)
595
596    invalid_reason = shapely.is_valid_reason(geom)
597
598    if invalid_reason.startswith("Self-intersection") and isinstance(geom, MultiPolygon):
599        # we allow self-intersecting multi-polygons, if
600        # (1) the intersection is just lines or points, and
601        # (2) all the polygons inside are valid
602        intersection = shapely.intersection_all(geom.geoms)
603        accept = not isinstance(intersection, Polygon | MultiPolygon) and all(
604            poly.is_valid for poly in geom.geoms
605        )
606
607        if accept:
608            return GeometryDetails(accepted=geom, invalid_reason=invalid_reason)
609
610        return GeometryDetails(invalid=geom, invalid_reason=invalid_reason)
611
612    if isinstance(geom, Polygon):
613        valid_polygons = [g for g in _flatten(shapely.make_valid(geom)) if isinstance(g, Polygon)]
614        if len(valid_polygons) == 1:
615            return GeometryDetails(
616                fixed=valid_polygons[0],
617                invalid=geom,
618                invalid_reason=invalid_reason,
619            )
620
621    if isinstance(geom, MultiPolygon):
622        valid_polygons = [g for g in _flatten(shapely.make_valid(geom)) if isinstance(g, Polygon)]
623        if len(valid_polygons) == len(geom.geoms):
624            return GeometryDetails(
625                fixed=MultiPolygon(valid_polygons),
626                invalid=geom,
627                invalid_reason=invalid_reason,
628            )
629
630    return GeometryDetails(invalid=geom, invalid_reason=invalid_reason)
631
632
633def _geometry(raw_elem: _OverpassDict) -> BaseGeometry | None:
634    """
635    Construct the geometry a given OSM element makes up.
636
637    Args:
638        raw_elem: an element from a query's result set
639
640    Returns:
641        - None if there is no geometry available for this element.
642        - Point when given a node.
643        - LineString when given an open way.
644        - LinearRing when given a closed way, that supposedly is *not* an area.
645        - Polygon when given a closed way, that supposedly is an area.
646        - (Multi-)Polygons containing given a (multipolygon) relation.
647          Relation members that are not ways, or are not part of any polygon boundary, are
648          not part of the result geometry.
649
650    Raises:
651        ValueError: if element is not of type 'node', 'way', 'relation', or 'area'
652    """
653    if raw_elem.get("type") not in _KNOWN_ELEMENTS:
654        msg = "expected element of type 'node', 'way', 'relation', or 'area'"
655        raise ValueError(msg)
656
657    if raw_elem["type"] == "node":
658        lat, lon = raw_elem.get("lat"), raw_elem.get("lon")
659        if lat and lon:
660            return Point(lat, lon)
661
662    if raw_elem["type"] == "way":
663        ls = _line(raw_elem)
664        if ls and ls.is_ring and _is_area_element(raw_elem):
665            return Polygon(ls)
666        return ls
667
668    if _is_area_element(raw_elem):
669        outers = (
670            ls
671            for ls in (
672                _line(mem) for mem in raw_elem.get("members", ()) if mem.get("role") == "outer"
673            )
674            if ls
675        )
676        inners = (
677            ls
678            for ls in (
679                _line(mem) for mem in raw_elem.get("members", ()) if mem.get("role") == "inner"
680            )
681            if ls
682        )
683
684        shells = [ls for ls in _flatten(shapely.ops.linemerge(outers)) if ls.is_closed]
685        holes = [ls for ls in _flatten(shapely.ops.linemerge(inners)) if ls.is_closed]
686
687        polys = [
688            Polygon(shell=shell, holes=[hole for hole in holes if shell.contains(hole)])
689            for shell in shells
690        ]
691
692        if len(polys) == 1:
693            return polys[0]
694
695        return MultiPolygon(polys)
696
697    return None
698
699
700def _line(way: _OverpassDict) -> LineString | LinearRing | None:
701    """Returns the geometry of a way in the result set."""
702    if "geometry" not in way or len(way["geometry"]) < 2:
703        return None
704    is_ring = way["geometry"][0] == way["geometry"][-1]
705    cls = LinearRing if is_ring else LineString
706    return cls((c["lat"], c["lon"]) for c in way["geometry"])
707
708
709def _flatten(obj: BaseGeometry) -> Iterable[BaseGeometry]:
710    """Recursively flattens multipart geometries."""
711    if isinstance(obj, BaseMultipartGeometry):
712        return (nested for contained in obj.geoms for nested in _flatten(contained))
713    return (obj,)
714
715
716def _is_area_element(el: _OverpassDict) -> bool:
717    """
718    Decide if ``el`` likely represents an area, and should be viewed as a (multi-)polygon.
719
720    Args:
721        el: a way or relation from a query's result set
722
723    Returns:
724        ``False`` if the input is not a relation or closed way.
725        ``False``, unless there are tags which indicate that the way represents an area.
726
727    References:
728        - https://wiki.openstreetmap.org/wiki/Overpass_API/Areas
729        - https://github.com/drolbr/Overpass-API/blob/master/src/rules/areas.osm3s
730          (from 2018-04-09)
731        - https://wiki.openstreetmap.org/wiki/Overpass_turbo/Polygon_Features
732        - https://github.com/tyrasd/osm-polygon-features/blob/master/polygon-features.json
733          (from 2016-11-03)
734    """
735    # Check if the element is explicitly an area
736    if el["type"] == "area":
737        return True
738
739    # Check if a given way is open
740    if el["type"] == "way" and ("geometry" not in el or el["geometry"][0] != el["geometry"][-1]):
741        return False
742
743    # Assume not an area if there are no tags available
744    if "tags" not in el:
745        return False
746
747    tags = el["tags"]
748
749    # Check if the element is explicitly tagged as not area
750    if tags.get("area") == "no":
751        return False
752
753    # Check if there is a tag where any value other than 'no' suggests area
754    # (note: Overpass may require the "name" tag as well)
755    if any(tags.get(name, "no") != "no" for name in _AREA_TAG_NAMES):
756        return True
757
758    # Check if there are tag values that suggest area
759    # (note: Overpass may require the "name" tag as well)
760    return any(
761        (
762            v in _AREA_TAG_VALUES_ONE_OF.get(k, ())
763            or v not in _AREA_TAG_VALUES_NONE_OF.get(k, (v,))
764            for k, v in tags.items()
765        )
766    )
767
768
769_AREA_TAG_NAMES: Final[set[str]] = {
770    "area",
771    "area:highway",
772    "amenity",
773    "boundary",
774    "building",
775    "building:part",
776    "craft",
777    "golf",
778    "historic",
779    "indoor",
780    "landuse",
781    "leisure",
782    "military",
783    "office",
784    "place",
785    "public_transport",
786    "ruins",
787    "shop",
788    "tourism",
789    # for relations
790    "admin_level",
791    "postal_code",
792    "addr:postcode",
793}
794
795_AREA_TAG_VALUES_ONE_OF: Final[dict[str, set[str]]] = {
796    "barrier": {"city_wall", "ditch", "hedge", "retaining_wall", "wall", "spikes"},
797    "highway": {"services", "rest_area", "escape", "elevator"},
798    "power": {"plant", "substation", "generator", "transformer"},
799    "railway": {"station", "turntable", "roundhouse", "platform"},
800    "waterway": {"riverbank", "dock", "boatyard", "dam"},
801    # for relations
802    "type": {"multipolygon"},
803}
804
805_AREA_TAG_VALUES_NONE_OF: Final[dict[str, set[str]]] = {
806    "aeroway": {"no", "taxiway"},
807    "man_made": {"no", "cutline", "embankment", "pipeline"},
808    "natural": {"no", "coastline", "cliff", "ridge", "arete", "tree_row"},
809}
810
811_WIKIDATA_Q_ID: Final[re.Pattern[str]] = re.compile(r"^Q\d+$")
812
813
814def _geojson_properties(obj: Element | Relationship) -> GeoJsonDict:
815    elem = obj if isinstance(obj, Element) else obj.member
816
817    properties = {
818        "id": elem.id,
819        "type": elem.type,
820        "tags": elem.tags,
821        "timestamp": elem.meta.timestamp if elem.meta else None,
822        "version": elem.meta.version if elem.meta else None,
823        "changeset": elem.meta.changeset if elem.meta else None,
824        "user": elem.meta.user_name if elem.meta else None,
825        "uid": elem.meta.user_id if elem.meta else None,
826        "nodes": getattr(elem, "nodes", None),
827    }
828
829    if isinstance(elem, Way | Relation):
830        # TODO: these are lat/lon order, as opposed to the geometry in GeoJSON - problem?
831        properties["bounds"] = elem.bounds
832        properties["center"] = elem.center.coords[0] if elem.center else None
833
834    properties = {k: v for k, v in properties.items() if v is not None}
835
836    if isinstance(obj, Relationship):
837        properties["role"] = obj.role or ""
838        properties["__rel__"] = _geojson_properties(obj.relation)
839
840    return properties
841
842
843def _geojson_geometry(obj: Element | Relationship) -> GeoJsonDict | None:
844    elem = obj if isinstance(obj, Element) else obj.member
845
846    if not elem.base_geometry:
847        return None
848
849    # Flip coordinates for GeoJSON compliance.
850    geom = shapely.ops.transform(lambda lat, lon: (lon, lat), elem.base_geometry)
851
852    # GeoJSON-like mapping that implements __geo_interface__.
853    mapping = shapely.geometry.mapping(geom)
854
855    # This geometry does not exist in GeoJSON.
856    if mapping["type"] == "LinearRing":
857        mapping["type"] = "LineString"
858
859    return mapping
860
861
862def _geojson_bbox(obj: Element | Relationship) -> Bbox | None:
863    elem = obj if isinstance(obj, Element) else obj.member
864
865    geom = elem.base_geometry
866    if not geom:
867        return elem.bounds if isinstance(elem, Way | Relation) else None
868
869    bounds = geom.bounds  # can be (nan, nan, nan, nan)
870    if not any(math.isnan(c) for c in bounds):
871        (minlat, minlon, maxlat, maxlon) = bounds
872        return minlon, minlat, maxlon, maxlat
873
874    return None
875
876
877def _geojson_feature(obj: Element | Relationship) -> GeoJsonDict:
878    feature: GeoJsonDict = {
879        "type": "Feature",
880        "geometry": _geojson_geometry(obj),
881        "properties": _geojson_properties(obj),
882    }
883
884    bbox = _geojson_bbox(obj)
885    if bbox:
886        feature["bbox"] = bbox
887
888    return feature
def collect_elements(query: aio_overpass.Query) -> list[Element]:
444def collect_elements(query: Query) -> list[Element]:
445    """
446    Produce typed elements from the result set of a query.
447
448    This function exclusively collects elements that are of type "node", "way", or "relation".
449
450    Element data is "conflated", which means that if elements appear more than once in a
451    result set, their data is merged. This is useful f.e. when querying tags for relation members:
452    using ``rel(...); out tags;`` will only print tags for relation itself, not its members.
453    For those you will have to recurse down from the relation, which means members will show
454    up twice in the result set: once untagged as a member of the relation, and once tagged at
455    the top level. This function will have these two occurrences point to the same, single object.
456
457    The order of elements and relation members is retained.
458
459    If you want to query Overpass' "area" elements, you should use the `way(pivot)` or `rel(pivot)`
460    filter to select the element of the chosen type that defines the outline of the given area.
461    Otherwise, they will be ignored.
462
463    Derived elements with other types - f.e. produced by ``make`` and ``convert`` statements
464    or when using ``out count`` - are ignored. If you need to work with other derived elements,
465    you should not use this function.
466
467    Args:
468        query: a finished query
469
470    Returns:
471        the result set as a list of typed elements
472
473    Raises:
474        ValueError: If the input query is unfinished/has no result set.
475        KeyError: The only times there should be missing keys is when either using ``out noids``,
476                  or when building derived elements that are missing common keys.
477    """
478    if not query.done:
479        msg = "query has no result set"
480        raise ValueError(msg)
481
482    collector = _ElementCollector()
483    _collect_untyped(query, collector)
484    _collect_typed(collector)
485    _collect_relationships(collector)
486    return [collector.typed_dict[elem_key] for elem_key in collector.result_set]

Produce typed elements from the result set of a query.

This function exclusively collects elements that are of type "node", "way", or "relation".

Element data is "conflated", which means that if elements appear more than once in a result set, their data is merged. This is useful f.e. when querying tags for relation members: using rel(...); out tags; will only print tags for relation itself, not its members. For those you will have to recurse down from the relation, which means members will show up twice in the result set: once untagged as a member of the relation, and once tagged at the top level. This function will have these two occurrences point to the same, single object.

The order of elements and relation members is retained.

If you want to query Overpass' "area" elements, you should use the way(pivot) or rel(pivot) filter to select the element of the chosen type that defines the outline of the given area. Otherwise, they will be ignored.

Derived elements with other types - f.e. produced by make and convert statements or when using out count - are ignored. If you need to work with other derived elements, you should not use this function.

Arguments:
  • query: a finished query
Returns:

the result set as a list of typed elements

Raises:
  • ValueError: If the input query is unfinished/has no result set.
  • KeyError: The only times there should be missing keys is when either using out noids, or when building derived elements that are missing common keys.
@dataclass(kw_only=True, repr=False, eq=False)
class Element(aio_overpass.spatial.Spatial):
106@dataclass(kw_only=True, repr=False, eq=False)
107class Element(Spatial):
108    """
109    Elements are the basic components of OpenStreetMap's data.
110
111    A query's result set is made up of these elements.
112
113    Objects of this class do not necessarily describe OSM elements in their entirety.
114    The degrees of detail are decided by the ``out`` statements used in an Overpass query:
115    using ``out ids`` f.e. would only include an element's ID, but not its tags, geometry, etc.
116
117    Element geometries have coordinates in the EPSG:4326 coordinate reference system,
118    meaning that the coordinates are (latitude, longitude) tuples on the WGS 84 reference ellipsoid.
119    The geometries are Shapely objects, where the x/y coordinates refer to lat/lon.
120    Since Shapely works on the Cartesian plane, not all operations are useful: distances between
121    Shapely objects are Euclidean distances for instance - not geodetic distances.
122
123    Tags provide the semantics of elements. There are *classifying* tags, where for a certain key
124    there is a limited number of agreed upon values: the ``highway`` tag f.e. is used to identify
125    any kind of road, street or path, and to classify its importance within the road network.
126    Deviating values are perceived as erroneous. Other tags are *describing*, where any value
127    is acceptable for a certain key: the ``name`` tag is a prominent example for this.
128
129    Objects of this class are not meant to represent "derived elements" of the Overpass API,
130    that can have an entirely different data structure compared to traditional elements.
131    An example of this is the structure produced by ``out count``, but also special statements like
132    ``make`` and ``convert``.
133
134    Attributes:
135        id: A number that uniquely identifies an element of a certain type
136            (nodes, ways and relations each have their own ID space).
137            Note that this ID cannot safely be used to link to a specific object on OSM.
138            OSM IDs may change at any time, e.g. if an object is deleted and re-added.
139        tags: A list of key-value pairs that describe the element, or ``None`` if the element's
140              tags not included in the query's result set.
141        meta: Metadata of this element, or ``None`` when not using ``out meta``
142        relations: All relations that are **also in the query's result set**, and that
143                   **are known** to contain this element as a member.
144
145    References:
146        - https://wiki.openstreetmap.org/wiki/Elements
147        - https://wiki.openstreetmap.org/wiki/Map_features
148        - https://wiki.openstreetmap.org/wiki/Overpass_API/Overpass_QL#out
149        - https://wiki.openstreetmap.org/wiki/Overpass_API/Permanent_ID
150    """
151
152    __slots__ = ()
153
154    id: int
155    tags: dict[str, str] | None
156    meta: Metadata | None
157    relations: list["Relationship"]
158
159    @property
160    def base_geometry(self) -> BaseGeometry | None:
161        """
162        The element's geometry, if available.
163
164        For the specifics, refer to the documentation of the ``geometry`` property in each subclass.
165        """
166        match self:
167            case Node() | Way() | Relation():
168                return self.geometry
169            case _:
170                raise NotImplementedError
171
172    @property
173    def base_geometry_details(self) -> GeometryDetails | None:
174        """More info on the validity of ``base_geometry``."""
175        match self:
176            case Way() | Relation():
177                return self.geometry_details
178            case Node():
179                return GeometryDetails(valid=self.geometry)
180            case _:
181                raise NotImplementedError
182
183    def tag(self, key: str, default: str | None = None) -> str | None:
184        """
185        Get the tag value for the given key.
186
187        Returns ``default`` if there is no ``key`` tag.
188
189        References:
190            - https://wiki.openstreetmap.org/wiki/Tags
191        """
192        if not self.tags:
193            return default
194        return self.tags.get(key, default)
195
196    @property
197    def type(self) -> str:
198        """The element's type: "node", "way", or "relation"."""
199        match self:
200            case Node():
201                return "node"
202            case Way():
203                return "way"
204            case Relation():
205                return "relation"
206            case _:
207                raise AssertionError
208
209    @property
210    def link(self) -> str:
211        """This element on openstreetmap.org."""
212        return f"https://www.openstreetmap.org/{self.type}/{self.id}"
213
214    @property
215    def wikidata_id(self) -> str | None:
216        """
217        [Wikidata](https://www.wikidata.org) item ID of this element.
218
219        This is "perhaps, the most stable and reliable manner to obtain Permanent ID
220        of relevant spatial features".
221
222        References:
223            - https://wiki.openstreetmap.org/wiki/Permanent_ID
224            - https://wiki.openstreetmap.org/wiki/Overpass_API/Permanent_ID
225            - https://wiki.openstreetmap.org/wiki/Key:wikidata
226            - https://www.wikidata.org/wiki/Wikidata:Notability
227            - Nodes on Wikidata: https://www.wikidata.org/wiki/Property:P11693
228            - Ways on Wikidata: https://www.wikidata.org/wiki/Property:P10689
229            - Relations on Wikidata: https://www.wikidata.org/wiki/Property:P402
230        """
231        # since tag values are not enforced, use a regex to filter out bad IDs
232        if self.tags and "wikidata" in self.tags and _WIKIDATA_Q_ID.match(self.tags["wikidata"]):
233            return self.tags["wikidata"]
234        return None
235
236    @property
237    def wikidata_link(self) -> str | None:
238        """This element on wikidata.org."""
239        if self.wikidata_id:
240            return f"https://www.wikidata.org/wiki/{self.wikidata_id}"
241        return None
242
243    @property
244    def geojson(self) -> GeoJsonDict:
245        """
246        A mapping of this object, using the GeoJSON format.
247
248        Objects are mapped as the following:
249         - ``Node`` -> ``Feature`` with optional ``Point`` geometry
250         - ``Way`` -> ``Feature`` with optional ``LineString`` or ``Polygon`` geometry
251         - ``Relation`` with geometry -> ``Feature`` with ``Polygon`` or ``MultiPolygon`` geometry
252         - ``Relation`` -> ``FeatureCollection`` (nested ``Relations`` are mapped to unlocated
253           ``Features``)
254
255        ``Feature`` properties contain all the following keys if they are present for the element:
256        ``id``, ``type``, ``role``, ``tags``, ``nodes``, ``bounds``, ``center``, ``timestamp``,
257        ``version``, ``changeset``, ``user``, ``uid``.
258        The JSON object in ``properties`` is therefore very similar to the original JSON object
259        returned by Overpass, skipping only ``geometry``, ``lat`` and ``lon``.
260
261        Additionally, features inside ``FeatureCollections`` receive the special ``__rel__``
262        property. Its value is an object containing all properties of the relation the collection
263        represents. This works around the fact that ``FeatureCollections`` have no ``properties``
264        member. The benefit is that you can take relation tags into consideration when styling
265        and rendering their members on a map (f.e. with Leaflet). The downside is that these
266        properties are duplicated for every feature.
267        """
268        if isinstance(self, Relation) and not self.geometry:
269            return {
270                "type": "FeatureCollection",
271                "features": [_geojson_feature(relship) for relship in self.members],
272            }
273
274        return _geojson_feature(self)
275
276    def __repr__(self) -> str:
277        return f"{type(self).__name__}({self.id})"

Elements are the basic components of OpenStreetMap's data.

A query's result set is made up of these elements.

Objects of this class do not necessarily describe OSM elements in their entirety. The degrees of detail are decided by the out statements used in an Overpass query: using out ids f.e. would only include an element's ID, but not its tags, geometry, etc.

Element geometries have coordinates in the EPSG:4326 coordinate reference system, meaning that the coordinates are (latitude, longitude) tuples on the WGS 84 reference ellipsoid. The geometries are Shapely objects, where the x/y coordinates refer to lat/lon. Since Shapely works on the Cartesian plane, not all operations are useful: distances between Shapely objects are Euclidean distances for instance - not geodetic distances.

Tags provide the semantics of elements. There are classifying tags, where for a certain key there is a limited number of agreed upon values: the highway tag f.e. is used to identify any kind of road, street or path, and to classify its importance within the road network. Deviating values are perceived as erroneous. Other tags are describing, where any value is acceptable for a certain key: the name tag is a prominent example for this.

Objects of this class are not meant to represent "derived elements" of the Overpass API, that can have an entirely different data structure compared to traditional elements. An example of this is the structure produced by out count, but also special statements like make and convert.

Attributes:
  • id: A number that uniquely identifies an element of a certain type (nodes, ways and relations each have their own ID space). Note that this ID cannot safely be used to link to a specific object on OSM. OSM IDs may change at any time, e.g. if an object is deleted and re-added.
  • tags: A list of key-value pairs that describe the element, or None if the element's tags not included in the query's result set.
  • meta: Metadata of this element, or None when not using out meta
  • relations: All relations that are also in the query's result set, and that are known to contain this element as a member.
References:
Element( *, id: int, tags: dict[str, str] | None, meta: Metadata | None, relations: list[Relationship])
id: int
tags: dict[str, str] | None
meta: Metadata | None
relations: list[Relationship]
base_geometry: shapely.geometry.base.BaseGeometry | None
159    @property
160    def base_geometry(self) -> BaseGeometry | None:
161        """
162        The element's geometry, if available.
163
164        For the specifics, refer to the documentation of the ``geometry`` property in each subclass.
165        """
166        match self:
167            case Node() | Way() | Relation():
168                return self.geometry
169            case _:
170                raise NotImplementedError

The element's geometry, if available.

For the specifics, refer to the documentation of the geometry property in each subclass.

base_geometry_details: GeometryDetails | None
172    @property
173    def base_geometry_details(self) -> GeometryDetails | None:
174        """More info on the validity of ``base_geometry``."""
175        match self:
176            case Way() | Relation():
177                return self.geometry_details
178            case Node():
179                return GeometryDetails(valid=self.geometry)
180            case _:
181                raise NotImplementedError

More info on the validity of base_geometry.

def tag(self, key: str, default: str | None = None) -> str | None:
183    def tag(self, key: str, default: str | None = None) -> str | None:
184        """
185        Get the tag value for the given key.
186
187        Returns ``default`` if there is no ``key`` tag.
188
189        References:
190            - https://wiki.openstreetmap.org/wiki/Tags
191        """
192        if not self.tags:
193            return default
194        return self.tags.get(key, default)

Get the tag value for the given key.

Returns default if there is no key tag.

References:
type: str
196    @property
197    def type(self) -> str:
198        """The element's type: "node", "way", or "relation"."""
199        match self:
200            case Node():
201                return "node"
202            case Way():
203                return "way"
204            case Relation():
205                return "relation"
206            case _:
207                raise AssertionError

The element's type: "node", "way", or "relation".

wikidata_id: str | None
214    @property
215    def wikidata_id(self) -> str | None:
216        """
217        [Wikidata](https://www.wikidata.org) item ID of this element.
218
219        This is "perhaps, the most stable and reliable manner to obtain Permanent ID
220        of relevant spatial features".
221
222        References:
223            - https://wiki.openstreetmap.org/wiki/Permanent_ID
224            - https://wiki.openstreetmap.org/wiki/Overpass_API/Permanent_ID
225            - https://wiki.openstreetmap.org/wiki/Key:wikidata
226            - https://www.wikidata.org/wiki/Wikidata:Notability
227            - Nodes on Wikidata: https://www.wikidata.org/wiki/Property:P11693
228            - Ways on Wikidata: https://www.wikidata.org/wiki/Property:P10689
229            - Relations on Wikidata: https://www.wikidata.org/wiki/Property:P402
230        """
231        # since tag values are not enforced, use a regex to filter out bad IDs
232        if self.tags and "wikidata" in self.tags and _WIKIDATA_Q_ID.match(self.tags["wikidata"]):
233            return self.tags["wikidata"]
234        return None
geojson: dict[str, typing.Any]
243    @property
244    def geojson(self) -> GeoJsonDict:
245        """
246        A mapping of this object, using the GeoJSON format.
247
248        Objects are mapped as the following:
249         - ``Node`` -> ``Feature`` with optional ``Point`` geometry
250         - ``Way`` -> ``Feature`` with optional ``LineString`` or ``Polygon`` geometry
251         - ``Relation`` with geometry -> ``Feature`` with ``Polygon`` or ``MultiPolygon`` geometry
252         - ``Relation`` -> ``FeatureCollection`` (nested ``Relations`` are mapped to unlocated
253           ``Features``)
254
255        ``Feature`` properties contain all the following keys if they are present for the element:
256        ``id``, ``type``, ``role``, ``tags``, ``nodes``, ``bounds``, ``center``, ``timestamp``,
257        ``version``, ``changeset``, ``user``, ``uid``.
258        The JSON object in ``properties`` is therefore very similar to the original JSON object
259        returned by Overpass, skipping only ``geometry``, ``lat`` and ``lon``.
260
261        Additionally, features inside ``FeatureCollections`` receive the special ``__rel__``
262        property. Its value is an object containing all properties of the relation the collection
263        represents. This works around the fact that ``FeatureCollections`` have no ``properties``
264        member. The benefit is that you can take relation tags into consideration when styling
265        and rendering their members on a map (f.e. with Leaflet). The downside is that these
266        properties are duplicated for every feature.
267        """
268        if isinstance(self, Relation) and not self.geometry:
269            return {
270                "type": "FeatureCollection",
271                "features": [_geojson_feature(relship) for relship in self.members],
272            }
273
274        return _geojson_feature(self)

A mapping of this object, using the GeoJSON format.

Objects are mapped as the following:
  • Node -> Feature with optional Point geometry
  • Way -> Feature with optional LineString or Polygon geometry
  • Relation with geometry -> Feature with Polygon or MultiPolygon geometry
  • Relation -> FeatureCollection (nested Relations are mapped to unlocated Features)

Feature properties contain all the following keys if they are present for the element: id, type, role, tags, nodes, bounds, center, timestamp, version, changeset, user, uid. The JSON object in properties is therefore very similar to the original JSON object returned by Overpass, skipping only geometry, lat and lon.

Additionally, features inside FeatureCollections receive the special __rel__ property. Its value is an object containing all properties of the relation the collection represents. This works around the fact that FeatureCollections have no properties member. The benefit is that you can take relation tags into consideration when styling and rendering their members on a map (f.e. with Leaflet). The downside is that these properties are duplicated for every feature.

@dataclass(kw_only=True, slots=True, repr=False, eq=False)
class Node(Element):
280@dataclass(kw_only=True, slots=True, repr=False, eq=False)
281class Node(Element):
282    """
283    A point in space, at a specific coordinate.
284
285    Nodes are used to define standalone point features (e.g. a bench),
286    or to define the shape or "path" of a way.
287
288    Attributes:
289        geometry: A Point or ``None`` if the coordinate is not included in the query's result set.
290
291    References:
292        - https://wiki.openstreetmap.org/wiki/Node
293    """
294
295    geometry: Point | None

A point in space, at a specific coordinate.

Nodes are used to define standalone point features (e.g. a bench), or to define the shape or "path" of a way.

Attributes:
  • geometry: A Point or None if the coordinate is not included in the query's result set.
References:
Node( *, id: int, tags: dict[str, str] | None, meta: Metadata | None, relations: list[Relationship], geometry: shapely.geometry.point.Point | None)
geometry: shapely.geometry.point.Point | None
id: int
tags: dict[str, str] | None
meta: Metadata | None
relations: list[Relationship]
@dataclass(kw_only=True, slots=True, repr=False, eq=False)
class Way(Element):
298@dataclass(kw_only=True, slots=True, repr=False, eq=False)
299class Way(Element):
300    """
301    A way is an ordered list of nodes.
302
303    An open way is a way whose first node is not its last node (e.g. a railway line).
304    A closed way is a way whose first node is also its last node, and may be interpreted either
305    as a closed polyline (e.g. a roundabout), an area (e.g. a patch of grass), or both
306    (e.g. a roundabout surrounding a grassy area).
307
308    Attributes:
309        node_ids: The IDs of the nodes that make up this way, or ``None`` if they are not included
310                  in the query's result set.
311        bounds: The enclosing bounding box of all nodes, or ``None`` when not using ``out bb``.
312                The ``bounds`` property of Shapely geometries can be used as an alternative.
313        center: The center of ``bounds``, or ``None`` when not using ``out center``.
314                If you need a coordinate that is inside the element's geometry, consider Shapely's
315                ``representative_point()`` and ``centroid``.
316        geometry: A Linestring if the way is open, a LinearRing if the way is closed,
317                  a Polygon if the way is closed and its tags indicate that it represents an area,
318                  or ``None`` if the geometry is not included in the query's result set.
319        geometry_details: More info on the validity of ``geometry``.
320
321    References:
322        - https://wiki.openstreetmap.org/wiki/Way
323    """
324
325    node_ids: list[int] | None
326    bounds: Bbox | None
327    center: Point | None
328    geometry: LineString | LinearRing | Polygon | None
329    geometry_details: GeometryDetails[LineString | LinearRing | Polygon] | None

A way is an ordered list of nodes.

An open way is a way whose first node is not its last node (e.g. a railway line). A closed way is a way whose first node is also its last node, and may be interpreted either as a closed polyline (e.g. a roundabout), an area (e.g. a patch of grass), or both (e.g. a roundabout surrounding a grassy area).

Attributes:
  • node_ids: The IDs of the nodes that make up this way, or None if they are not included in the query's result set.
  • bounds: The enclosing bounding box of all nodes, or None when not using out bb. The bounds property of Shapely geometries can be used as an alternative.
  • center: The center of bounds, or None when not using out center. If you need a coordinate that is inside the element's geometry, consider Shapely's representative_point() and centroid.
  • geometry: A Linestring if the way is open, a LinearRing if the way is closed, a Polygon if the way is closed and its tags indicate that it represents an area, or None if the geometry is not included in the query's result set.
  • geometry_details: More info on the validity of geometry.
References:
Way( *, id: int, tags: dict[str, str] | None, meta: Metadata | None, relations: list[Relationship], node_ids: list[int] | None, bounds: tuple[float, float, float, float] | None, center: shapely.geometry.point.Point | None, geometry: shapely.geometry.linestring.LineString | shapely.geometry.polygon.LinearRing | shapely.geometry.polygon.Polygon | None, geometry_details: Optional[GeometryDetails[shapely.geometry.linestring.LineString | shapely.geometry.polygon.LinearRing | shapely.geometry.polygon.Polygon]])
node_ids: list[int] | None
bounds: tuple[float, float, float, float] | None
center: shapely.geometry.point.Point | None
geometry: shapely.geometry.linestring.LineString | shapely.geometry.polygon.LinearRing | shapely.geometry.polygon.Polygon | None
geometry_details: Optional[GeometryDetails[shapely.geometry.linestring.LineString | shapely.geometry.polygon.LinearRing | shapely.geometry.polygon.Polygon]]
id: int
tags: dict[str, str] | None
meta: Metadata | None
relations: list[Relationship]
@dataclass(kw_only=True, slots=True, repr=False, eq=False)
class Relation(Element):
332@dataclass(kw_only=True, slots=True, repr=False, eq=False)
333class Relation(Element):
334    """
335    A relation is a group of nodes and ways that have a logical or geographic relationship.
336
337    This relationship is described through its tags.
338
339    A relation may define an area geometry, which may have boundaries made up of several
340    unclosed ways. Relations of ``type=multipolygon`` may have boundaries ("outer" role) and
341    holes ("inner" role) made up of several unclosed ways.
342
343    Tags describing the multipolygon always go on the relation. The inner and outer ways are tagged
344    if they describe something in their own right. For example,
345     - a multipolygon relation may be tagged as landuse=forest if it outlines a forest,
346     - its outer ways may be tagged as barrier=fence if the forest is fenced,
347     - and its inner ways may be tagged as natural=water if there is a lake within the forest
348       boundaries.
349
350    Attributes:
351        bounds: The bounding box of this element, or ``None`` when not using ``out bb``.
352                It encloses all node and way members; relations as members have no effect.
353                The ``bounds`` property of Shapely geometries can be used as an alternative.
354        center: The center of ``bounds``, or ``None`` when not using ``out center``.
355                If you need a coordinate that is inside the element's geometry, consider Shapely's
356                ``representative_point()`` and ``centroid``.
357        members: Ordered member elements of this relation, with an optional role
358        geometry: If this relation is deemed to represent an area, these are the complex polygons
359                  whose boundaries and holes are made up of the ways inside the relation. Members
360                  that are not ways, or are not part of any polygon boundary, are not part of the
361                  result geometry. This is ``None`` if the geometry of the relation members is not
362                  included in the query's result set, or if the relation is not deemed to represent
363                  an area.
364        geometry_details: More info on the validity of ``geometry``.
365
366    References:
367        - https://wiki.openstreetmap.org/wiki/Relation
368        - https://wiki.openstreetmap.org/wiki/Relation:multipolygon
369        - https://wiki.openstreetmap.org/wiki/Relation:boundary
370    """
371
372    bounds: Bbox | None
373    center: Point | None
374    members: list["Relationship"]
375    geometry: Polygon | MultiPolygon | None
376    geometry_details: GeometryDetails[Polygon | MultiPolygon] | None
377
378    def __iter__(self) -> Iterator[tuple[str | None, Element]]:
379        """Iterates over all members in the form of ``(role, element)``."""
380        for relship in self.members:
381            yield relship.role, relship.member

A relation is a group of nodes and ways that have a logical or geographic relationship.

This relationship is described through its tags.

A relation may define an area geometry, which may have boundaries made up of several unclosed ways. Relations of type=multipolygon may have boundaries ("outer" role) and holes ("inner" role) made up of several unclosed ways.

Tags describing the multipolygon always go on the relation. The inner and outer ways are tagged if they describe something in their own right. For example,

  • a multipolygon relation may be tagged as landuse=forest if it outlines a forest,
  • its outer ways may be tagged as barrier=fence if the forest is fenced,
  • and its inner ways may be tagged as natural=water if there is a lake within the forest boundaries.
Attributes:
  • bounds: The bounding box of this element, or None when not using out bb. It encloses all node and way members; relations as members have no effect. The bounds property of Shapely geometries can be used as an alternative.
  • center: The center of bounds, or None when not using out center. If you need a coordinate that is inside the element's geometry, consider Shapely's representative_point() and centroid.
  • members: Ordered member elements of this relation, with an optional role
  • geometry: If this relation is deemed to represent an area, these are the complex polygons whose boundaries and holes are made up of the ways inside the relation. Members that are not ways, or are not part of any polygon boundary, are not part of the result geometry. This is None if the geometry of the relation members is not included in the query's result set, or if the relation is not deemed to represent an area.
  • geometry_details: More info on the validity of geometry.
References:
Relation( *, id: int, tags: dict[str, str] | None, meta: Metadata | None, relations: list[Relationship], bounds: tuple[float, float, float, float] | None, center: shapely.geometry.point.Point | None, members: list[Relationship], geometry: shapely.geometry.polygon.Polygon | shapely.geometry.multipolygon.MultiPolygon | None, geometry_details: Optional[GeometryDetails[shapely.geometry.polygon.Polygon | shapely.geometry.multipolygon.MultiPolygon]])
bounds: tuple[float, float, float, float] | None
center: shapely.geometry.point.Point | None
members: list[Relationship]
geometry: shapely.geometry.polygon.Polygon | shapely.geometry.multipolygon.MultiPolygon | None
geometry_details: Optional[GeometryDetails[shapely.geometry.polygon.Polygon | shapely.geometry.multipolygon.MultiPolygon]]
id: int
tags: dict[str, str] | None
meta: Metadata | None
relations: list[Relationship]
@dataclass(kw_only=True, slots=True, repr=False)
class Relationship(aio_overpass.spatial.Spatial):
384@dataclass(kw_only=True, slots=True, repr=False)
385class Relationship(Spatial):
386    """
387    The relationship of an element that is part of a relation, with an optional role.
388
389    Attributes:
390        member:     any element
391        relation:   a relation that the member is a part of
392        role:       describes the function of the member in the context of the relation
393
394    References:
395        - https://wiki.openstreetmap.org/wiki/Relation#Roles
396    """
397
398    member: Element
399    relation: Relation
400    role: str | None
401
402    @property
403    def geojson(self) -> GeoJsonDict:
404        """
405        A mapping of ``member``.
406
407        This is ``member.geojson``, with the added properties ``role`` and ``__rel__``.
408        """
409        return _geojson_feature(self)
410
411    def __repr__(self) -> str:
412        role = f" as '{self.role}'" if self.role else " "
413        return f"{type(self).__name__}({self.member}{role} in {self.relation})"

The relationship of an element that is part of a relation, with an optional role.

Attributes:
  • member: any element
  • relation: a relation that the member is a part of
  • role: describes the function of the member in the context of the relation
References:
Relationship( *, member: Element, relation: Relation, role: str | None)
member: Element
relation: Relation
role: str | None
geojson: dict[str, typing.Any]
402    @property
403    def geojson(self) -> GeoJsonDict:
404        """
405        A mapping of ``member``.
406
407        This is ``member.geojson``, with the added properties ``role`` and ``__rel__``.
408        """
409        return _geojson_feature(self)

A mapping of member.

This is member.geojson, with the added properties role and __rel__.

Bbox: TypeAlias = tuple[float, float, float, float]

The bounding box of a spatial object.

This tuple can be understood as any of - (s, w, n, e) - (minlat, minlon, maxlat, maxlon) - (minx, miny, maxx, maxy)

@dataclass(kw_only=True, slots=True)
class GeometryDetails(typing.Generic[~G]):
 68@dataclass(kw_only=True, slots=True)
 69class GeometryDetails(Generic[G]):
 70    """
 71    Element geometry with more info on its validity.
 72
 73    Shapely validity is based on an [OGC standard](https://www.ogc.org/standard/sfa/).
 74
 75    For MultiPolygons, one assertion is that its elements may only touch at a finite number
 76    of Points, which means they may not share an edge on their exteriors. In terms of
 77    OSM multipolygons, it makes sense to lift this requirement, and such geometries
 78    end up in the ``accepted`` field.
 79
 80    For invalid MultiPolygon and Polygons, we use Shapely's ``make_valid()``. If and only if
 81    the amount of polygons stays the same before and after making them valid,
 82    they will end up in the ``valid`` field.
 83
 84    Attributes:
 85        valid: if set, this is the original valid geometry
 86        accepted: if set, this is the original geometry that is invalid by Shapely standards,
 87                  but accepted by us
 88        fixed: if set, this is the geometry fixed by ``make_valid()``
 89        invalid: if set, this is the original invalid geometry
 90        invalid_reason: if the original geometry is invalid by Shapely standards,
 91                        this message states why
 92    """
 93
 94    valid: G | None = None
 95    accepted: G | None = None
 96    fixed: G | None = None
 97    invalid: G | None = None
 98    invalid_reason: str | None = None
 99
100    @property
101    def best(self) -> G | None:
102        """The "best" geometry, prioritizing ``fixed`` over ``invalid``."""
103        return self.valid or self.accepted or self.fixed or self.invalid

Element geometry with more info on its validity.

Shapely validity is based on an OGC standard.

For MultiPolygons, one assertion is that its elements may only touch at a finite number of Points, which means they may not share an edge on their exteriors. In terms of OSM multipolygons, it makes sense to lift this requirement, and such geometries end up in the accepted field.

For invalid MultiPolygon and Polygons, we use Shapely's make_valid(). If and only if the amount of polygons stays the same before and after making them valid, they will end up in the valid field.

Attributes:
  • valid: if set, this is the original valid geometry
  • accepted: if set, this is the original geometry that is invalid by Shapely standards, but accepted by us
  • fixed: if set, this is the geometry fixed by make_valid()
  • invalid: if set, this is the original invalid geometry
  • invalid_reason: if the original geometry is invalid by Shapely standards, this message states why
GeometryDetails( *, valid: Optional[~G] = None, accepted: Optional[~G] = None, fixed: Optional[~G] = None, invalid: Optional[~G] = None, invalid_reason: str | None = None)
valid: Optional[~G]
accepted: Optional[~G]
fixed: Optional[~G]
invalid: Optional[~G]
invalid_reason: str | None
best: Optional[~G]
100    @property
101    def best(self) -> G | None:
102        """The "best" geometry, prioritizing ``fixed`` over ``invalid``."""
103        return self.valid or self.accepted or self.fixed or self.invalid

The "best" geometry, prioritizing fixed over invalid.

@dataclass(kw_only=True, slots=True)
class Metadata:
45@dataclass(kw_only=True, slots=True)
46class Metadata:
47    """
48    Metadata concerning the most recent edit of an OSM element.
49
50    Attributes:
51        version: The version number of the element
52        timestamp: Timestamp (ISO 8601) of the most recent change of this element
53        changeset: The changeset in which the element was most recently changed
54        user_name: Name of the user that made the most recent change to the element
55        user_id: ID of the user that made the most recent change to the element
56    """
57
58    version: int
59    timestamp: str
60    changeset: int
61    user_name: str
62    user_id: int

Metadata concerning the most recent edit of an OSM element.

Attributes:
  • version: The version number of the element
  • timestamp: Timestamp (ISO 8601) of the most recent change of this element
  • changeset: The changeset in which the element was most recently changed
  • user_name: Name of the user that made the most recent change to the element
  • user_id: ID of the user that made the most recent change to the element
Metadata( *, version: int, timestamp: str, changeset: int, user_name: str, user_id: int)
version: int
timestamp: str
changeset: int
user_name: str
user_id: int