Exemple #1
0
def check_crs(crs):

    """
    Checks a CRS instance

    Args:
        crs (``CRS`` | int | dict | str): The CRS instance.

    Returns:
        ``rasterio.crs.CRS``
    """

    if isinstance(crs, pyproj.crs.crs.CRS):
        dst_crs = CRS.from_proj4(crs.to_proj4())
    elif isinstance(crs, CRS):
        dst_crs = crs
    elif isinstance(crs, int):
        dst_crs = CRS.from_epsg(crs)
    elif isinstance(crs, dict):
        dst_crs = CRS.from_dict(crs)
    elif isinstance(crs, str):

        if crs.startswith('+proj'):
            dst_crs = CRS.from_proj4(crs)
        else:
            dst_crs = CRS.from_string(crs)

    else:
        logger.exception('  The CRS was not understood.')
        raise TypeError

    return dst_crs
def test_schema():
    """Translate Model to Schema."""
    tms = morecantile.tms.get("WebMercatorQuad")
    assert tms.schema()
    assert tms.schema_json()
    assert tms.json(exclude_none=True)
    assert tms.dict(exclude_none=True)

    crs = CRS.from_proj4(
        "+proj=stere +lat_0=90 +lon_0=0 +k=2 +x_0=0 +y_0=0 +R=3396190 +units=m +no_defs"
    )
    extent = [-13584760.000, -13585240.000, 13585240.000, 13584760.000]
    tms = morecantile.TileMatrixSet.custom(extent,
                                           crs,
                                           identifier="MarsNPolek2MOLA5k")
    assert tms.schema()
    assert tms.schema_json()
    assert tms.dict(exclude_none=True)
    json_doc = json.loads(tms.json(exclude_none=True))
    # We cannot translate PROJ4 to epsg so it's set to None
    assert json_doc[
        "supportedCRS"] == "http://www.opengis.net/def/crs/EPSG/0/None"

    crs = CRS.from_epsg(3031)
    extent = [-948.75, -543592.47, 5817.41,
              -3333128.95]  # From https:///epsg.io/3031
    tms = morecantile.TileMatrixSet.custom(extent,
                                           crs,
                                           identifier="MyCustomTmsEPSG3031")
    assert tms.schema()
    assert tms.schema_json()
    assert tms.json(exclude_none=True)
    json_doc = json.loads(tms.json(exclude_none=True))
    assert json_doc[
        "supportedCRS"] == "http://www.opengis.net/def/crs/EPSG/0/3031"
Exemple #3
0
def check_crs(crs):

    """
    Checks a CRS instance

    Args:
        crs (``CRS`` | int | dict | str): The CRS instance.

    Returns:
        ``rasterio.crs.CRS``
    """

    import warnings

    with rio.Env():

        with warnings.catch_warnings():

            warnings.simplefilter('ignore', UserWarning)

            if isinstance(crs, pyproj.crs.crs.CRS):
                dst_crs = CRS.from_proj4(crs.to_proj4())
            elif isinstance(crs, CRS):
                dst_crs = crs
            elif isinstance(crs, int):
                dst_crs = CRS.from_epsg(crs)
            elif isinstance(crs, dict):
                dst_crs = CRS.from_dict(crs)
            elif isinstance(crs, str):

                if crs.startswith('+proj'):
                    dst_crs = CRS.from_proj4(crs)
                else:
                    dst_crs = CRS.from_string(crs.replace('+init=', ''))

            else:
                logger.exception('  The CRS was not understood.')
                raise TypeError

    return dst_crs
Exemple #4
0
def _parse_crs(crs):
    """Parse a coordinate reference system from a variety of representations.

    Parameters
    ----------
    crs : {str, dict, int, CRS}
        Must be either a rasterio CRS object, a proj-string, rasterio supported
        dictionary, WKT string, or EPSG integer.

    Returns
    -------
    rasterio.crs.CRS
        The parsed CRS.

    Raises
    ------
    CRSError
        Raises an error if the input cannot be parsed.

    """

    #
    # NOTE: This doesn't currently throw an error if the EPSG code is invalid.
    #
    parsed = None
    if isinstance(crs, CRS):
        parsed = crs
    elif isinstance(crs, str):
        try:
            # proj-string or wkt
            parsed = CRS.from_string(crs)
        except CRSError:
            # wkt
            parsed = CRS.from_wkt(crs)
    elif isinstance(crs, dict):
        parsed = CRS(crs)
    elif isinstance(crs, int):
        parsed = CRS.from_epsg(crs)
    elif isinstance(crs, pyproj.Proj):
        parsed = CRS.from_proj4(crs.proj4_init)

    if parsed is None or not parsed.is_valid:
        raise CRSError('Could not parse CRS: {}'.format(crs))

    return parsed
