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