
    :param num_level_zero_tiles_x: Number of tiles at level zero in X direction.
    :param num_level_zero_tiles_y:  Number of tiles at level zero in Y direction.
    :param tile_width: The tile width.
    :param tile_height: The tile height.
    :param geo_extent: The geographical extent.
    """

    def __init__(self,
                 num_levels: int,
                 num_level_zero_tiles_x: int,
                 num_level_zero_tiles_y: int,
                 tile_width: int,
                 tile_height: int,
                 geo_extent: GeoExtent):
        self.num_levels = num_levels
        self.tile_width = tile_width
        self.tile_height = tile_height
        self.num_level_zero_tiles_x = num_level_zero_tiles_x
        self.num_level_zero_tiles_y = num_level_zero_tiles_y
        self.geo_extent = geo_extent

    def num_tiles(self, level: int) -> Tuple[int, int]:
        return self.num_tiles_x(level), self.num_tiles_y(level)

    def num_tiles_x(self, level: int) -> int:
        return self.num_level_zero_tiles_x * (1 << level)

    def num_tiles_y(self, level: int) -> int:
        return self.num_level_zero_tiles_y * (1 << level)

    def size(self, level: int) -> Tuple[int, int]:
        return self.width(level), self.height(level)

    def width(self, level: int) -> int:
        return self.num_tiles_x(level) * self.tile_width

    def height(self, level: int) -> int:
        return self.num_tiles_y(level) * self.tile_height

    @property
    def tile_size(self) -> Tuple[int, int]:
        return self.tile_width, self.tile_height

    @property
    def min_width(self) -> int:
        return self.width(0)

    @property
    def min_height(self) -> int:
        return self.height(0)

    @property
    def max_width(self) -> int:
        return self.width(self.num_levels - 1)

    @property
    def max_height(self) -> int:
        return self.height(self.num_levels - 1)

    def __hash__(self) -> int:
        return self.num_levels \
               + 2 * self.tile_width \
               + 4 * self.tile_height \
               + 8 * self.num_level_zero_tiles_x \
               + 16 * self.num_level_zero_tiles_y \
               + hash(self.geo_extent)  # noqa: E126

    def __eq__(self, o: Any) -> bool:
        try:
            return self.num_levels == o.num_levels \
                   and self.tile_width == o.tile_width \
                   and self.tile_height == o.tile_height \
                   and self.num_level_zero_tiles_x == o.num_level_zero_tiles_x \
                   and self.num_level_zero_tiles_y == o.num_level_zero_tiles_y \
                   and self.geo_extent == o.geo_extent  # noqa: E126
        except AttributeError:
            return False

    def __str__(self):
        return '\n'.join(['number of pyramid levels: {nl}'.format(nl=self.num_levels),
                          'number of tiles at level zero: {nx} x {ny}'.format(nx=self.num_level_zero_tiles_x,
                                                                              ny=self.num_level_zero_tiles_y),
                          'pyramid tile size: {tw} x {th}'.format(tw=self.tile_width, th=self.tile_height),
                          'image size at level zero: {w} x {h}'.format(w=self.min_width, h=self.min_height),
                          'image size at level {k}: {w} x {h}'.format(k=self.num_levels - 1,
                                                                      w=self.max_width, h=self.max_height)])

    def __repr__(self):
        return 'TilingScheme(%s, %s, %s, %s, %s, %s)' % (
            self.num_levels, self.num_level_zero_tiles_x, self.num_level_zero_tiles_y,
            self.tile_width, self.tile_height, repr(self.geo_extent))

    def to_json(self):
        return dict(numLevelZeroTilesX=self.num_level_zero_tiles_x,
                    numLevelZeroTilesY=self.num_level_zero_tiles_y,
                    tileWidth=self.tile_width,
                    tileHeight=self.tile_height,
                    numLevels=self.num_levels,
                    invY=self.geo_extent.inv_y,
                    extent=dict(west=self.geo_extent.west,
                                south=self.geo_extent.south,
                                east=self.geo_extent.east,
                                north=self.geo_extent.north))

    @classmethod
    def create(cls,
               w: int, h: int,
               tile_width: int, tile_height: int,
               geo_extent: GeoExtent) -> 'TilingScheme':
        """
        Create a new TilingScheme object for image size given by *w* and *h*.

        *geo_spatial_rect* is a tuple (*x1*, *y1*, *x2*, *y2*) and is the geo-spatial rectangle
        that covers all grid cells and is extracted directly from the geo-location information
        in the (NetCDF/CF) file:
        * *x1* longitude of center of first row and first column in image;
        * *y1* latitude of center of first row and first column in image;
        * *x2* longitude of center of last row and last column in image;
        * *y2* latitude of center of last row and last column in image.

        Note that
        * *x1* may be greater than *x2* which means that the anti-meridian is crossed;
        * *y1* is usually greater than *y2* meaning that the latitude axis is flipped.

        :param w: original image width
        :param h: original image height
        :param tile_width: optimal tile width
        :param tile_height: optimal tile height
        :param geo_extent: The geo-spatial extent
        :return: A new TilingScheme object
        """
        gsb_x1, gsb_y1, gsb_x2, gsb_y2 = geo_extent.coords

        if gsb_x1 < gsb_x2:
            # crossing_anti-meridian = False
            gsb_w = gsb_x2 - gsb_x1
        else:
            # crossing_anti-meridian = True
            gsb_w = 360. + gsb_x2 - gsb_x1

        gsb_h = gsb_y2 - gsb_y1

        w_mode = MODE_GE
        if gsb_x1 == -180. and gsb_x2 == 180.:
            w_mode = MODE_EQ
        h_mode = MODE_GE
        if gsb_y1 == -90. and gsb_y1 == 90. or gsb_y1 == 90. and gsb_y2 == -90.:
            h_mode = MODE_EQ

        (w_new, h_new), (tw, th), (nt0x, nt0y), nl = pow2_2d_subdivision(w, h,
                                                                         w_mode=w_mode, h_mode=h_mode,
                                                                         tw_opt=min(w, tile_width or 512),
                                                                         th_opt=min(h, tile_height or 512))

        assert w_new >= w
        assert h_new >= h