Exemple #5
0
    def create_grs_schema(cls,
                          name,
                          description,
                          projection,
                          meridian,
                          degreesx,
                          degreesy,
                          bbox,
                          srid=100001):
        """Create a Brazil Data Cube Grid Schema."""
        grid_mapping, proj4 = create_grids(names=[name],
                                           projection=projection,
                                           meridian=meridian,
                                           degreesx=[degreesx],
                                           degreesy=[degreesy],
                                           bbox=bbox,
                                           srid=srid)
        grid = grid_mapping[name]

        with db.session.begin_nested():
            crs = CRS.from_proj4(proj4)
            data = dict(auth_name='Albers Equal Area',
                        auth_srid=srid,
                        srid=srid,
                        srtext=crs.to_wkt(),
                        proj4text=proj4)

            spatial_index, _ = get_or_create_model(SpatialRefSys,
                                                   defaults=data,
                                                   srid=srid)

            grs = GridRefSys.create_geometry_table(table_name=name,
                                                   features=grid['features'],
                                                   srid=srid)
            grs.description = description
            db.session.add(grs)
            for tile_obj in grid['tiles']:
                tile = Tile(**tile_obj, grs=grs)
                db.session.add(tile)
        db.session.commit()

        return 'Grid {} created with successfully'.format(name), 201
Exemple #6
0
    def _get_utm_crs(self, datum='WGS84', ellps='WGS84'):
        """Get coordinate system from zone number associated with the
        longitude of the northwest corner

        Parameters
        ==========
        datum : str, optional
            Origin of destination coordinate system, used to describe
            PROJ.4 string; default is WGS84.
        ellps : str, optional
            Ellipsoid defining the shape of the earth in the destination
            coordinate system, used to describe PROJ.4 string; default
            is WGS84.
        """
        #west, south, east, north = self.bounds
        self.zone_number = int((self.bounds[0] + 180) / 6) + 1
        proj = '+proj=utm +zone={:d} '.format(self.zone_number) \
             + '+datum={:s} +units=m +no_defs '.format(datum) \
             + '+ellps={:s} +towgs84=0,0,0'.format(ellps)
        self.utm_crs = CRS.from_proj4(proj)
