Commit 3a64a9f3 authored by Bhoopalam's avatar Bhoopalam
Browse files

Initial backend

parent f2aa86d9
FROM ghcr.io/osgeo/gdal:alpine-normal-3.8.5
WORKDIR /srv/app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app ./app
ENV PORT=8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
\ No newline at end of file
# Quick start
## Local (Python)
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # fill credentials
uvicorn app.main:app --reload
Open http://localhost:8000/docs
### Example: Search CDSE Sentinel‑2 L2A
curl -X POST http://localhost:8000/search \
-H 'content-type: application/json' \
-d '{
"provider":"cdse",
"bbox":{"minx":20.78,"miny":50.16,"maxx":20.90,"maxy":50.24},
"datetime":"2020-01-01/2020-12-31",
"max_cloud":20,
"limit":10
}'
### Example: Build a time series for slider (CDSE)
curl -X POST http://localhost:8000/timeseries \
-H 'content-type: application/json' \
-d '{
"provider":"cdse",
"bbox":{"minx":20.78,"miny":50.16,"maxx":20.90,"maxy":50.24},
"datetime":"2019-01-01/2024-12-31",
"max_cloud":15,
"limit":30
}'
### Example: View COG tiles (True Color) using TiTiler
# Replace <COG_URL> with a Sentinel‑2 L2A COG (e.g., from AWS sentinel-cogs)
open "http://localhost:8000/tiles/cog/tilejson.json?url=<COG_URL>&assets=B04,B03,B02"
## Docker
docker build -t imagery-api .
docker run -p 8000:8000 --env-file .env imagery-api
\ No newline at end of file
import httpx
from typing import Optional, List, Dict, Any
from app.settings import settings
from app.schemas import ImageryItem, ItemAsset
from app.util import build_copernicus_browser_link, build_earth_explorer_link
# Default collections for CDSE
DEFAULT_COLLECTIONS = ["sentinel-2-l2a"]
async def search_cdse(
*,
bbox: Optional[List[float]] = None,
geometry: Optional[Dict[str, Any]] = None,
datetime: str,
max_cloud: Optional[float] = 30,
collections: Optional[List[str]] = None,
limit: int = 50,
sort: str = "desc",
) -> Dict[str, Any]:
url = f"{settings.cdse_stac_endpoint}/search"
cols = collections or DEFAULT_COLLECTIONS
query: Dict[str, Any] = {
"collections": cols,
"datetime": datetime,
"limit": limit,
"sortby": [{"field": "properties.datetime", "direction": sort}],
}
if bbox:
query["bbox"] = bbox
if geometry:
query["intersects"] = geometry
if max_cloud is not None:
# STAC eo:cloud_cover (<= max_cloud)
query.setdefault("query", {})["eo:cloud_cover"] = {"lte": max_cloud}
async with httpx.AsyncClient(timeout=60) as client:
r = await client.post(url, json=query)
r.raise_for_status()
return r.json()
def normalize_cdse_item(feat: Dict[str, Any]) -> ImageryItem:
props = feat.get("properties", {})
assets = {
k: ItemAsset(key=k, href=v.get("href"), roles=v.get("roles"), type=v.get("type"))
for k, v in feat.get("assets", {}).items()
}
# Thumbnail / overview keys vary; try common ones
preview = None
for key in ("thumbnail", "rendered_preview", "overview", "preview"):
if key in feat.get("assets", {}):
preview = feat["assets"][key]["href"]
break
geom = feat.get("geometry") or {}
lon = geom.get("coordinates", [[[0, 0]]])[0][0][0]
lat = geom.get("coordinates", [[[0, 0]]])[0][0][1]
return ImageryItem(
id=feat.get("id"),
provider="cdse",
collection=feat.get("collection", "sentinel-2-l2a"),
datetime=props.get("datetime"),
geometry=geom,
bbox=feat.get("bbox", []),
assets=assets,
preview=preview,
linkouts={
"copernicus_browser": build_copernicus_browser_link(lat, lon, props.get("datetime", "")),
"earth_explorer": build_earth_explorer_link(),
},
)
\ No newline at end of file
import httpx
from typing import Optional, List, Dict, Any
from app.settings import settings
from app.schemas import ImageryItem, ItemAsset
from app.util import build_earth_explorer_link
# Common Landsat dataset names (search / override as needed)
DEFAULT_DATASETS = [
"LANDSAT_8_C2_L2",
"LANDSAT_9_C2_L2",
]
async def _m2m_auth(client: httpx.AsyncClient) -> str:
"""Authenticate using USGS M2M application token (login-token)."""
url = f"{settings.usgs_m2m_base}/api/login-token"
payload = {
"username": settings.usgs_m2m_username,
"token": settings.usgs_m2m_app_token,
}
r = await client.post(url, json=payload)
r.raise_for_status()
return r.json()["data"]["apiKey"]
async def search_usgs(
*,
bbox: Optional[List[float]] = None,
geometry: Optional[Dict[str, Any]] = None,
datetime: str,
datasets: Optional[List[str]] = None,
max_results: int = 50,
) -> List[Dict[str, Any]]:
datasets = datasets or DEFAULT_DATASETS
async with httpx.AsyncClient(timeout=60) as client:
api_key = await _m2m_auth(client)
headers = {"X-Auth-Token": api_key}
results: List[Dict[str, Any]] = []
for ds in datasets:
url = f"{settings.usgs_m2m_base}/api/scene-search"
payload: Dict[str, Any] = {
"datasetName": ds,
"maxResults": max_results,
"temporalFilter": {"start": datetime.split("/")[0], "end": datetime.split("/")[1]},
}
if bbox:
payload["spatialFilter"] = {"filterType": "mbr", "lowerLeft": {"longitude": bbox[0], "latitude": bbox[1]}, "upperRight": {"longitude": bbox[2], "latitude": bbox[3]}}
if geometry:
payload["spatialFilter"] = {"filterType": "geojson", "geoJson": geometry}
r = await client.post(url, json=payload, headers=headers)
r.raise_for_status()
data = r.json()
if data.get("data", {}).get("results"):
results.extend([dict(x, dataset=ds) for x in data["data"]["results"]])
return results
def normalize_usgs_item(scene: Dict[str, Any]) -> ImageryItem:
# USGS response differs; normalize minimally for the viewer
entity_id = scene.get("entityId") or scene.get("entity_id")
acq_date = scene.get("acquisitionDate") or scene.get("temporalCoverage", {}).get("startDate")
geom = scene.get("spatialBounds", {}).get("coordinates")
bbox = scene.get("spatialBounds", {}).get("bbox") or []
assets: Dict[str, ItemAsset] = {}
quicklook = scene.get("browseUrl") or scene.get("thumbnailUrl")
if quicklook:
assets["thumbnail"] = ItemAsset(key="thumbnail", href=quicklook, roles=["thumbnail"], type="image/jpeg")
return ImageryItem(
id=entity_id,
provider="usgs",
collection=scene.get("dataset"),
datetime=acq_date,
geometry={"type": "Polygon", "coordinates": geom} if geom else {"type": "Polygon", "coordinates": []},
bbox=bbox,
assets=assets,
preview=quicklook,
linkouts={
"earth_explorer": build_earth_explorer_link(),
},
)
\ No newline at end of file
from fastapi import Depends
from app.settings import settings
def get_settings():
return settings
\ No newline at end of file
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.settings import settings
from app.routers import search, timeseries, tiles
app = FastAPI(title="Imagery Time-Slider API", version="0.1.0")
app.add_middleware(
CORSMiddleware,
allow_origins=settings.allowed_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/health")
async def health():
return {"ok": True}
app.include_router(search.router)
app.include_router(timeseries.router)
app.include_router(tiles.router)
\ No newline at end of file
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment