Source code for promis.geo.geospatial

"""This module contains base classes for geospatial objects like polygons, routes and points."""

#
# Copyright (c) Simon Kohaut, Honda Research Institute Europe GmbH
#
# This file is part of ProMis and licensed under the BSD 3-Clause License.
# You should have received a copy of the BSD 3-Clause License along with ProMis.
# If not, see https://opensource.org/license/bsd-3-clause/.
#

# Standard Library
from abc import ABC, abstractmethod
from typing import Any, cast
from uuid import uuid4

# Third Party
from geojson import Feature, dumps
from requests import post


[docs] class Geospatial(ABC): """The common abstract base class for both polar and cartesian geospatial objects. See :meth:`~Geospatial.to_geo_json` on how this class can be used for visualizing geometries. Args: location_type: The type of this polygon name: An optional name of this polygon identifier: A unique identifier for this object, in :math:`[0, 2^{63})`, i.e. 64 signed bits """ def __init__( self, location_type: str | None, name: str | None, identifier: int | None, ) -> None: self.location_type = location_type if location_type is not None else "UNKNOWN" self.name = name self.identifier = identifier if identifier is not None else uuid4().int % 2**63 super().__init__() @property def identifier(self) -> int | None: """The numerical identifier of this object. Must be `None` or in :math:`[0, 2^{63})`, i.e. 64 signed bits. """ return self._identifier @identifier.setter def identifier(self, value: int | None) -> None: assert value is None or 0 <= value < 2**63, "Identifiers must be in [0, 2**63) or None" self._identifier = value
[docs] def to_geo_json( self, indent: int | str | None = None, properties: dict = None, **kwargs ) -> str: """Returns the GeoJSON representation of the geometry embedded into a feature. Args: indent: The number of levels to indent or ``None`` (see :func:`json.dumps`) kwargs: Much like indent, any keyword argument that can be passed to :func:`json.dumps`, like ``allow_nan``, ``sort_keys``, and more Returns: The GeoJSON representation as a string Examples: GeoJSON is a widely used format that can be interpreted by a variety of GIS programs (geo information systems). Among them are for example the very simple website `geojson.io <https://geojson.io/>`__. However, sometimes the geometries are too large to be handled by the web browser. Then there are other programs available, like the free open-source tool `QGIS (Desktop) <https://www.qgis.org/de/site/>`__. Its even available in the usual Ubuntu repositories, so just run ``[sudo] apt install qgis``. Later, you can simply copy-pasta it into the tool. The geojson representation can be obtained like this (using a :class:`~promis.geo.location.PolarLocation` just as an example): >>> from promis.geo.location import PolarLocation >>> darmstadt = PolarLocation(latitude=49.878091, longitude=8.654052, identifier=0) >>> print(darmstadt.to_geo_json(indent=4)) { "type": "Feature", "id": 0, "geometry": { "type": "Point", "coordinates": [ 8.654052, 49.878091 ] }, "properties": { "location_type": "UNKNOWN" } } See also: - `GeoJSON on Wikipedia <https://en.wikipedia.org/wiki/GeoJSON>`__ - `geojson.io <https://geojson.io/>`__ - `QGIS (Desktop) <https://www.qgis.org/de/site/>`__ """ # this relies on the inheriting instance to provide __geo_interface__ property/attribute if properties is None: properties = {} return cast( str, dumps( Feature( geometry=self, id=self.identifier, properties={"location_type": self.location_type} | properties, ), indent=indent, **kwargs, ), )
[docs] def send_to_gui(self, url: str ="http://localhost:8000/add_geojson", timeout: int = 1): """Send an HTTP POST-request to the GUI backend. Args: url: url of the backend timeout: request timeout in second Raise: :class:`~requests.HTTPError`: When the HTTP request returned an unsuccessful status code :class:`~requests.ConnectionError`: If the request fails due to connection issues """ data = self.to_geo_json() r = post(url=url, data=data, timeout=timeout) r.raise_for_status()
@property @abstractmethod def __geo_interface__(self) -> dict[str, Any]: raise NotImplementedError() @property def _repr_extras(self) -> str: """Create a string representation of the extra attributes for use in :meth:`~__repr__`. Examples: The output is suited to be directly inlucded before the final closing bracet of a typical implementation of ``__repr__()``: >>> from promis.geo.location import PolarLocation >>> PolarLocation(0, 0, identifier=12)._repr_extras ', identifier=12' >>> PolarLocation(0, 0, name="", identifier=12)._repr_extras ', name="", identifier=12' >>> PolarLocation( ... 0, 0, location_type="water_vehicle", identifier=12 ... )._repr_extras ', location_type=water_vehicle, identifier=12' The class :class:`promis.geo.location.PolarLocation` was only chosen as an example. Returns: The arguments in the syntax of keyword arguments, as is common for :meth:`~__repr__`. """ result = "" if self.location_type != "UNKNOWN": result += f", location_type={self.location_type}" if self.name is not None: result += f', name="{self.name}"' if self.identifier is not None: result += f", identifier={self.identifier}" return result @abstractmethod def __repr__(self) -> str: raise NotImplementedError() def __eq__(self, other: Any) -> bool: return ( isinstance(other, Geospatial) and self.location_type == other.location_type and self.name == other.name and self.identifier == other.identifier )