Commit 15b0ccdd authored by Bhoopalam's avatar Bhoopalam
Browse files

Version 2 with new endpoints

parent 3a64a9f3
from fastapi import APIRouter, HTTPException
from app.schemas import SearchBody, SearchResponse
from app.connectors.copernicus_stac import search_cdse, normalize_cdse_item
from app.connectors.usgs_m2m import search_usgs, normalize_usgs_item
from app.util import bbox_to_list
router = APIRouter(prefix="/search", tags=["search"])
@router.post("", response_model=SearchResponse)
async def search(body: SearchBody):
bbox_list = None
if body.bbox:
bbox_list = [body.bbox.minx, body.bbox.miny, body.bbox.maxx, body.bbox.maxy]
if body.provider == "cdse":
raw = await search_cdse(
bbox=bbox_list,
geometry=body.geometry,
datetime=body.datetime,
max_cloud=body.max_cloud,
collections=body.collections,
limit=body.limit,
sort=body.sort,
)
items = [normalize_cdse_item(f) for f in raw.get("features", [])]
return SearchResponse(items=items, next=raw.get("links", [{}])[-1].get("href"))
if body.provider == "usgs":
scenes = await search_usgs(
bbox=bbox_list,
geometry=body.geometry,
datetime=body.datetime,
datasets=body.collections,
max_results=body.limit,
)
items = [normalize_usgs_item(s) for s in scenes]
return SearchResponse(items=items)
# Optional AWS COG source kept for future expansion
raise HTTPException(400, detail="Unsupported provider for search")
\ No newline at end of file
from fastapi import APIRouter
from titiler.core.factory import TilerFactory
router = APIRouter()
# Mount TiTiler COG endpoints at /tiles/cog
cog = TilerFactory(router_prefix="/tiles/cog")
router.include_router(cog.router)
# Notes:
# - Use /tiles/cog/tilejson.json?url=<COG_URL>&assets=B04,B03,B02 for RGB
# - For S2 COGs on AWS, pass the HTTPS URL (RequesterPays OFF for Element84 COGs)
# - Frontend can consume the returned tilejson.tiles[0] template directly
\ No newline at end of file
from fastapi import APIRouter, HTTPException
from typing import List
from app.schemas import SearchBody, TimeSeriesResponse, TimeSlice
from app.routers.search import search as do_search
router = APIRouter(prefix="/timeseries", tags=["timeseries"])
@router.post("", response_model=TimeSeriesResponse)
async def timeseries(body: SearchBody):
# Reuse /search, then map to time slices; front-end can plug this into a slider
res = await do_search(body)
slices: List[TimeSlice] = []
for it in sorted(res.items, key=lambda x: x.datetime):
# If you mount /tiles (TiTiler), front-end can request dynamic RGB tiles per item/asset
slices.append(TimeSlice(
iso_date=it.datetime,
label=it.datetime.split("T")[0] if it.datetime else "",
item_id=it.id,
provider=it.provider,
# tile_template can be filled after you implement your own tiling logic
tile_template=None,
tilejson=None,
))
return TimeSeriesResponse(slices=slices)
\ No newline at end of file
from pydantic import BaseModel, Field
from typing import Literal, Optional, List, Dict, Any
Provider = Literal["cdse", "usgs", "aws_s2_cog"]
class BBox(BaseModel):
# WebMercator / WGS84 lon/lat
minx: float
miny: float
maxx: float
maxy: float
class SearchBody(BaseModel):
provider: Provider
bbox: Optional[BBox] = None
geometry: Optional[Dict[str, Any]] = None # GeoJSON
datetime: str = Field(..., description="STAC datetime interval e.g. 2020-01-01/2020-12-31")
max_cloud: Optional[float] = 30
collections: Optional[List[str]] = None # override defaults per provider
limit: int = 50
sort: Literal["asc", "desc"] = "desc"
class ItemAsset(BaseModel):
key: str
href: str
roles: Optional[List[str]] = None
type: Optional[str] = None
class ImageryItem(BaseModel):
id: str
provider: Provider
collection: str
datetime: str
geometry: Dict[str, Any]
bbox: List[float]
assets: Dict[str, ItemAsset]
preview: Optional[str] = None # thumbnail URL if available
linkouts: Optional[Dict[str, str]] = None # Copernicus Browser / EarthExplorer links
class SearchResponse(BaseModel):
items: List[ImageryItem]
next: Optional[str] = None
class TimeSlice(BaseModel):
iso_date: str
label: str
item_id: str
provider: Provider
tilejson: Optional[Dict[str, Any]] = None
tile_template: Optional[str] = None
class TimeSeriesResponse(BaseModel):
slices: List[TimeSlice]
\ No newline at end of file
from pydantic_settings import BaseSettings
from typing import List
class Settings(BaseSettings):
cdse_stac_endpoint: str = "https://stac.dataspace.copernicus.eu/v1"
cdse_oauth_token_url: str | None = None
cdse_client_id: str | None = None
cdse_client_secret: str | None = None
usgs_m2m_base: str = "https://m2m.cr.usgs.gov"
usgs_m2m_username: str | None = None
usgs_m2m_app_token: str | None = None
sentinel_hub_instance_id: str | None = None
tile_cache_dir: str = "./cache"
allowed_origins: List[str] = ["*"]
class Config:
env_file = ".env"
settings = Settings()
\ No newline at end of file
from typing import Dict, Any, List, Optional
def bbox_to_list(bbox) -> List[float]:
return [bbox.minx, bbox.miny, bbox.maxx, bbox.maxy]
def stac_datetime(start: str, end: str) -> str:
return f"{start}/{end}"
def iso_to_label(iso: str) -> str:
# 2020-05-17T10:11:12Z -> 2020‑05‑17
return iso.split("T")[0]
def build_copernicus_browser_link(lat: float, lon: float, date_iso: str, cloud: int = 30) -> str:
base = "https://browser.dataspace.copernicus.eu/"
return (f"{base}?zoom=13&lat={lat:.6f}&lng={lon:.6f}"
f"&themeId=DEFAULT-THEME&datasetId=S2_L2A_CDAS&cloudCoverage={cloud}"
f"&dateMode=SINGLE&toTime={date_iso}")
def build_earth_explorer_link() -> str:
# EE does not accept bbox in URL; provide homepage link
return "https://earthexplorer.usgs.gov/"
\ No newline at end of file
# === Copernicus Data Space Ecosystem ===
CDSE_STAC_ENDPOINT=https://stac.dataspace.copernicus.eu/v1
# OAuth client (needed for S3/download/process later; STAC search can be public)
CDSE_OAUTH_TOKEN_URL=https://identity.dataspace.copernicus.eu/auth/realms/CDSE/protocol/openid-connect/token
CDSE_CLIENT_ID=
CDSE_CLIENT_SECRET=
# === USGS M2M ===
# As of 2025, use Application Token with the login-token endpoint
USGS_M2M_USERNAME=
USGS_M2M_APP_TOKEN=
USGS_M2M_BASE=https://m2m.cr.usgs.gov
# === Optional: Sentinel Hub OGC (WMS/WMTS) passthrough ===
SENTINEL_HUB_INSTANCE_ID=
# === Tile cache & general ===
TILE_CACHE_DIR=./cache
ALLOWED_ORIGINS=*
LOG_LEVEL=info
\ No newline at end of file
fastapi==0.115.0
uvicorn[standard]==0.30.6
httpx==0.27.2
pydantic==2.9.2
pydantic-settings==2.5.2
python-dotenv==1.0.1
# Optional (for /tiles with COGs)
titiler.core==0.16.2
rio-tiler==6.7.0
\ No newline at end of file
This diff is collapsed.
{"type":"FeatureCollection","features":[{"type":"Feature","properties":{},"geometry":{"coordinates":[[[9.16145374315991,48.78558930650067],[9.16145374315991,48.77109004651268],[9.18585591426276,48.77109004651268],[9.18585591426276,48.78558930650067],[9.16145374315991,48.78558930650067]]],"type":"Polygon"}}]}
\ No newline at end of file
# Preprocess AOI and Download Satellite Imagery from Microsoft Planetary Computer
# Requirements: pip install geopandas rasterio pystac-client planetary-computer requests
import geopandas as gpd
import rasterio
from rasterio.mask import mask
import json
import os
from pystac_client import Client
import planetary_computer as pc
import requests
import matplotlib.pyplot as plt
# --- CONFIG ---
AOI_PATH = "map.geojson" # Your AOI file (exported from geojson.io or Google Earth)
BUFFER_METERS = 200 # Buffer distance in meters
OUTPUT_DIR = "processed" # Output directory for processed files
COLLECTION = "sentinel-2-l2a" # or "landsat-8-c2-l2"
DATE_RANGE = [
("2016-06-01", "2016-08-31"), # Past
("2025-06-01", "2025-08-31") # Present
]
MAX_CLOUD = 10 # %
# --- STEP 1: Buffer AOI ---
def buffer_aoi(aoi_path, buffer_meters, output_path):
aoi = gpd.read_file(aoi_path)
aoi_buffered = aoi.to_crs(epsg=3857).buffer(buffer_meters).to_crs(aoi.crs)
aoi_buffered_gdf = gpd.GeoDataFrame(geometry=aoi_buffered)
aoi_buffered_gdf.to_file(output_path, driver="GeoJSON")
print(f"Buffered AOI saved to {output_path}")
return output_path
# --- STEP 2: Download Image from MPC ---
def download_best_image(aoi_geojson, collection, date_range, max_cloud, out_path):
# Load AOI geometry
with open(aoi_geojson) as f:
geojson = json.load(f)
geometry = geojson['features'][0]['geometry']
# Search MPC STAC
catalog = Client.open("https://planetarycomputer.microsoft.com/api/stac/v1")
search = catalog.search(
collections=[collection],
intersects=geometry,
datetime=f"{date_range[0]}/{date_range[1]}",
query={"eo:cloud_cover": {"lt": max_cloud}}
)
items = list(search.get_items())
if not items:
print(f"No images found for {date_range}")
return None
# Pick the least cloudy image
items.sort(key=lambda x: x.properties.get("eo:cloud_cover", 100))
item = items[0]
asset = item.assets["visual"] if "visual" in item.assets else list(item.assets.values())[0]
signed_href = pc.sign(asset.href)
# Download the image
print(f"Downloading {signed_href} ...")
r = requests.get(signed_href, stream=True)
with open(out_path, "wb") as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
print(f"Downloaded image to {out_path}")
return out_path
# --- STEP 3: Display Images Side by Side ---
def show_images_side_by_side(image_paths):
fig, axes = plt.subplots(1, 2, figsize=(16, 8))
for i, img_path in enumerate(image_paths):
with rasterio.open(img_path) as src:
img = src.read([1, 2, 3]) # RGB bands
img = img.transpose(1, 2, 0)
# Normalize for display
img = (img - img.min()) / (img.max() - img.min())
axes[i].imshow(img)
axes[i].set_title(f"Image {i+1}")
axes[i].axis('off')
plt.tight_layout()
plt.show()
if __name__ == "__main__":
os.makedirs(OUTPUT_DIR, exist_ok=True)
# Download images only, skip buffer and clipping
image_paths = []
for i, drange in enumerate(DATE_RANGE):
out_img = os.path.join(OUTPUT_DIR, f"raw_{i+1}.tif")
img_path = download_best_image(AOI_PATH, COLLECTION, drange, MAX_CLOUD, out_img)
if img_path:
image_paths.append(img_path)
if len(image_paths) == 2:
show_images_side_by_side(image_paths)
print("\nAll done! You now have raw rasters in the 'processed' folder and can view them side by side.")
{
"type": "FeatureCollection",
"name": "map_buffered",
"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } },
"features": [
{ "type": "Feature", "properties": { }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 9.166994209422283, 48.784091968852827 ], [ 9.178154201230427, 48.784091968852827 ], [ 9.178330301820953, 48.784086268546858 ], [ 9.178504706466526, 48.78406922252217 ], [ 9.178675735555073, 48.784040994929839 ], [ 9.178841741982971, 48.784001857598447 ], [ 9.179001127017576, 48.783952187416872 ], [ 9.179152355693873, 48.783892462705673 ], [ 9.179293971597041, 48.783823258611697 ], [ 9.179424610888514, 48.783745241570486 ], [ 9.179543015440506, 48.783659162889563 ], [ 9.179648044952451, 48.783565851514489 ], [ 9.179738687932733, 48.783466206047081 ], [ 9.179814071439905, 48.783361186092904 ], [ 9.179873469489584, 48.783251803021074 ], [ 9.179916310046078, 48.783139110225356 ], [ 9.179942180531409, 48.783024192980392 ], [ 9.179950831798665, 48.782908157990647 ], [ 9.179950831798665, 48.778299983780748 ], [ 9.179942180531409, 48.778183937869152 ], [ 9.179916310046078, 48.77806900927871 ], [ 9.179873469489584, 48.777956304846001 ], [ 9.179814071439905, 48.777846909994913 ], [ 9.179738687932733, 48.777741878282676 ], [ 9.179648044952451, 48.777642221252542 ], [ 9.179543015440506, 48.777548898690974 ], [ 9.179424610888514, 48.777462809382932 ], [ 9.179293971597041, 48.777384782454767 ], [ 9.179152355693873, 48.77731556938766 ], [ 9.179001127017576, 48.777255836778998 ], [ 9.178841741982971, 48.777206159921221 ], [ 9.178675735555073, 48.777167017260076 ], [ 9.178504706466526, 48.777138785785802 ], [ 9.178330301820953, 48.777121737401508 ], [ 9.178154201230427, 48.777116036303902 ], [ 9.166994209422283, 48.777116036303902 ], [ 9.166818108831755, 48.777121737401508 ], [ 9.166643704186182, 48.777138785785802 ], [ 9.166472675097637, 48.777167017260076 ], [ 9.166306668669737, 48.777206159921221 ], [ 9.166147283635134, 48.777255836778998 ], [ 9.165996054958837, 48.77731556938766 ], [ 9.16585443905567, 48.777384782454767 ], [ 9.165723799764194, 48.777462809382932 ], [ 9.165605395212204, 48.777548898690974 ], [ 9.165500365700257, 48.777642221252542 ], [ 9.165409722719977, 48.777741878282676 ], [ 9.165334339212803, 48.777846909994913 ], [ 9.165274941163126, 48.777956304846001 ], [ 9.165232100606632, 48.77806900927871 ], [ 9.165206230121299, 48.778183937869152 ], [ 9.165197578854045, 48.778299983780748 ], [ 9.165197578854045, 48.782908157990647 ], [ 9.165206230121299, 48.783024192980392 ], [ 9.165232100606632, 48.783139110225356 ], [ 9.165274941163126, 48.783251803021074 ], [ 9.165334339212803, 48.783361186092904 ], [ 9.165409722719977, 48.783466206047081 ], [ 9.165500365700257, 48.783565851514489 ], [ 9.165605395212204, 48.783659162889563 ], [ 9.165723799764194, 48.783745241570486 ], [ 9.16585443905567, 48.783823258611697 ], [ 9.165996054958837, 48.783892462705673 ], [ 9.166147283635134, 48.783952187416872 ], [ 9.166306668669737, 48.784001857598447 ], [ 9.166472675097637, 48.784040994929839 ], [ 9.166643704186182, 48.78406922252217 ], [ 9.166818108831755, 48.784086268546858 ], [ 9.166994209422283, 48.784091968852827 ] ] ] } }
]
}
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