Exemple #7
0
    def create_grs_schema(cls,
                          name,
                          description,
                          projection,
                          meridian,
                          degreesx,
                          degreesy,
                          bbox,
                          srid=100001):
        """Create a Brazil Data Cube Grid Schema."""
        bbox = bbox.split(',')
        bbox_obj = {
            "w": float(bbox[0]),
            "n": float(bbox[1]),
            "e": float(bbox[2]),
            "s": float(bbox[3])
        }
        tile_srs_p4 = "+proj=longlat +ellps=GRS80 +datum=GRS80 +no_defs"
        if projection == 'aea':
            tile_srs_p4 = "+proj=aea +lat_0=-12 +lon_0={} +lat_1=-2 +lat_2=-22 +x_0=5000000 +y_0=10000000 +ellps=GRS80 +units=m +no_defs".format(
                meridian)
        elif projection == 'sinu':
            tile_srs_p4 = "+proj=sinu +lon_0={} +x_0=0 +y_0=0 +a=6371007.181 +b=6371007.181 +units=m +no_defs".format(
                meridian)

        # Number of tiles and base tile
        num_tiles_x = int(360. / degreesx)
        num_tiles_y = int(180. / degreesy)
        h_base = num_tiles_x / 2
        v_base = num_tiles_y / 2

        # Tile size in meters (dx,dy) at center of system (argsmeridian,0.)
        src_crs = '+proj=longlat +ellps=GRS80 +datum=GRS80 +no_defs'
        dst_crs = tile_srs_p4
        xs = [(meridian - degreesx / 2), (meridian + degreesx / 2), meridian,
              meridian, 0.]
        ys = [0., 0., -degreesy / 2, degreesy / 2, 0.]
        out = transform(src_crs, dst_crs, xs, ys, zs=None)
        x1 = out[0][0]
        x2 = out[0][1]
        y1 = out[1][2]
        y2 = out[1][3]
        dx = x2 - x1
        dy = y2 - y1

        # Coordinates of WRS center (0.,0.) - top left coordinate of (h_base,v_base)
        x_center = out[0][4]
        y_center = out[1][4]
        # Border coordinates of WRS grid
        x_min = x_center - dx * h_base
        y_max = y_center + dy * v_base

        # Upper Left is (xl,yu) Bottom Right is (xr,yb)
        xs = [bbox_obj['w'], bbox_obj['e'], meridian, meridian]
        ys = [0., 0., bbox_obj['n'], bbox_obj['s']]
        out = transform(src_crs, dst_crs, xs, ys, zs=None)
        xl = out[0][0]
        xr = out[0][1]
        yu = out[1][2]
        yb = out[1][3]
        h_min = int((xl - x_min) / dx)
        h_max = int((xr - x_min) / dx)
        v_min = int((y_max - yu) / dy)
        v_max = int((y_max - yb) / dy)

        tiles = []
        features = []
        dst_crs = '+proj=longlat +ellps=GRS80 +datum=GRS80 +no_defs'
        src_crs = tile_srs_p4

        for ix in range(h_min, h_max + 1):
            x1 = x_min + ix * dx
            x2 = x1 + dx
            for iy in range(v_min, v_max + 1):
                y1 = y_max - iy * dy
                y2 = y1 - dy
                # Evaluate the bounding box of tile in longlat
                xs = [x1, x2, x2, x1]
                ys = [y1, y1, y2, y2]
                out = rasterio.warp.transform(src_crs,
                                              dst_crs,
                                              xs,
                                              ys,
                                              zs=None)

                polygon = from_shape(Polygon([(x1, y2), (x2, y2), (x2, y1),
                                              (x1, y1), (x1, y2)]),
                                     srid=SRID_ALBERS_EQUAL_AREA)

                # Insert tile
                tile_name = '{0:03d}{1:03d}'.format(ix, iy)
                tiles.append(dict(name=tile_name))
                features.append(dict(tile=tile_name, geom=polygon))

        with db.session.begin_nested():
            crs = CRS.from_proj4(tile_srs_p4)
            data = dict(auth_name='Albers Equal Area',
                        auth_srid=srid,
                        srid=srid,
                        srtext=crs.to_wkt(),
                        proj4text=tile_srs_p4)

            spatial_index, _ = get_or_create_model(SpatialRefSys,
                                                   defaults=data,
                                                   srid=srid)

            grs = GridRefSys.create_geometry_table(table_name=name,
                                                   features=features,
                                                   srid=SRID_ALBERS_EQUAL_AREA)
            grs.description = description
            db.session.add(grs)

            [db.session.add(Tile(**tile, grs=grs)) for tile in tiles]
        db.session.commit()

        return 'Grid {} created with successfully'.format(name), 201
Exemple #8
0
def test_exception_proj4():
    """Get the exception message we expect"""
    with pytest.raises(CRSError):
        CRS.from_proj4("+proj=bogus")
Exemple #9
0
def test_exception_proj4():
    """Get the exception message we expect"""
    with pytest.raises(CRSError):
        CRS.from_proj4("+proj=bogus")
    def mask(data, df, query=None, keep='in'):
        """
        Masks a DataArray by vector polygon geometry

        Args:
            data (DataArray): The ``xarray.DataArray`` to mask.
            df (GeoDataFrame or str): The ``geopandas.GeoDataFrame`` or filename to use for masking.
            query (Optional[str]): A query to apply to ``df``.
            keep (Optional[str]): If ``keep`` = 'in', mask values outside of the geometry (keep inside).
                Otherwise, if ``keep`` = 'out', mask values inside (keep outside).

        Returns:
             ``xarray.DataArray``

        Examples:
            >>> import geowombat as gw
            >>>
            >>> with gw.open('image.tif') as ds:
            >>>     ds = ds.gw.mask(df)
        """

        if isinstance(df, str) and os.path.isfile(df):
            df = gpd.read_file(df)

        if query:
            df = df.query(query)

        try:

            if data.crs.strip() != CRS.from_dict(df.crs).to_proj4().strip():

                # Re-project the DataFrame to match the image CRS
                df = df.to_crs(data.crs)

        except:

            if data.crs.strip() != CRS.from_proj4(df.crs).to_proj4().strip():
                df = df.to_crs(data.crs)

        # Rasterize the geometry and store as a DataArray
        mask = xr.DataArray(data=da.from_array(
            features.rasterize(list(df.geometry.values),
                               out_shape=(data.gw.nrows, data.gw.ncols),
                               transform=data.transform,
                               fill=0,
                               out=None,
                               all_touched=True,
                               default_value=1,
                               dtype='int32'),
            chunks=(data.gw.row_chunks, data.gw.col_chunks)),
                            dims=['y', 'x'],
                            coords={
                                'y': data.y.values,
                                'x': data.x.values
                            })

        # Return the masked array
        if keep == 'out':
            return data.where(mask != 1)
        else:
            return data.where(mask == 1)
    def clip(self, data, df, query=None, mask_data=False, expand_by=0):
        """
        Clips a DataArray by vector polygon geometry

        Args:
            data (DataArray): The ``xarray.DataArray`` to subset.
            df (GeoDataFrame or str): The ``geopandas.GeoDataFrame`` or filename to clip to.
            query (Optional[str]): A query to apply to ``df``.
            mask_data (Optional[bool]): Whether to mask values outside of the ``df`` geometry envelope.
            expand_by (Optional[int]): Expand the clip array bounds by ``expand_by`` pixels on each side.

        Returns:
             ``xarray.DataArray``

        Examples:
            >>> import geowombat as gw
            >>>
            >>> with gw.open('image.tif') as ds:
            >>>     ds = gw.clip(ds, df, query="Id == 1")
            >>>
            >>> # or
            >>>
            >>> with gw.open('image.tif') as ds:
            >>>     ds = ds.gw.clip(df, query="Id == 1")
        """

        if isinstance(df, str) and os.path.isfile(df):
            df = gpd.read_file(df)

        if query:
            df = df.query(query)

        try:

            if data.crs.strip() != CRS.from_dict(df.crs).to_proj4().strip():

                # Re-project the DataFrame to match the image CRS
                df = df.to_crs(data.crs)

        except:

            if data.crs.strip() != CRS.from_proj4(df.crs).to_proj4().strip():
                df = df.to_crs(data.crs)

        row_chunks = data.gw.row_chunks
        col_chunks = data.gw.col_chunks

        left, bottom, right, top = df.total_bounds

        # Align the geometry array grid
        align_transform, align_width, align_height = align_bounds(
            left, bottom, right, top, data.res)

        # Get the new bounds
        new_left, new_bottom, new_right, new_top = array_bounds(
            align_height, align_width, align_transform)

        if expand_by > 0:

            new_left -= data.gw.cellx * expand_by
            new_bottom -= data.gw.celly * expand_by
            new_right += data.gw.cellx * expand_by
            new_top += data.gw.celly * expand_by

        # Subset the array
        data = self.subset(data,
                           left=new_left,
                           bottom=new_bottom,
                           right=new_right,
                           top=new_top)

        if mask_data:

            # Rasterize the geometry and store as a DataArray
            mask = xr.DataArray(data=da.from_array(
                features.rasterize(list(df.geometry.values),
                                   out_shape=(align_height, align_width),
                                   transform=align_transform,
                                   fill=0,
                                   out=None,
                                   all_touched=True,
                                   default_value=1,
                                   dtype='int32'),
                chunks=(row_chunks, col_chunks)),
                                dims=['y', 'x'],
                                coords={
                                    'y': data.y.values,
                                    'x': data.x.values
                                })

            # Return the clipped array
            return data.where(mask == 1)

        else:
            return data
