"""
Spatial extents classes for map layer and space time datasets

Usage:

.. code-block:: pycon

    >>> import grass.temporal as tgis
    >>> tgis.init()
    >>> extent = tgis.RasterSpatialExtent(
    ...     ident="raster@PERMANENT",
    ...     north=90,
    ...     south=90,
    ...     east=180,
    ...     west=180,
    ...     top=100,
    ...     bottom=-20,
    ... )
    >>> extent = tgis.Raster3DSpatialExtent(
    ...     ident="raster3d@PERMANENT",
    ...     north=90,
    ...     south=90,
    ...     east=180,
    ...     west=180,
    ...     top=100,
    ...     bottom=-20,
    ... )
    >>> extent = tgis.VectorSpatialExtent(
    ...     ident="vector@PERMANENT",
    ...     north=90,
    ...     south=90,
    ...     east=180,
    ...     west=180,
    ...     top=100,
    ...     bottom=-20,
    ... )
    >>> extent = tgis.STRDSSpatialExtent(
    ...     ident="strds@PERMANENT",
    ...     north=90,
    ...     south=90,
    ...     east=180,
    ...     west=180,
    ...     top=100,
    ...     bottom=-20,
    ... )
    >>> extent = tgis.STR3DSSpatialExtent(
    ...     ident="str3ds@PERMANENT",
    ...     north=90,
    ...     south=90,
    ...     east=180,
    ...     west=180,
    ...     top=100,
    ...     bottom=-20,
    ... )
    >>> extent = tgis.STVDSSpatialExtent(
    ...     ident="stvds@PERMANENT",
    ...     north=90,
    ...     south=90,
    ...     east=180,
    ...     west=180,
    ...     top=100,
    ...     bottom=-20,
    ... )

(C) 2012-2013 by the GRASS Development Team
This program is free software under the GNU General Public
License (>=v2). Read the file COPYING that comes with GRASS
for details.

:authors: Soeren Gebbert
"""

from typing import Literal

from .base import SQLDatabaseInterface


class SpatialExtent(SQLDatabaseInterface):
    """This is the spatial extent base class for all maps and space time datasets

    This class implements a three dimensional axis aligned bounding box
    and functions to compute topological relationships

    Usage:

    .. code-block:: pycon

        >>> init()
        >>> extent = SpatialExtent(
        ...     table="raster_spatial_extent",
        ...     ident="soil@PERMANENT",
        ...     north=90,
        ...     south=90,
        ...     east=180,
        ...     west=180,
        ...     top=100,
        ...     bottom=-20,
        ... )
        >>> extent.id
        'soil@PERMANENT'
        >>> extent.north
        90.0
        >>> extent.south
        90.0
        >>> extent.east
        180.0
        >>> extent.west
        180.0
        >>> extent.top
        100.0
        >>> extent.bottom
        -20.0
        >>> extent.print_info()
         +-------------------- Spatial extent ----------------------------------------+
         | North:...................... 90.0
         | South:...................... 90.0
         | East:.. .................... 180.0
         | West:....................... 180.0
         | Top:........................ 100.0
         | Bottom:..................... -20.0
        >>> extent.print_shell_info()
        north=90.0
        south=90.0
        east=180.0
        west=180.0
        top=100.0
        bottom=-20.0

    """

    def __init__(
        self,
        table=None,
        ident=None,
        north=None,
        south=None,
        east=None,
        west=None,
        top=None,
        bottom=None,
        proj="XY",
    ) -> None:
        SQLDatabaseInterface.__init__(self, table, ident)
        self.set_id(ident)
        self.set_spatial_extent_from_values(north, south, east, west, top, bottom)
        self.set_projection(proj)

    def overlapping_2d(self, extent) -> bool:
        """Return True if this (A) and the provided spatial extent (B) overlaps
        in two dimensional space.
        Code is lend from wind_overlap.c in lib/gis

        Overlapping includes the spatial relations:

        - contain
        - in
        - cover
        - covered
        - equivalent

        .. code-block:: pycon

            >>> A = SpatialExtent(north=80, south=20, east=60, west=10)
            >>> B = SpatialExtent(north=80, south=20, east=60, west=10)
            >>> A.overlapping_2d(B)
            True

        :param extent: The spatial extent to check overlapping with
        :return: True or False
        """

        if self.get_projection() != extent.get_projection():
            self.msgr.error(
                _(
                    "Projections are different. Unable to compute "
                    "overlapping_2d for spatial extents"
                )
            )
            return False

        N = extent.get_north()
        S = extent.get_south()
        E = extent.get_east()
        W = extent.get_west()

        # Adjust the east and west in case of LL projection
        if self.get_projection() == "LL":
            while self.get_west() > E:
                E += 360.0
                W += 360.0

            while self.get_east() < W:
                E -= 360.0
                W -= 360.0

        return not (
            self.get_north() <= S
            or self.get_south() >= N
            or self.get_east() <= W
            or self.get_west() >= E
        )

    def overlapping(self, extent) -> bool:
        """Return True if this (A) and the provided spatial
        extent (B) overlaps in three dimensional space.

        Overlapping includes the spatial relations:

        - contain
        - in
        - cover
        - covered
        - equivalent

        Usage:

        .. code-block:: pycon

            >>> A = SpatialExtent(
            ...     north=80, south=20, east=60, west=10, bottom=-50, top=50
            ... )
            >>> B = SpatialExtent(
            ...     north=80, south=20, east=60, west=10, bottom=-50, top=50
            ... )
            >>> A.overlapping(B)
            True

        :param extent: The spatial extent to check overlapping with
        :return: True or False
        """

        if not self.overlapping_2d(extent):
            return False

        T = extent.get_top()
        B = extent.get_bottom()

        return not (self.get_top() <= B or self.get_bottom() >= T)

    def intersect_2d(self, extent):
        """Return the two dimensional intersection as spatial_extent
           object or None in case no intersection was found.

        :param extent: The spatial extent to intersect with
        :return: The intersection spatial extent
        """

        if not self.overlapping_2d(extent):
            return None

        eN = extent.get_north()
        eS = extent.get_south()
        eE = extent.get_east()
        eW = extent.get_west()

        N = self.get_north()
        S = self.get_south()
        E = self.get_east()
        W = self.get_west()

        # Adjust the east and west in case of LL projection
        if self.get_projection() == "LL":
            while eE < W:
                eE += 360.0
                eW += 360.0

            while eW > E:
                eE -= 360.0
                eW -= 360.0

        # Compute the extent
        nN = N
        nS = S
        nE = E
        nW = W

        if eW > W:
            nW = eW
        if eE < E:
            nE = eE
        if eN < N:
            nN = eN
        if eS > S:
            nS = eS

        return SpatialExtent(
            north=nN,
            south=nS,
            east=nE,
            west=nW,
            top=0,
            bottom=0,
            proj=self.get_projection(),
        )

    def intersect(self, extent):
        """Return the three dimensional intersection as spatial_extent
        object or None in case no intersection was found.

        Usage:

        .. code-block:: pycon

            >>> A = SpatialExtent(
            ...     north=80, south=20, east=60, west=10, bottom=-50, top=50
            ... )
            >>> B = SpatialExtent(
            ...     north=80, south=20, east=60, west=10, bottom=-50, top=50
            ... )
            >>> C = A.intersect(B)
            >>> C.print_info()
             +-------------------- Spatial extent ----------------------------------------+
             | North:...................... 80.0
             | South:...................... 20.0
             | East:.. .................... 60.0
             | West:....................... 10.0
             | Top:........................ 50.0
             | Bottom:..................... -50.0
            >>> B = SpatialExtent(
            ...     north=40, south=30, east=60, west=10, bottom=-50, top=50
            ... )
            >>> C = A.intersect(B)
            >>> C.print_info()
             +-------------------- Spatial extent ----------------------------------------+
             | North:...................... 40.0
             | South:...................... 30.0
             | East:.. .................... 60.0
             | West:....................... 10.0
             | Top:........................ 50.0
             | Bottom:..................... -50.0
            >>> B = SpatialExtent(
            ...     north=40, south=30, east=60, west=30, bottom=-50, top=50
            ... )
            >>> C = A.intersect(B)
            >>> C.print_info()
             +-------------------- Spatial extent ----------------------------------------+
             | North:...................... 40.0
             | South:...................... 30.0
             | East:.. .................... 60.0
             | West:....................... 30.0
             | Top:........................ 50.0
             | Bottom:..................... -50.0
            >>> B = SpatialExtent(
            ...     north=40, south=30, east=60, west=30, bottom=-30, top=50
            ... )
            >>> C = A.intersect(B)
            >>> C.print_info()
             +-------------------- Spatial extent ----------------------------------------+
             | North:...................... 40.0
             | South:...................... 30.0
             | East:.. .................... 60.0
             | West:....................... 30.0
             | Top:........................ 50.0
             | Bottom:..................... -30.0
            >>> B = SpatialExtent(
            ...     north=40, south=30, east=60, west=30, bottom=-30, top=30
            ... )
            >>> C = A.intersect(B)
            >>> C.print_info()
             +-------------------- Spatial extent ----------------------------------------+
             | North:...................... 40.0
             | South:...................... 30.0
             | East:.. .................... 60.0
             | West:....................... 30.0
             | Top:........................ 30.0
             | Bottom:..................... -30.0


        :param extent: The spatial extent to intersect with
        :return: The intersection spatial extent
        """  # noqa: E501

        if not self.overlapping(extent):
            return None

        new = self.intersect_2d(extent)

        eT = extent.get_top()
        eB = extent.get_bottom()

        T = self.get_top()
        B = self.get_bottom()

        nT = T
        nB = B

        if eB > B:
            nB = eB
        if eT < T:
            nT = eT

        new.set_top(nT)
        new.set_bottom(nB)

        return new

    def union_2d(self, extent):
        """Return the two dimensional union as spatial_extent
           object or None in case the extents does not overlap or meet.

        :param extent: The spatial extent to create a union with
        :return: The union spatial extent
        """
        if not self.overlapping_2d(extent) and not self.meet_2d(extent):
            return None

        return self.disjoint_union_2d(extent)

    def disjoint_union_2d(self, extent):
        """Return the two dimensional union as spatial_extent.

        :param extent: The spatial extent to create a union with
        :return: The union spatial extent
        """
        eN = extent.get_north()
        eS = extent.get_south()
        eE = extent.get_east()
        eW = extent.get_west()

        N = self.get_north()
        S = self.get_south()
        E = self.get_east()
        W = self.get_west()

        # Adjust the east and west in case of LL projection
        if self.get_projection() == "LL":
            while eE < W:
                eE += 360.0
                eW += 360.0

            while eW > E:
                eE -= 360.0
                eW -= 360.0

        # Compute the extent
        nN = N
        nS = S
        nE = E
        nW = W

        if eW < W:
            nW = eW
        if eE > E:
            nE = eE
        if eN > N:
            nN = eN
        if eS < S:
            nS = eS

        return SpatialExtent(
            north=nN,
            south=nS,
            east=nE,
            west=nW,
            top=0,
            bottom=0,
            proj=self.get_projection(),
        )

    def union(self, extent):
        """Return the three dimensional union as spatial_extent
           object or None in case the extents does not overlap or meet.

        :param extent: The spatial extent to create a union with
        :return: The union spatial extent
        """
        if not self.overlapping(extent) and not self.meet(extent):
            return None

        return self.disjoint_union(extent)

    def disjoint_union(self, extent):
        """Return the three dimensional union as spatial_extent .

        Usage:

        .. code-block:: pycon

            >>> A = SpatialExtent(
            ...     north=80, south=20, east=60, west=10, bottom=-50, top=50
            ... )
            >>> B = SpatialExtent(
            ...     north=80, south=20, east=60, west=10, bottom=-50, top=50
            ... )
            >>> C = A.disjoint_union(B)
            >>> C.print_info()
             +-------------------- Spatial extent ----------------------------------------+
             | North:...................... 80.0
             | South:...................... 20.0
             | East:.. .................... 60.0
             | West:....................... 10.0
             | Top:........................ 50.0
             | Bottom:..................... -50.0
            >>> B = SpatialExtent(
            ...     north=40, south=30, east=60, west=10, bottom=-50, top=50
            ... )
            >>> C = A.disjoint_union(B)
            >>> C.print_info()
             +-------------------- Spatial extent ----------------------------------------+
             | North:...................... 80.0
             | South:...................... 20.0
             | East:.. .................... 60.0
             | West:....................... 10.0
             | Top:........................ 50.0
             | Bottom:..................... -50.0
            >>> B = SpatialExtent(
            ...     north=40, south=30, east=60, west=30, bottom=-50, top=50
            ... )
            >>> C = A.disjoint_union(B)
            >>> C.print_info()
             +-------------------- Spatial extent ----------------------------------------+
             | North:...................... 80.0
             | South:...................... 20.0
             | East:.. .................... 60.0
             | West:....................... 10.0
             | Top:........................ 50.0
             | Bottom:..................... -50.0
            >>> B = SpatialExtent(
            ...     north=40, south=30, east=60, west=30, bottom=-30, top=50
            ... )
            >>> C = A.disjoint_union(B)
            >>> C.print_info()
             +-------------------- Spatial extent ----------------------------------------+
             | North:...................... 80.0
             | South:...................... 20.0
             | East:.. .................... 60.0
             | West:....................... 10.0
             | Top:........................ 50.0
             | Bottom:..................... -50.0
            >>> B = SpatialExtent(
            ...     north=40, south=30, east=60, west=30, bottom=-30, top=30
            ... )
            >>> C = A.disjoint_union(B)
            >>> C.print_info()
             +-------------------- Spatial extent ----------------------------------------+
             | North:...................... 80.0
             | South:...................... 20.0
             | East:.. .................... 60.0
             | West:....................... 10.0
             | Top:........................ 50.0
             | Bottom:..................... -50.0
            >>> A = SpatialExtent(
            ...     north=80, south=20, east=60, west=10, bottom=-50, top=50
            ... )
            >>> B = SpatialExtent(
            ...     north=90, south=80, east=70, west=20, bottom=-30, top=60
            ... )
            >>> C = A.disjoint_union(B)
            >>> C.print_info()
             +-------------------- Spatial extent ----------------------------------------+
             | North:...................... 90.0
             | South:...................... 20.0
             | East:.. .................... 70.0
             | West:....................... 10.0
             | Top:........................ 60.0
             | Bottom:..................... -50.0


        :param extent: The spatial extent to create a disjoint union with
        :return: The union spatial extent
        """  # noqa: E501

        new = self.disjoint_union_2d(extent)

        eT = extent.get_top()
        eB = extent.get_bottom()

        T = self.get_top()
        B = self.get_bottom()

        nT = T
        nB = B

        if eB < B:
            nB = eB
        if eT > T:
            nT = eT

        new.set_top(nT)
        new.set_bottom(nB)

        return new

    def is_in_2d(self, extent) -> bool:
        """Return True if this extent (A) is located in the provided spatial
        extent (B) in two dimensions.

        ::

             _____
            |A _  |
            | |_| |
            |_____|B


        :param extent: The spatial extent
        :return: True or False
        """
        if self.get_projection() != extent.get_projection():
            self.msgr.error(
                _(
                    "Projections are different. Unable to compute "
                    "is_in_2d for spatial extents"
                )
            )
            return False

        eN = extent.get_north()
        eS = extent.get_south()
        eE = extent.get_east()
        eW = extent.get_west()

        N = self.get_north()
        S = self.get_south()
        E = self.get_east()
        W = self.get_west()

        # Adjust the east and west in case of LL projection
        if self.get_projection() == "LL":
            while eE < W:
                eE += 360.0
                eW += 360.0

            while eW > E:
                eE -= 360.0
                eW -= 360.0

        return not (eW >= W or eE <= E or eN <= N or eS >= S)

    def is_in(self, extent) -> bool:
        """Return True if this extent (A) is located in the provided spatial
        extent (B) in three dimensions.

        Usage:

        .. code-block:: pycon

            >>> A = SpatialExtent(
            ...     north=79, south=21, east=59, west=11, bottom=-49, top=49
            ... )
            >>> B = SpatialExtent(
            ...     north=80, south=20, east=60, west=10, bottom=-50, top=50
            ... )
            >>> A.is_in(B)
            True
            >>> B.is_in(A)
            False

        :param extent: The spatial extent
        :return: True or False
        """
        if not self.is_in_2d(extent):
            return False

        eT = extent.get_top()
        eB = extent.get_bottom()

        T = self.get_top()
        B = self.get_bottom()

        return not (eB >= B or eT <= T)

    def contain_2d(self, extent):
        """Return True if this extent (A) contains the provided spatial
        extent (B) in two dimensions.

        Usage:

        .. code-block:: pycon

            >>> A = SpatialExtent(north=80, south=20, east=60, west=10)
            >>> B = SpatialExtent(north=79, south=21, east=59, west=11)
            >>> A.contain_2d(B)
            True
            >>> B.contain_2d(A)
            False

        :param extent: The spatial extent
        :return: True or False
        """
        return extent.is_in_2d(self)

    def contain(self, extent):
        """Return True if this extent (A) contains the provided spatial
        extent (B) in three dimensions.

        Usage:

        .. code-block:: pycon

            >>> A = SpatialExtent(
            ...     north=80, south=20, east=60, west=10, bottom=-50, top=50
            ... )
            >>> B = SpatialExtent(
            ...     north=79, south=21, east=59, west=11, bottom=-49, top=49
            ... )
            >>> A.contain(B)
            True
            >>> B.contain(A)
            False

        :param extent: The spatial extent
        :return: True or False
        """
        return extent.is_in(self)

    def equivalent_2d(self, extent) -> bool:
        """Return True if this extent (A) is equal to the provided spatial
        extent (B) in two dimensions.

        Usage:

        .. code-block:: pycon

            >>> A = SpatialExtent(north=80, south=20, east=60, west=10)
            >>> B = SpatialExtent(north=80, south=20, east=60, west=10)
            >>> A.equivalent_2d(B)
            True
            >>> B.equivalent_2d(A)
            True

        :param extent: The spatial extent
        :return: True or False
        """
        if self.get_projection() != extent.get_projection():
            self.msgr.error(
                _(
                    "Projections are different. Unable to compute "
                    "equivalent_2d for spatial extents"
                )
            )
            return False

        eN = extent.get_north()
        eS = extent.get_south()
        eE = extent.get_east()
        eW = extent.get_west()

        N = self.get_north()
        S = self.get_south()
        E = self.get_east()
        W = self.get_west()

        # Adjust the east and west in case of LL projection
        if self.get_projection() == "LL":
            while eE < W:
                eE += 360.0
                eW += 360.0

            while eW > E:
                eE -= 360.0
                eW -= 360.0

        return not (eW != W or eE != E or eN != N or eS != S)

    def equivalent(self, extent) -> bool:
        """Return True if this extent (A) is equal to the provided spatial
        extent (B) in three dimensions.

        Usage:

        .. code-block:: pycon

            >>> A = SpatialExtent(
            ...     north=80, south=20, east=60, west=10, bottom=-50, top=50
            ... )
            >>> B = SpatialExtent(
            ...     north=80, south=20, east=60, west=10, bottom=-50, top=50
            ... )
            >>> A.equivalent(B)
            True
            >>> B.equivalent(A)
            True

        :param extent: The spatial extent
        :return: True or False
        """

        if not self.equivalent_2d(extent):
            return False

        eT = extent.get_top()
        eB = extent.get_bottom()

        T = self.get_top()
        B = self.get_bottom()

        return not (eB != B or eT != T)

    def cover_2d(self, extent) -> bool:
        """Return True if this extent (A) covers the provided spatial
        extent (B) in two dimensions.

        ::

             _____    _____    _____    _____
            |A  __|  |__  A|  |A | B|  |B | A|
            |  |B |  | B|  |  |  |__|  |__|  |
            |__|__|  |__|__|  |_____|  |_____|

             _____    _____    _____    _____
            |A|B| |  |A  __|  |A _  |  |__  A|
            | |_| |  |  |__|B | |B| | B|__|  |
            |_____|  |_____|  |_|_|_|  |_____|

             _____    _____    _____    _____
            |A|B  |  |_____|A |A|B|A|  |_____|A
            | |   |  |B    |  | | | |  |_____|B
            |_|___|  |_____|  |_|_|_|  |_____|A


        The following cases are excluded:

        - contain
        - in
        - equivalent

        :param extent: The spatial extent
        :return: True or False
        """

        if self.get_projection() != extent.get_projection():
            self.msgr.error(
                _(
                    "Projections are different. Unable to compute"
                    " cover_2d for spatial extents"
                )
            )
            return False

        # Exclude equivalent_2d
        if self.equivalent_2d(extent):
            return False

        eN = extent.get_north()
        eS = extent.get_south()
        eE = extent.get_east()
        eW = extent.get_west()

        N = self.get_north()
        S = self.get_south()
        E = self.get_east()
        W = self.get_west()

        # Adjust the east and west in case of LL projection
        if self.get_projection() == "LL":
            while eE < W:
                eE += 360.0
                eW += 360.0

            while eW > E:
                eE -= 360.0
                eW -= 360.0

        # Edges of extent located outside of self are not allowed
        if eW >= E:
            return False
        if eE <= W:
            return False
        if eS >= N:
            return False
        if eN <= S:
            return False

        # First we check that at least one edge of extent meets an edge of self
        if eW != W and eE != E and eN != N and eS != S:
            return False

        # We check that at least one edge of extent is located in self
        edge_count = 0
        if W < eW < E:
            edge_count += 1
        if W < eE < E:
            edge_count += 1
        if S < eN < N:
            edge_count += 1
        if S < eS < N:
            edge_count += 1

        return edge_count != 0

    def cover(self, extent) -> bool:
        """Return True if this extent covers the provided spatial
        extent in three dimensions.

        The following cases are excluded:

        - contain
        - in
        - equivalent

        :param extent: The spatial extent
        :return: True or False
        """
        if self.get_projection() != extent.get_projection():
            self.msgr.error(
                _(
                    "Projections are different. Unable to compute "
                    "cover for spatial extents"
                )
            )
            return False

        # Exclude equivalent_2d
        if self.equivalent_2d(extent):
            return False

        eN = extent.get_north()
        eS = extent.get_south()
        eE = extent.get_east()
        eW = extent.get_west()

        eT = extent.get_top()
        eB = extent.get_bottom()

        N = self.get_north()
        S = self.get_south()
        E = self.get_east()
        W = self.get_west()

        T = self.get_top()
        B = self.get_bottom()

        # Adjust the east and west in case of LL projection
        if self.get_projection() == "LL":
            while eE < W:
                eE += 360.0
                eW += 360.0

            while eW > E:
                eE -= 360.0
                eW -= 360.0

        # Edges of extent located outside of self are not allowed
        if (eW >= E) or (eE <= W) or (eS >= N) or (eN <= S) or (eB >= T) or (eT <= B):
            return False

        # First we check that at least one edge of extent meets an edge of self
        if eW != W and eE != E and eN != N and eS != S and eB != B and eT != T:
            return False

        # We check that at least one edge of extent is located in self
        edge_count = 0
        if W < eW < E:
            edge_count += 1
        if W < eE < E:
            edge_count += 1
        if S < eN < N:
            edge_count += 1
        if S < eS < N:
            edge_count += 1
        if S < eN < N:
            edge_count += 1
        if S < eS < N:
            edge_count += 1
        if B < eT < T:
            edge_count += 1
        if B < eB < T:
            edge_count += 1

        return edge_count != 0

    def covered_2d(self, extent):
        """Return True if this extent is covered by the provided spatial
        extent in two dimensions.

        The following cases are excluded:

        - contain
        - in
        - equivalent

        :param extent: The spatial extent
        :return: True or False
        """

        return extent.cover_2d(self)

    def covered(self, extent):
        """Return True if this extent is covered by the provided spatial
        extent in three dimensions.

        The following cases are excluded:

        - contain
        - in
        - equivalent

        :param extent: The spatial extent
        :return: True or False
        """

        return extent.cover(self)

    def overlap_2d(self, extent) -> bool:
        """Return True if this extent (A) overlaps with the provided spatial
        extent (B) in two dimensions.
        Code is lend from wind_overlap.c in lib/gis

        ::

             _____
            |A  __|__
            |  |  | B|
            |__|__|  |
               |_____|


        The following cases are excluded:

        - contain
        - in
        - cover
        - covered
        - equivalent

        :param extent: The spatial extent
        :return: True or False
        """

        if self.contain_2d(extent):
            return False

        if self.is_in_2d(extent):
            return False

        if self.cover_2d(extent):
            return False

        if self.covered_2d(extent):
            return False

        if self.equivalent_2d(extent):
            return False

        N = extent.get_north()
        S = extent.get_south()
        E = extent.get_east()
        W = extent.get_west()

        # Adjust the east and west in case of LL projection
        if self.get_projection() == "LL":
            while self.get_west() > E:
                E += 360.0
                W += 360.0

            while self.get_east() < W:
                E -= 360.0
                W -= 360.0

        return not (
            self.get_north() <= S
            or self.get_south() >= N
            or self.get_east() <= W
            or self.get_west() >= E
        )

    def overlap(self, extent) -> bool:
        """Return True if this extent overlaps with the provided spatial
        extent in three dimensions.

        The following cases are excluded:

        - contain
        - in
        - cover
        - covered
        - equivalent

        :param extent: The spatial extent
        :return: True or False
        """

        if self.is_in(extent):
            return False

        if self.contain(extent):
            return False

        if self.cover(extent):
            return False

        if self.covered(extent):
            return False

        if self.equivalent(extent):
            return False

        N = extent.get_north()
        S = extent.get_south()
        E = extent.get_east()
        W = extent.get_west()
        T = extent.get_top()
        B = extent.get_bottom()

        # Adjust the east and west in case of LL projection
        if self.get_projection() == "LL":
            while self.get_west() > E:
                E += 360.0
                W += 360.0

            while self.get_east() < W:
                E -= 360.0
                W -= 360.0

        return not (
            self.get_north() <= S
            or self.get_south() >= N
            or self.get_east() <= W
            or self.get_west() >= E
            or self.get_top() <= B
            or self.get_bottom() >= T
        )

    def meet_2d(self, extent) -> bool:
        """Return True if this extent (A) meets with the provided spatial
        extent (B) in two dimensions.

        ::

              _____ _____
             |  A  |  B  |
             |_____|     |
                   |_____|
              _____ _____
             |  B  |  A  |
             |     |     |
             |_____|_____|
               ___
              | A |
              |   |
              |___|
             |  B  |
             |     |
             |_____|
              _____
             |  B  |
             |     |
             |_____|_
               |  A  |
               |     |
               |_____|


        :param extent: The spatial extent
        :return: True or False
        """

        eN = extent.get_north()
        eS = extent.get_south()
        eE = extent.get_east()
        eW = extent.get_west()

        N = self.get_north()
        S = self.get_south()
        E = self.get_east()
        W = self.get_west()

        # Adjust the east and west in case of LL projection
        if self.get_projection() == "LL":
            while eE < W:
                eE += 360.0
                eW += 360.0

            while eW > E:
                eE -= 360.0
                eW -= 360.0

        edge = None
        edge_count = 0

        if eW == E:
            edge = "E"
            edge_count += 1
        if eE == W:
            edge = "W"
            edge_count += 1
        if eS == N:
            edge = "N"
            edge_count += 1
        if eN == S:
            edge = "S"
            edge_count += 1

        # Meet a single edge only
        if edge_count != 1:
            return False

        # Check boundaries of the faces
        if edge in {"E", "W"}:
            if eS > N or eN < S:
                return False

        if edge in {"N", "S"}:
            if eW > E or eE < W:
                return False

        return True

    def meet(self, extent) -> bool:
        """Return True if this extent meets with the provided spatial
        extent in three dimensions.

        :param extent: The spatial extent
        :return: True or False
        """
        eN = extent.get_north()
        eS = extent.get_south()
        eE = extent.get_east()
        eW = extent.get_west()

        eT = extent.get_top()
        eB = extent.get_bottom()

        N = self.get_north()
        S = self.get_south()
        E = self.get_east()
        W = self.get_west()

        T = self.get_top()
        B = self.get_bottom()

        # Adjust the east and west in case of LL projection
        if self.get_projection() == "LL":
            while eE < W:
                eE += 360.0
                eW += 360.0

            while eW > E:
                eE -= 360.0
                eW -= 360.0

        edge = None
        edge_count = 0

        if eW == E:
            edge = "E"
            edge_count += 1
        if eE == W:
            edge = "W"
            edge_count += 1
        if eS == N:
            edge = "N"
            edge_count += 1
        if eN == S:
            edge = "S"
            edge_count += 1
        if eB == T:
            edge = "T"
            edge_count += 1
        if eT == B:
            edge = "B"
            edge_count += 1

        # Meet a single edge only
        if edge_count != 1:
            return False

        # Check boundaries of the faces
        if edge in {"E", "W"}:
            if eS > N or eN < S:
                return False
            if eB > T or eT < B:
                return False

        if edge in {"N", "S"}:
            if eW > E or eE < W:
                return False
            if eB > T or eT < B:
                return False

        if edge in {"T", "B"}:
            if eW > E or eE < W:
                return False
            if eS > N or eN < S:
                return False

        return True

    def disjoint_2d(self, extent) -> bool:
        """Return True if this extent (A) is disjoint with the provided spatial
        extent (B) in three dimensions.

        ::

              _____
             |  A  |
             |_____|
             _______
            |   B   |
            |_______|


        :param extent: The spatial extent
        :return: True or False
        """

        return not (
            self.is_in_2d(extent)
            or self.contain_2d(extent)
            or self.cover_2d(extent)
            or self.covered_2d(extent)
            or self.equivalent_2d(extent)
            or self.overlapping_2d(extent)
            or self.meet_2d(extent)
        )

    def disjoint(self, extent) -> bool:
        """Return True if this extent is disjoint with the provided spatial
        extent in three dimensions.

        :param extent: The spatial extent
        :return: True or False
        """

        return not (
            self.is_in(extent)
            or self.contain(extent)
            or self.cover(extent)
            or self.covered(extent)
            or self.equivalent(extent)
            or self.overlapping(extent)
            or self.meet(extent)
        )

    def spatial_relation_2d(
        self, extent
    ) -> Literal[
        "equivalent",
        "contain",
        "in",
        "cover",
        "covered",
        "overlap",
        "meet",
        "disjoint",
        "unknown",
    ]:
        """Returns the two dimensional spatial relation between this
        extent and the provided spatial extent in two dimensions.

        Spatial relations are:

        - disjoint
        - meet
        - overlap
        - cover
        - covered
        - in
        - contain
        - equivalent

        Usage: see self.spatial_relation()
        """

        if self.equivalent_2d(extent):
            return "equivalent"
        if self.contain_2d(extent):
            return "contain"
        if self.is_in_2d(extent):
            return "in"
        if self.cover_2d(extent):
            return "cover"
        if self.covered_2d(extent):
            return "covered"
        if self.overlap_2d(extent):
            return "overlap"
        if self.meet_2d(extent):
            return "meet"
        if self.disjoint_2d(extent):
            return "disjoint"

        return "unknown"

    def spatial_relation(
        self, extent
    ) -> Literal[
        "equivalent",
        "contain",
        "in",
        "cover",
        "covered",
        "overlap",
        "meet",
        "disjoint",
        "unknown",
    ]:
        """Returns the two dimensional spatial relation between this
        extent and the provided spatial extent in three dimensions.

        Spatial relations are:

        - disjoint
        - meet
        - overlap
        - cover
        - covered
        - in
        - contain
        - equivalent


        Usage:

        .. code-block:: pycon

            >>> A = SpatialExtent(
            ...     north=80, south=20, east=60, west=10, bottom=-50, top=50
            ... )
            >>> B = SpatialExtent(
            ...     north=80, south=20, east=60, west=10, bottom=-50, top=50
            ... )
            >>> A.spatial_relation(B)
            'equivalent'
            >>> B.spatial_relation(A)
            'equivalent'
            >>> B = SpatialExtent(
            ...     north=70, south=20, east=60, west=10, bottom=-50, top=50
            ... )
            >>> A.spatial_relation_2d(B)
            'cover'
            >>> A.spatial_relation(B)
            'cover'
            >>> B = SpatialExtent(
            ...     north=70, south=30, east=60, west=10, bottom=-50, top=50
            ... )
            >>> A.spatial_relation_2d(B)
            'cover'
            >>> A.spatial_relation(B)
            'cover'
            >>> B.spatial_relation_2d(A)
            'covered'
            >>> B.spatial_relation(A)
            'covered'
            >>> B = SpatialExtent(
            ...     north=70, south=30, east=50, west=10, bottom=-50, top=50
            ... )
            >>> A.spatial_relation_2d(B)
            'cover'
            >>> B.spatial_relation_2d(A)
            'covered'
            >>> A.spatial_relation(B)
            'cover'
            >>> B = SpatialExtent(
            ...     north=70, south=30, east=50, west=20, bottom=-50, top=50
            ... )
            >>> B.spatial_relation(A)
            'covered'
            >>> B = SpatialExtent(
            ...     north=70, south=30, east=50, west=20, bottom=-50, top=50
            ... )
            >>> A.spatial_relation_2d(B)
            'contain'
            >>> A.spatial_relation(B)
            'cover'
            >>> B = SpatialExtent(
            ...     north=70, south=30, east=50, west=20, bottom=-40, top=50
            ... )
            >>> A.spatial_relation(B)
            'cover'
            >>> B = SpatialExtent(
            ...     north=70, south=30, east=50, west=20, bottom=-40, top=40
            ... )
            >>> A.spatial_relation(B)
            'contain'
            >>> B.spatial_relation(A)
            'in'
            >>> B = SpatialExtent(
            ...     north=90, south=30, east=50, west=20, bottom=-40, top=40
            ... )
            >>> A.spatial_relation_2d(B)
            'overlap'
            >>> A.spatial_relation(B)
            'overlap'
            >>> B = SpatialExtent(
            ...     north=90, south=5, east=70, west=5, bottom=-40, top=40
            ... )
            >>> A.spatial_relation_2d(B)
            'in'
            >>> A.spatial_relation(B)
            'overlap'
            >>> B = SpatialExtent(
            ...     north=90, south=5, east=70, west=5, bottom=-40, top=60
            ... )
            >>> A.spatial_relation(B)
            'overlap'
            >>> B = SpatialExtent(
            ...     north=90, south=5, east=70, west=5, bottom=-60, top=60
            ... )
            >>> A.spatial_relation(B)
            'in'
            >>> A = SpatialExtent(
            ...     north=80, south=60, east=60, west=10, bottom=-50, top=50
            ... )
            >>> B = SpatialExtent(
            ...     north=60, south=20, east=60, west=10, bottom=-50, top=50
            ... )
            >>> A.spatial_relation_2d(B)
            'meet'
            >>> A.spatial_relation(B)
            'meet'
            >>> A = SpatialExtent(
            ...     north=60, south=40, east=60, west=10, bottom=-50, top=50
            ... )
            >>> B = SpatialExtent(
            ...     north=80, south=60, east=60, west=10, bottom=-50, top=50
            ... )
            >>> A.spatial_relation_2d(B)
            'meet'
            >>> A.spatial_relation(B)
            'meet'
            >>> A = SpatialExtent(
            ...     north=80, south=40, east=60, west=40, bottom=-50, top=50
            ... )
            >>> B = SpatialExtent(
            ...     north=80, south=40, east=40, west=20, bottom=-50, top=50
            ... )
            >>> A.spatial_relation_2d(B)
            'meet'
            >>> A.spatial_relation(B)
            'meet'
            >>> A = SpatialExtent(
            ...     north=80, south=40, east=40, west=20, bottom=-50, top=50
            ... )
            >>> B = SpatialExtent(
            ...     north=90, south=30, east=60, west=40, bottom=-50, top=50
            ... )
            >>> A.spatial_relation_2d(B)
            'meet'
            >>> A.spatial_relation(B)
            'meet'
            >>> A = SpatialExtent(
            ...     north=80, south=40, east=40, west=20, bottom=-50, top=50
            ... )
            >>> B = SpatialExtent(
            ...     north=70, south=50, east=60, west=40, bottom=-50, top=50
            ... )
            >>> A.spatial_relation_2d(B)
            'meet'
            >>> A.spatial_relation(B)
            'meet'
            >>> A = SpatialExtent(
            ...     north=80, south=40, east=40, west=20, bottom=-50, top=50
            ... )
            >>> B = SpatialExtent(
            ...     north=60, south=20, east=60, west=40, bottom=-50, top=50
            ... )
            >>> A.spatial_relation_2d(B)
            'meet'
            >>> A.spatial_relation(B)
            'meet'
            >>> A = SpatialExtent(
            ...     north=80, south=40, east=40, west=20, bottom=-50, top=50
            ... )
            >>> B = SpatialExtent(
            ...     north=40, south=20, east=60, west=40, bottom=-50, top=50
            ... )
            >>> A.spatial_relation_2d(B)
            'disjoint'
            >>> A.spatial_relation(B)
            'disjoint'
            >>> A = SpatialExtent(
            ...     north=80, south=40, east=40, west=20, bottom=-50, top=50
            ... )
            >>> B = SpatialExtent(
            ...     north=60, south=20, east=60, west=40, bottom=-60, top=60
            ... )
            >>> A.spatial_relation(B)
            'meet'
            >>> A = SpatialExtent(
            ...     north=80, south=40, east=40, west=20, bottom=-50, top=50
            ... )
            >>> B = SpatialExtent(
            ...     north=90, south=30, east=60, west=40, bottom=-40, top=40
            ... )
            >>> A.spatial_relation(B)
            'meet'
            >>> A = SpatialExtent(
            ...     north=80, south=40, east=60, west=20, bottom=0, top=50
            ... )
            >>> B = SpatialExtent(
            ...     north=80, south=40, east=60, west=20, bottom=-50, top=0
            ... )
            >>> A.spatial_relation(B)
            'meet'
            >>> A = SpatialExtent(
            ...     north=80, south=40, east=60, west=20, bottom=0, top=50
            ... )
            >>> B = SpatialExtent(
            ...     north=80, south=50, east=60, west=30, bottom=-50, top=0
            ... )
            >>> A.spatial_relation(B)
            'meet'
            >>> A = SpatialExtent(
            ...     north=80, south=40, east=60, west=20, bottom=0, top=50
            ... )
            >>> B = SpatialExtent(
            ...     north=70, south=50, east=50, west=30, bottom=-50, top=0
            ... )
            >>> A.spatial_relation(B)
            'meet'
            >>> A = SpatialExtent(
            ...     north=80, south=40, east=60, west=20, bottom=0, top=50
            ... )
            >>> B = SpatialExtent(
            ...     north=90, south=30, east=70, west=10, bottom=-50, top=0
            ... )
            >>> A.spatial_relation(B)
            'meet'
            >>> A = SpatialExtent(
            ...     north=80, south=40, east=60, west=20, bottom=0, top=50
            ... )
            >>> B = SpatialExtent(
            ...     north=70, south=30, east=50, west=10, bottom=-50, top=0
            ... )
            >>> A.spatial_relation(B)
            'meet'
            >>> A = SpatialExtent(
            ...     north=80, south=40, east=60, west=20, bottom=-50, top=0
            ... )
            >>> B = SpatialExtent(
            ...     north=80, south=40, east=60, west=20, bottom=0, top=50
            ... )
            >>> A.spatial_relation(B)
            'meet'
            >>> A = SpatialExtent(
            ...     north=80, south=40, east=60, west=20, bottom=-50, top=0
            ... )
            >>> B = SpatialExtent(
            ...     north=80, south=50, east=60, west=30, bottom=0, top=50
            ... )
            >>> A.spatial_relation(B)
            'meet'
            >>> A = SpatialExtent(
            ...     north=80, south=40, east=60, west=20, bottom=-50, top=0
            ... )
            >>> B = SpatialExtent(
            ...     north=70, south=50, east=50, west=30, bottom=0, top=50
            ... )
            >>> A.spatial_relation(B)
            'meet'
            >>> A = SpatialExtent(
            ...     north=80, south=40, east=60, west=20, bottom=-50, top=0
            ... )
            >>> B = SpatialExtent(
            ...     north=90, south=30, east=70, west=10, bottom=0, top=50
            ... )
            >>> A.spatial_relation(B)
            'meet'
            >>> A = SpatialExtent(
            ...     north=80, south=40, east=60, west=20, bottom=-50, top=0
            ... )
            >>> B = SpatialExtent(
            ...     north=70, south=30, east=50, west=10, bottom=0, top=50
            ... )
            >>> A.spatial_relation(B)
            'meet'
            >>> A = SpatialExtent(
            ...     north=80, south=20, east=60, west=10, bottom=-50, top=50
            ... )
            >>> B = SpatialExtent(
            ...     north=90, south=81, east=60, west=10, bottom=-50, top=50
            ... )
            >>> A.spatial_relation(B)
            'disjoint'
            >>> A = SpatialExtent(
            ...     north=80, south=20, east=60, west=10, bottom=-50, top=50
            ... )
            >>> B = SpatialExtent(
            ...     north=90, south=80, east=60, west=10, bottom=-50, top=50
            ... )
            >>> A.spatial_relation(B)
            'meet'

        """

        if self.equivalent(extent):
            return "equivalent"
        if self.contain(extent):
            return "contain"
        if self.is_in(extent):
            return "in"
        if self.cover(extent):
            return "cover"
        if self.covered(extent):
            return "covered"
        if self.overlap(extent):
            return "overlap"
        if self.meet(extent):
            return "meet"
        if self.disjoint(extent):
            return "disjoint"

        return "unknown"

    def set_spatial_extent_from_values(
        self, north, south, east, west, top, bottom
    ) -> None:
        """Set the three dimensional spatial extent

        :param north: The northern edge
        :param south: The southern edge
        :param east: The eastern edge
        :param west: The western edge
        :param top: The top edge
        :param bottom: The bottom edge
        """

        self.set_north(north)
        self.set_south(south)
        self.set_east(east)
        self.set_west(west)
        self.set_top(top)
        self.set_bottom(bottom)

    def set_spatial_extent(self, spatial_extent) -> None:
        """Set the three dimensional spatial extent

        :param spatial_extent: An object of type SpatialExtent or its subclasses
        """

        self.set_north(spatial_extent.get_north())
        self.set_south(spatial_extent.get_south())
        self.set_east(spatial_extent.get_east())
        self.set_west(spatial_extent.get_west())
        self.set_top(spatial_extent.get_top())
        self.set_bottom(spatial_extent.get_bottom())

    def set_projection(self, proj) -> None:
        """Set the projection of the spatial extent it should be XY or LL.
        As default the projection is XY
        """
        if proj is None or (proj not in {"XY", "LL"}):
            self.D["proj"] = "XY"
        else:
            self.D["proj"] = proj

    def set_spatial_extent_from_values_2d(self, north, south, east, west) -> None:
        """Set the two dimensional spatial extent from values

        :param north: The northern edge
        :param south: The southern edge
        :param east: The eastern edge
        :param west: The western edge
        """

        self.set_north(north)
        self.set_south(south)
        self.set_east(east)
        self.set_west(west)

    def set_spatial_extent_2d(self, spatial_extent) -> None:
        """Set the three dimensional spatial extent

        :param spatial_extent: An object of type SpatialExtent or its subclasses
        """

        self.set_north(spatial_extent.north)
        self.set_south(spatial_extent.south)
        self.set_east(spatial_extent.east)
        self.set_west(spatial_extent.west)

    def set_id(self, ident) -> None:
        """Convenient method to set the unique identifier (primary key)"""
        self.ident = ident
        self.D["id"] = ident

    def set_north(self, north) -> None:
        """Set the northern edge of the map"""
        if north is not None:
            self.D["north"] = float(north)
        else:
            self.D["north"] = None

    def set_south(self, south) -> None:
        """Set the southern edge of the map"""
        if south is not None:
            self.D["south"] = float(south)
        else:
            self.D["south"] = None

    def set_west(self, west) -> None:
        """Set the western edge of the map"""
        if west is not None:
            self.D["west"] = float(west)
        else:
            self.D["west"] = None

    def set_east(self, east) -> None:
        """Set the eastern edge of the map"""
        if east is not None:
            self.D["east"] = float(east)
        else:
            self.D["east"] = None

    def set_top(self, top) -> None:
        """Set the top edge of the map"""
        if top is not None:
            self.D["top"] = float(top)
        else:
            self.D["top"] = None

    def set_bottom(self, bottom) -> None:
        """Set the bottom edge of the map"""
        if bottom is not None:
            self.D["bottom"] = float(bottom)
        else:
            self.D["bottom"] = None

    def get_id(self):
        """Convenient method to get the unique identifier (primary key)

        :return: None if not found
        """
        if "id" in self.D:
            return self.D["id"]
        return None

    def get_projection(self):
        """Get the projection of the spatial extent"""
        return self.D["proj"]

    def get_volume(self):
        """Compute the volume of the extent, in case z is zero
        (top == bottom or top - bottom = 1) the area is returned"""

        if self.get_projection() == "LL":
            self.msgr.error(_("Volume computation is not supported for LL projections"))

        area = self.get_area()

        bbox = self.get_spatial_extent_as_tuple()

        z = abs(bbox[4] - bbox[5])

        if z == 0:
            z = 1.0

        return area * z

    def get_area(self):
        """Compute the area of the extent, extent in z direction is ignored"""

        if self.get_projection() == "LL":
            self.msgr.error(_("Area computation is not supported for LL projections"))

        bbox = self.get_spatial_extent_as_tuple()

        y = abs(bbox[0] - bbox[1])
        x = abs(bbox[2] - bbox[3])

        return x * y

    def get_spatial_extent_as_tuple(self):
        """Return a tuple (north, south, east, west, top, bottom)
        of the spatial extent"""

        return (self.north, self.south, self.east, self.west, self.top, self.bottom)

    def get_spatial_extent_as_tuple_2d(self):
        """Return a tuple (north, south, east, west,) of the 2d spatial extent"""
        return (self.north, self.south, self.east, self.west)

    def get_north(self):
        """Get the northern edge of the map

        :return: None if not found
        """
        if "north" in self.D:
            return self.D["north"]
        return None

    def get_south(self):
        """Get the southern edge of the map

        :return: None if not found
        """
        if "south" in self.D:
            return self.D["south"]
        return None

    def get_east(self):
        """Get the eastern edge of the map

        :return: None if not found
        """
        if "east" in self.D:
            return self.D["east"]
        return None

    def get_west(self):
        """Get the western edge of the map

        :return: None if not found
        """
        if "west" in self.D:
            return self.D["west"]
        return None

    def get_top(self):
        """Get the top edge of the map

        :return: None if not found
        """
        if "top" in self.D:
            return self.D["top"]
        return None

    def get_bottom(self):
        """Get the bottom edge of the map

        :return: None if not found
        """
        if "bottom" in self.D:
            return self.D["bottom"]
        return None

    id = property(fget=get_id, fset=set_id)
    north = property(fget=get_north, fset=set_north)
    south = property(fget=get_south, fset=set_south)
    east = property(fget=get_east, fset=set_east)
    west = property(fget=get_west, fset=set_west)
    top = property(fget=get_top, fset=set_top)
    bottom = property(fget=get_bottom, fset=set_bottom)

    def print_info(self) -> None:
        """Print information about this class in human readable style"""
        #      0123456789012345678901234567890
        print(
            " +-------------------- Spatial extent ----------------------------------------+"  # noqa: E501
        )
        print(" | North:...................... " + str(self.get_north()))
        print(" | South:...................... " + str(self.get_south()))
        print(" | East:.. .................... " + str(self.get_east()))
        print(" | West:....................... " + str(self.get_west()))
        print(" | Top:........................ " + str(self.get_top()))
        print(" | Bottom:..................... " + str(self.get_bottom()))

    def print_shell_info(self) -> None:
        """Print information about this class in shell style"""
        print("north=" + str(self.get_north()))
        print("south=" + str(self.get_south()))
        print("east=" + str(self.get_east()))
        print("west=" + str(self.get_west()))
        print("top=" + str(self.get_top()))
        print("bottom=" + str(self.get_bottom()))


###############################################################################


class RasterSpatialExtent(SpatialExtent):
    def __init__(
        self,
        ident=None,
        north=None,
        south=None,
        east=None,
        west=None,
        top=None,
        bottom=None,
    ) -> None:
        SpatialExtent.__init__(
            self, "raster_spatial_extent", ident, north, south, east, west, top, bottom
        )


class Raster3DSpatialExtent(SpatialExtent):
    def __init__(
        self,
        ident=None,
        north=None,
        south=None,
        east=None,
        west=None,
        top=None,
        bottom=None,
    ) -> None:
        SpatialExtent.__init__(
            self,
            "raster3d_spatial_extent",
            ident,
            north,
            south,
            east,
            west,
            top,
            bottom,
        )


class VectorSpatialExtent(SpatialExtent):
    def __init__(
        self,
        ident=None,
        north=None,
        south=None,
        east=None,
        west=None,
        top=None,
        bottom=None,
    ) -> None:
        SpatialExtent.__init__(
            self, "vector_spatial_extent", ident, north, south, east, west, top, bottom
        )


class STRDSSpatialExtent(SpatialExtent):
    def __init__(
        self,
        ident=None,
        north=None,
        south=None,
        east=None,
        west=None,
        top=None,
        bottom=None,
    ) -> None:
        SpatialExtent.__init__(
            self, "strds_spatial_extent", ident, north, south, east, west, top, bottom
        )


class STR3DSSpatialExtent(SpatialExtent):
    def __init__(
        self,
        ident=None,
        north=None,
        south=None,
        east=None,
        west=None,
        top=None,
        bottom=None,
    ) -> None:
        SpatialExtent.__init__(
            self, "str3ds_spatial_extent", ident, north, south, east, west, top, bottom
        )


class STVDSSpatialExtent(SpatialExtent):
    def __init__(
        self,
        ident=None,
        north=None,
        south=None,
        east=None,
        west=None,
        top=None,
        bottom=None,
    ) -> None:
        SpatialExtent.__init__(
            self, "stvds_spatial_extent", ident, north, south, east, west, top, bottom
        )


###############################################################################

if __name__ == "__main__":
    import doctest

    doctest.testmod()
