diff --git a/covid_api/api/api_v1/api.py b/covid_api/api/api_v1/api.py index e787c41..8294a7f 100644 --- a/covid_api/api/api_v1/api.py +++ b/covid_api/api/api_v1/api.py @@ -11,6 +11,7 @@ groups, tiles, metadata, + planet, ) api_router = APIRouter() @@ -22,3 +23,4 @@ api_router.include_router(sites.router, tags=["sites"]) api_router.include_router(groups.router, tags=["indicator groups"]) api_router.include_router(detections.router, tags=["detections"]) +api_router.include_router(planet.router, tags=["planet"]) diff --git a/covid_api/api/api_v1/endpoints/planet.py b/covid_api/api/api_v1/endpoints/planet.py new file mode 100644 index 0000000..a2869d1 --- /dev/null +++ b/covid_api/api/api_v1/endpoints/planet.py @@ -0,0 +1,85 @@ +"""API planet mosaic tiles.""" + +from typing import Any, Dict, Union, Optional + +import re + +from functools import partial + +import numpy as np +import requests + +from fastapi import APIRouter, Depends, Query, Path +from starlette.concurrency import run_in_threadpool + +from rio_tiler.profiles import img_profiles +from rio_tiler.utils import render + +from covid_api.api import utils +from covid_api.db.memcache import CacheLayer +from covid_api.ressources.enums import ImageType +from covid_api.ressources.common import drivers, mimetype +from covid_api.ressources.responses import TileResponse + + +_render = partial(run_in_threadpool, render) +_tile = partial(run_in_threadpool, utils.planet_mosaic_tile) + +router = APIRouter() +responses = { + 200: { + "content": { + "image/png": {}, + "image/jpg": {}, + "image/webp": {}, + "image/tiff": {}, + "application/x-binary": {}, + }, + "description": "Return an image.", + } +} +tile_routes_params: Dict[str, Any] = dict( + responses=responses, tags=["planet"], response_class=TileResponse +) + + +@router.get(r"/planet/{z}/{x}/{y}", **tile_routes_params) +async def tile( + z: int = Path(..., ge=0, le=30, description="Mercator tiles's zoom level"), + x: int = Path(..., description="Mercator tiles's column"), + y: int = Path(..., description="Mercator tiles's row"), + scenes: str = Query(..., description="Comma separated Planets scenes to mosaic."), + cache_client: CacheLayer = Depends(utils.get_cache), +) -> TileResponse: + """Handle /planet requests.""" + timings = [] + headers: Dict[str, str] = {} + + tile_hash = utils.get_hash(**dict(z=z, x=x, y=y, scenes=scenes,)) + + content = None + if cache_client: + try: + content, ext = cache_client.get_image_from_cache(tile_hash) + headers["X-Cache"] = "HIT" + except Exception: + content = None + + if not content: + with utils.Timer() as t: + tile, mask = await _tile(scenes, x, y, z) + timings.append(("Read", t.elapsed)) + + content = await _render(tile, mask) + + timings.append(("Format", t.elapsed)) + + if cache_client and content: + cache_client.set_image_cache(tile_hash, (content)) + + if timings: + headers["X-Server-Timings"] = "; ".join( + ["{} - {:0.2f}".format(name, time * 1000) for (name, time) in timings] + ) + + return TileResponse(content, media_type=mimetype["png"], headers=headers) diff --git a/covid_api/api/utils.py b/covid_api/api/utils.py index 74260e7..7ba85c7 100644 --- a/covid_api/api/utils.py +++ b/covid_api/api/utils.py @@ -7,10 +7,16 @@ import time import json import hashlib +import math +import random +from io import BytesIO + import numpy as np from shapely.geometry import shape, box from rasterstats.io import bounds_window +from rasterio.io import MemoryFile +import requests from starlette.requests import Request @@ -28,6 +34,7 @@ from covid_api.db.memcache import CacheLayer from covid_api.models.timelapse import Feature +from covid_api.core.config import PLANET_API_KEY def get_cache(request: Request) -> CacheLayer: @@ -679,3 +686,31 @@ def get_custom_cmap(cname) -> Dict: ColorMapName = Enum("ColorMapNames", [(a, a) for a in COLOR_MAP_NAMES]) # type: ignore + + +def planet_mosaic_tile(scenes, x, y, z): + mosaic_tile = np.zeros((4, 256, 256), dtype=np.uint8) + for scene in scenes.split(","): + api_num = math.floor(random.random() * 3) + 1 + url = f"https://tiles{api_num}.planet.com/data/v1/PSScene3Band/{scene}/{z}/{x}/{y}.png?api_key={PLANET_API_KEY}" + r = requests.get(url) + with MemoryFile(BytesIO(r.content)) as memfile: + with memfile.open() as src: + data = src.read() + print(data.sum()) + # any place we don't have data yet, add some + mosaic_tile = np.where( + mosaic_tile[3] == 0, mosaic_tile + data, mosaic_tile + ) + + # if the tile is full, stop + if np.count_nonzero(mosaic_tile[3]) == mosaic_tile[3].size: + break + + # salt the resulting image + salt = np.random.randint(0, 3, (256, 256), dtype=np.uint8) + mosaic_tile[:3] = np.where( + mosaic_tile[:3] < 254, mosaic_tile[:3] + salt, mosaic_tile[:3] + ) + + return mosaic_tile[:3], mosaic_tile[3] diff --git a/covid_api/core/config.py b/covid_api/core/config.py index 3239403..576ad29 100644 --- a/covid_api/core/config.py +++ b/covid_api/core/config.py @@ -21,3 +21,5 @@ INDICATOR_BUCKET = os.environ.get("INDICATOR_BUCKET", "covid-eo-data") DT_FORMAT = "%Y-%m-%d" + +PLANET_API_KEY = os.getenv("PLANET_API_KEY")