Exemple #12
0
def warp(filename,
         resampling='nearest',
         bounds=None,
         crs=None,
         res=None,
         nodata=0,
         warp_mem_limit=512,
         num_threads=1,
         tap=False):
    """
    Warps an image to a VRT object

    Args:
        filename (str): The input file name.
        resampling (Optional[str]): The resampling method. Choices are ['average', 'bilinear', 'cubic',
            'cubic_spline', 'gauss', 'lanczos', 'max', 'med', 'min', 'mode', 'nearest'].
        bounds (Optional[tuple]): The extent bounds to warp to.
        crs (Optional[object]): The CRS to warp to.
        res (Optional[tuple]): The cell resolution to warp to.
        nodata (Optional[int or float]): The 'no data' value.
        warp_mem_limit (Optional[int]): The memory limit (in MB) for the ``rasterio.vrt.WarpedVRT`` function.
        num_threads (Optional[int]): The number of warp worker threads.
        tap (Optional[bool]): Whether to target align pixels.

    Returns:
        ``rasterio.vrt.WarpedVRT``
    """

    with rio.open(filename) as src:

        if res:
            dst_res = res
        else:
            dst_res = src.res

        if crs:

            if isinstance(crs, CRS):
                dst_crs = crs
            elif isinstance(crs, int):
                dst_crs = CRS.from_epsg(crs)
            elif isinstance(crs, dict):
                dst_crs = CRS.from_dict(crs)
            elif isinstance(crs, str):

                if crs.startswith('+proj'):
                    dst_crs = CRS.from_proj4(crs)
                else:
                    dst_crs = CRS.from_string(crs)

            else:
                logger.exception('  The CRS was not understood.')

        else:
            dst_crs = src.crs

        dst_transform, dst_width, dst_height = calculate_default_transform(
            src.crs,
            dst_crs,
            src.width,
            src.height,
            left=src.bounds.left,
            bottom=src.bounds.bottom,
            right=src.bounds.right,
            top=src.bounds.top,
            resolution=dst_res)

        # Check if the data need to be subset
        if bounds and (bounds != src.bounds):

            if isinstance(bounds, str):

                if bounds.startswith('BoundingBox'):

                    bounds_str = bounds.replace('BoundingBox(', '').split(',')

                    for str_ in bounds_str:

                        if str_.strip().startswith('left='):
                            left_coord = float(
                                str_.strip().split('=')[1].replace(')', ''))
                        elif str_.strip().startswith('bottom='):
                            bottom_coord = float(
                                str_.strip().split('=')[1].replace(')', ''))
                        elif str_.strip().startswith('right='):
                            right_coord = float(
                                str_.strip().split('=')[1].replace(')', ''))
                        elif str_.strip().startswith('top='):
                            top_coord = float(
                                str_.strip().split('=')[1].replace(')', ''))

                    bounds = BoundingBox(left=left_coord,
                                         bottom=bottom_coord,
                                         right=right_coord,
                                         top=top_coord)

                else:
                    logger.exception('  The bounds were not accepted.')

            left, bottom, right, top = bounds

            # Keep the CRS but subset the data
            dst_transform, dst_width, dst_height = calculate_default_transform(
                dst_crs,
                dst_crs,
                dst_width,
                dst_height,
                left=left,
                bottom=bottom,
                right=right,
                top=top,
                resolution=dst_res)

            dst_width = int((right - left) / dst_res[0])
            dst_height = int((top - bottom) / dst_res[1])

        else:
            bounds = src.bounds

        # Do not warp if all the key metadata match the reference information
        if (src.bounds == bounds) and (src.res == dst_res) and (
                src.crs == dst_crs) and (src.width
                                         == dst_width) and (src.height
                                                            == dst_height):
            output = filename
        else:

            if tap:

                # Align the cells
                dst_transform, dst_width, dst_height = aligned_target(
                    dst_transform, dst_width, dst_height, dst_res)

            vrt_options = {
                'resampling': getattr(Resampling, resampling),
                'crs': dst_crs,
                'transform': dst_transform,
                'height': dst_height,
                'width': dst_width,
                'nodata': nodata,
                'warp_mem_limit': warp_mem_limit,
                'warp_extras': {
                    'multi': True,
                    'warp_option': 'NUM_THREADS={:d}'.format(num_threads)
                }
            }

            with WarpedVRT(src, **vrt_options) as vrt:
                output = vrt

    return output
