def test_find_extrema_cross_antimeridian(): """Extrema should be calculated correctly""" features = [ { "geometry": { "coordinates": [ [-190.01, 49.15], [-101.95, -8.41], [-43.24, -32.84], [37.62, -25.17], [71.72, -7.01], [190.01, 48.69], ], "type": "LineString", }, "properties": {}, "type": "Feature", }, { "geometry": { "coordinates": [[-98.09, 61.44], [-46.76, 61.1]], "type": "LineString", }, "properties": {}, "type": "Feature", }, { "geometry": { "coordinates": [[-6.33, 59.89], [59.06, 59.89]], "type": "LineString", }, "properties": {}, "type": "Feature", }, ] bounds = find_extrema(features) assert bounds == ( -190.0099999999, -32.8399999999, 190.0099999999, 61.439999999899996, )
def test_find_extrema_clipped_northsouth(): """Extrema should be calculated correctly""" features = [ { "geometry": { "coordinates": [ [-190.01, 90], [-101.95, -8.41], [-43.24, -32.84], [37.62, -25.17], [71.72, -7.01], [190.01, -90], ], "type": "LineString", }, "properties": {}, "type": "Feature", }, { "geometry": { "coordinates": [[-98.09, 61.44], [-46.76, 61.1]], "type": "LineString", }, "properties": {}, "type": "Feature", }, { "geometry": { "coordinates": [[-6.33, 59.89], [59.06, 59.89]], "type": "LineString", }, "properties": {}, "type": "Feature", }, ] bounds = find_extrema(features) assert bounds == ( -190.0099999999, -85.0511287798066, 190.0099999999, 85.0511287798066, )
def stac_to_mosaicJSON( query: Dict, minzoom: int = 7, maxzoom: int = 12, optimized_selection: bool = True, maximum_items_per_tile: int = 20, stac_collection_limit: int = 500, seasons: Tuple = ["spring", "summer", "autumn", "winter"], stac_url: str = os.environ.get("SATAPI_URL", "https://sat-api.developmentseed.org"), ) -> Dict: """ Create a mosaicJSON from a stac request. Attributes ---------- query : str sat-api query. minzoom : int, optional, (default: 7) Mosaic Min Zoom. maxzoom : int, optional (default: 12) Mosaic Max Zoom. optimized_selection : bool, optional (default: true) Limit one Path-Row scene per quadkey. maximum_items_per_tile : int, optional (default: 20) Limit number of scene per quadkey. Use 0 to use all items. stac_collection_limit : int, optional (default: None) Limits the number of items returned by sat-api stac_url : str, optional (default: from ENV) Returns ------- out : dict MosaicJSON definition. """ if stac_collection_limit: query.update(limit=stac_collection_limit) logger.debug(json.dumps(query)) def fetch_sat_api(query): headers = { "Content-Type": "application/json", "Accept-Encoding": "gzip", "Accept": "application/geo+json", } url = f"{stac_url}/stac/search" data = requests.post(url, headers=headers, json=query).json() error = data.get("message", "") if error: raise Exception(f"SAT-API failed and returned: {error}") meta = data.get("meta", {}) if not meta.get("found"): return [] logger.debug(json.dumps(meta)) features = data["features"] if data["links"]: curr_page = int(meta["page"]) query["page"] = curr_page + 1 query["limit"] = meta["limit"] features = list(itertools.chain(features, fetch_sat_api(query))) return features features = fetch_sat_api(query) if not features: raise Exception(f"No asset found for query '{json.dumps(query)}'") logger.debug(f"Found: {len(features)} scenes") features = list( filter( lambda x: _get_season(x["properties"]["datetime"], max(x["bbox"][1], x["bbox"][3])) in seasons, features, )) if optimized_selection: dataset = [] prs = [] for item in features: pr = item["properties"]["eo:column"] + "-" + item["properties"][ "eo:row"] if pr not in prs: prs.append(pr) dataset.append(item) else: dataset = features if query.get("bbox"): bounds = query["bbox"] else: bounds = burntiles.find_extrema(dataset) for i in range(len(dataset)): dataset[i]["geometry"] = shape(dataset[i]["geometry"]) tiles = burntiles.burn([bbox_to_geojson(bounds)], minzoom) tiles = list(set(["{2}-{0}-{1}".format(*tile.tolist()) for tile in tiles])) logger.debug(f"Number tiles: {len(tiles)}") mosaic_definition = dict( mosaicjson="0.0.1", minzoom=minzoom, maxzoom=maxzoom, bounds=bounds, center=[(bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2, minzoom], tiles={}, ) for tile in tiles: z, x, y = list(map(int, tile.split("-"))) tile = mercantile.Tile(x=x, y=y, z=z) quadkey = mercantile.quadkey(*tile) geometry = box(*mercantile.bounds(tile)) intersect_dataset = list( filter(lambda x: geometry.intersects(x["geometry"]), dataset)) if len(intersect_dataset): # We limit the item per quadkey to 20 if maximum_items_per_tile: intersect_dataset = intersect_dataset[0:maximum_items_per_tile] mosaic_definition["tiles"][quadkey] = [ scene["properties"]["landsat:product_id"] for scene in intersect_dataset ] return mosaic_definition
def create_mosaic( dataset_list: Tuple, minzoom: int = None, maxzoom: int = None, max_threads: int = 20, minimum_tile_cover: float = None, tile_cover_sort: bool = False, version: str = "0.0.2", quiet: bool = True, ) -> Dict: """ Create mosaic definition content. Attributes ---------- dataset_list : tuple or list, required Dataset urls. minzoom: int, optional Force mosaic min-zoom. maxzoom: int, optional Force mosaic max-zoom. minimum_tile_cover: float, optional (default: 0) Filter files with low tile intersection coverage. tile_cover_sort: bool, optional (default: None) Sort intersecting files by coverage. max_threads : int Max threads to use (default: 20). version: str, optional mosaicJSON definition version quiet: bool, optional (default: True) Mask processing steps. Returns ------- mosaic_definition : dict Mosaic definition. """ if version not in ["0.0.1", "0.0.2"]: raise Exception(f"Invalid mosaicJSON's version: {version}") if not quiet: click.echo("Get files footprint", err=True) results = get_footprints(dataset_list, max_threads=max_threads, quiet=quiet) if minzoom is None: minzoom = list(set([feat["properties"]["minzoom"] for feat in results])) if len(minzoom) > 1: warnings.warn("Multiple MinZoom, Assets different minzoom values", UserWarning) minzoom = max(minzoom) if maxzoom is None: maxzoom = list(set([feat["properties"]["maxzoom"] for feat in results])) if len(maxzoom) > 1: warnings.warn( "Multiple MaxZoom, Assets have multiple resolution values", UserWarning) maxzoom = max(maxzoom) quadkey_zoom = minzoom datatype = list(set([feat["properties"]["datatype"] for feat in results])) if len(datatype) > 1: raise Exception("Dataset should have the same data type") if not quiet: click.echo(f"Get quadkey list for zoom: {quadkey_zoom}", err=True) tiles = burntiles.burn(results, quadkey_zoom) tiles = ["{2}-{0}-{1}".format(*tile.tolist()) for tile in tiles] bounds = burntiles.find_extrema(results) mosaic_definition = dict( mosaicjson=version, minzoom=minzoom, maxzoom=maxzoom, bounds=bounds, center=[(bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2, minzoom], tiles={}, version="1.0.0", ) if version == "0.0.2": mosaic_definition.update(dict(quadkey_zoom=quadkey_zoom)) if not quiet: click.echo(f"Feed Quadkey index", err=True) dataset_geoms = polygons( [feat["geometry"]["coordinates"][0] for feat in results]) dataset = [{ "path": f["properties"]["path"], "geometry": geom } for (f, geom) in zip(results, dataset_geoms)] for parent in tiles: z, x, y = list(map(int, parent.split("-"))) parent = mercantile.Tile(x=x, y=y, z=z) quad = mercantile.quadkey(*parent) tile_geometry = polygons( mercantile.feature(parent)["geometry"]["coordinates"][0]) fdataset = [ dataset[idx] for idx in numpy.nonzero( intersects(tile_geometry, dataset_geoms))[0] ] if minimum_tile_cover is not None or tile_cover_sort: fdataset = _filter_and_sort( tile_geometry, fdataset, minimum_cover=minimum_tile_cover, sort_cover=tile_cover_sort, ) if len(fdataset): mosaic_definition["tiles"][quad] = [f["path"] for f in fdataset] return mosaic_definition