Source code for geoalchemy2.elements

import binascii
import struct

try:
    from sqlalchemy.sql import functions
    from sqlalchemy.sql.functions import FunctionElement
except ImportError:  # SQLA < 0.9  # pragma: no cover
    from sqlalchemy.sql import expression as functions
    from sqlalchemy.sql.expression import FunctionElement
from sqlalchemy.types import to_instance
from sqlalchemy.ext.compiler import compiles

from .compat import PY3, str as str_
from .exc import ArgumentError


class _SpatialElement(functions.Function):
    """
    The base class for :class:`geoalchemy2.elements.WKTElement` and
    :class:`geoalchemy2.elements.WKBElement`.

    The first argument passed to the constructor is the data wrapped
    by the ``_SpatialElement` object being constructed.

    Additional arguments:

    ``srid``

        An integer representing the spatial reference system. E.g. 4326.
        Default value is -1, which means no/unknown reference system.

    ``extended``

        A boolean indicating whether the extended format (EWKT or EWKB)
        is used. Default is ``False``.

    """

    def __init__(self, data, srid=-1, extended=False):
        self.srid = srid
        self.data = data
        self.extended = extended
        if self.extended:
            args = [self.geom_from_extended_version, self.data]
        else:
            args = [self.geom_from, self.data, self.srid]
        functions.Function.__init__(self, *args)

    def __str__(self):
        return self.desc

    def __repr__(self):
        return "<%s at 0x%x; %s>" % \
            (self.__class__.__name__, id(self), self)  # pragma: no cover

    def __getattr__(self, name):
        #
        # This is how things like lake.geom.ST_Buffer(2) creates
        # SQL expressions of this form:
        #
        # ST_Buffer(ST_GeomFromWKB(:ST_GeomFromWKB_1), :param_1)
        #

        # We create our own _FunctionGenerator here, and use it in place of
        # SQLAlchemy's "func" object. This is to be able to "bind" the
        # function to the SQL expression. See also GenericFunction above.

        func_ = functions._FunctionGenerator(expr=self)
        return getattr(func_, name)

    def __getstate__(self):
        state = {
            'srid': self.srid,
            'data': str(self),
            'extended': self.extended,
            'name': self.name,
        }
        return state

    def __setstate__(self, state):
        self.__dict__.update(state)
        self.data = self._data_from_desc(state['data'])
        args = [self.name, self.data]
        if not self.extended:
            args.append(self.srid)
        # we need to call Function.__init__ to properly initialize SQLAlchemy's
        # internal states
        functions.Function.__init__(self, *args)

    @staticmethod
    def _data_from_desc(desc):
        raise NotImplementedError()


@compiles(_SpatialElement, 'sqlite')
def compile_spatialelement(element, compiler, **kw):
    return "{}({})".format(element.name.lstrip("ST_"),
                           compiler.process(element.clauses, **kw))


[docs]class WKTElement(_SpatialElement): """ Instances of this class wrap a WKT or EWKT value. Usage examples:: wkt_element_1 = WKTElement('POINT(5 45)') wkt_element_2 = WKTElement('POINT(5 45)', srid=4326) wkt_element_3 = WKTElement('SRID=4326;POINT(5 45)', extended=True) """ geom_from = 'ST_GeomFromText' geom_from_extended_version = 'ST_GeomFromEWKT' def __init__(self, data, srid=-1, extended=False): if extended and srid == -1: # read srid from EWKT if not data.startswith('SRID='): raise ArgumentError('invalid EWKT string {}'.format(data)) data_s = data.split(';', 1) if len(data_s) != 2: raise ArgumentError('invalid EWKT string {}'.format(data)) header = data_s[0] try: srid = int(header[5:]) except ValueError: raise ArgumentError('invalid EWKT string {}'.format(data)) _SpatialElement.__init__(self, data, srid, extended) @property def desc(self): """ This element's description string. """ return self.data @staticmethod def _data_from_desc(desc): return desc
[docs]class WKBElement(_SpatialElement): """ Instances of this class wrap a WKB or EWKB value. Geometry values read from the database are converted to instances of this type. In most cases you won't need to create ``WKBElement`` instances yourself. If ``extended`` is ``True`` and ``srid`` is ``-1`` at construction time then the SRID will be read from the EWKB data. Note: you can create ``WKBElement`` objects from Shapely geometries using the :func:`geoalchemy2.shape.from_shape` function. """ geom_from = 'ST_GeomFromWKB' geom_from_extended_version = 'ST_GeomFromEWKB' def __init__(self, data, srid=-1, extended=False): if extended and srid == -1: # read srid from the EWKB # # WKB struct { # byte byteOrder; # uint32 wkbType; # uint32 SRID; # struct geometry; # } # byteOrder enum { # WKB_XDR = 0, // Most Significant Byte First # WKB_NDR = 1, // Least Significant Byte First # } if isinstance(data, str_): # SpatiaLite case # assume that the string is an hex value header = binascii.unhexlify(data[:18]) else: header = data[:9] if not PY3: header = bytearray(header) byte_order, srid = header[0], header[5:] srid = struct.unpack('<I' if byte_order else '>I', srid)[0] _SpatialElement.__init__(self, data, srid, extended) @property def desc(self): """ This element's description string. """ if isinstance(self.data, str_): # SpatiaLite case return self.data desc = binascii.hexlify(self.data) if PY3: # hexlify returns a bytes object on py3 desc = str(desc, encoding="utf-8") return desc @staticmethod def _data_from_desc(desc): if PY3: desc = desc.encode(encoding="utf-8") return binascii.unhexlify(desc)
[docs]class RasterElement(FunctionElement): """ Instances of this class wrap a ``raster`` value. Raster values read from the database are converted to instances of this type. In most cases you won't need to create ``RasterElement`` instances yourself. """ name = 'raster' def __init__(self, data): self.data = data FunctionElement.__init__(self, self.data) def __str__(self): return self.desc # pragma: no cover def __repr__(self): return "<%s at 0x%x; %r>" % \ (self.__class__.__name__, id(self), self.desc) # pragma: no cover @property def desc(self): """ This element's description string. """ desc = binascii.hexlify(self.data) if PY3: # hexlify returns a bytes object on py3 desc = str(desc, encoding="utf-8") if len(desc) < 30: return desc return desc[:30] + '...' # pragma: no cover def __getattr__(self, name): # # This is how things like ocean.rast.ST_Value(...) creates # SQL expressions of this form: # # ST_Value(:ST_GeomFromWKB_1), :param_1) # # We create our own _FunctionGenerator here, and use it in place of # SQLAlchemy's "func" object. This is to be able to "bind" the # function to the SQL expression. See also GenericFunction. func_ = functions._FunctionGenerator(expr=self) return getattr(func_, name)
@compiles(RasterElement) def compile_RasterElement(element, compiler, **kw): """ This function makes sure the :class:`geoalchemy2.elements.RasterElement` contents are correctly casted to the ``raster`` type before using it. The other elements in this module don't need such a function because they are derived from :class:`functions.Function`. For the :class:`geoalchemy2.elements.RasterElement` class however it would not be of any use to have it compile to ``raster('...')`` so it is compiled to ``'...'::raster`` by this function. """ return "%s::raster" % compiler.process(element.clauses) class CompositeElement(FunctionElement): """ Instances of this class wrap a Postgres composite type. """ def __init__(self, base, field, type_): self.name = field self.type = to_instance(type_) super(CompositeElement, self).__init__(base) @compiles(CompositeElement) def _compile_pgelem(expr, compiler, **kw): return '(%s).%s' % (compiler.process(expr.clauses, **kw), expr.name)