def create_grids(names: List[str], projection, meridian,
                 degreesx: List[float], degreesy: List[float], bbox, srid=100001):
    """Create a list of hierarchically grids for Brazil Data Cube.

    With a meridian, bounding box and list of degrees, this function creates a list of grids
    based in the lowest degree. After that, the other grids are generated inherit the lowest
    to be hierarchical.

    For example, the grids BDC_SM_V2, BDC_MD_V2 and BDC_LG_V2 are hierarchically identical.
        - BDC_SM_V2 consists in tiles of 1.5 by 1.0 degrees.
        - BDC_MD_V2 represents 4 tiles of BDC_SM_V2 (2x2).
        - BDC_LG_V2 represents 16 tiles of BDC_SM_V2 (4x4) or 4 tiles of BDC_MD_V2 (2x2)

    Note:
        Keep the order of names according degree's

    Args:
        names (List[str]): List of grid names
        projection (str): The Grid projection kind - "sinu" for sinusoidal grids and "aea" for Albers Equal Area.
        meridian (float): The center pixel for grid as reference.
        degreesx (List[float]): List the degree in X
        degreesy (List[float]): List the degree in Y
        bbox (Tuple[float, float, float, float]): The bounding box limits (minx, miny, maxx, maxy).
        srid (int): The projection SRID to create.
    """
    bbox_obj = {
        "w": float(bbox[0]),
        "n": float(bbox[1]),
        "e": float(bbox[2]),
        "s": float(bbox[3])
    }
    tile_srs_p4 = "+proj=longlat +ellps=GRS80 +no_defs"
    if projection == 'aea':
        tile_srs_p4 = "+proj=aea +lat_0=-12 +lon_0={} +lat_1=-2 +lat_2=-22 +x_0=5000000 +y_0=10000000 +ellps=GRS80 +units=m +no_defs".format(
            meridian)
    elif projection == 'sinu':
        tile_srs_p4 = "+proj=sinu +lon_0={} +x_0=0 +y_0=0 +a=6371007.181 +b=6371007.181 +units=m +no_defs".format(
            meridian)

    ref_degree_x = degreesx[0]
    ref_degree_y = degreesy[0]

    # Tile size in meters (dx,dy) at center of system (argsmeridian,0.)
    src_crs = '+proj=longlat +ellps=GRS80 +no_defs'
    dst_crs = tile_srs_p4
    xs = [(meridian - ref_degree_x / 2.), (meridian + ref_degree_x / 2.), meridian, meridian, 0.]
    ys = [0., 0., -ref_degree_y / 2., ref_degree_y / 2., 0.]
    out = transform(CRS.from_proj4(src_crs), CRS.from_proj4(dst_crs), xs, ys, zs=None)
    xmin_center_tile = out[0][0]
    xmax_center_tile = out[0][1]
    ymin_center_tile = out[1][2]
    ymax_center_tile = out[1][3]
    tile_size_x = xmax_center_tile - xmin_center_tile
    tile_size_y = ymax_center_tile - ymin_center_tile

    bbox_alber_envelope_xs = [bbox_obj['w'], bbox_obj['e'], meridian, meridian]
    bbox_alber_envelope_ys = [0., 0., bbox_obj['n'], bbox_obj['s']]
    bbox_alber_envelope = transform(src_crs, dst_crs, bbox_alber_envelope_xs, bbox_alber_envelope_ys, zs=None)

    total_tiles_left = math.ceil(abs((xmin_center_tile - bbox_alber_envelope[0][0])) / tile_size_x)
    total_tiles_upper = math.ceil(abs((ymax_center_tile - bbox_alber_envelope[1][2])) / tile_size_y)

    # Border coordinates of WRS grid
    x_min = xmin_center_tile - (tile_size_x * total_tiles_left)
    y_max = ymax_center_tile + (tile_size_y * total_tiles_upper)

    # Upper Left is (xl,yu) Bottom Right is (xr,yb)
    xs = [bbox_obj['w'], bbox_obj['e'], meridian, meridian]
    ys = [0., 0., bbox_obj['n'], bbox_obj['s']]
    out = transform(src_crs, dst_crs, xs, ys, zs=None)
    xl = out[0][0]
    xr = out[0][1]
    yu = out[1][2]
    yb = out[1][3]

    grids = {}

    for grid_name, res_x, res_y in zip(names, degreesx, degreesy):
        factor_x = res_x / ref_degree_x
        factor_y = res_y / ref_degree_y
        grid_tile_size_x = tile_size_x * factor_x
        grid_tile_size_y = tile_size_y * factor_y
        tiles, features = _create_tiles(tile_size=(grid_tile_size_x, grid_tile_size_y),
                                        grid_x_min=x_min, grid_y_max=y_max,
                                        bbox=(xl, xr, yb, yu),
                                        srid=srid)
        grids[grid_name] = dict(
            tiles=tiles,
            features=features
        )

    return grids, tile_srs_p4