""" Utility functions for Google Earth Engine data extraction and processing. """ from datetime import datetime, timedelta import json import os import tempfile import time import ee import geopandas as gpd from shapely.geometry import Point import pandas as pd from indices import add_s2_indices, add_s1_indices from variables import s2_bands, s1_bands def check_inside_civ(lat: float, lon: float): """ Check if the given latitude and longitude are inside Côte d'Ivoire. """ civ = gpd.read_file("data/CIV_0.json") point = Point(lon, lat) return civ.contains(point).any() def initialize_ee(): """ Initialize Google Earth Engine """ sa_email = os.getenv("EE_SERVICE_ACCOUNT") sa_key_json = os.getenv("EE_SERVICE_KEY") try: # Hugging Face if sa_email and sa_key_json: key_path = os.path.join(tempfile.gettempdir(), "ee-key.json") with open(key_path, "w", encoding="utf-8") as f: f.write(sa_key_json) creds = ee.ServiceAccountCredentials(sa_email, key_path) ee.Initialize(creds) print(f"[INFO] GEE initialized with service account {sa_email}") return # Local local_key = "secrets/gcp-sa-key.json" if os.path.exists(local_key): with open(local_key, "r", encoding="utf-8") as f: key_data = json.load(f) creds = ee.ServiceAccountCredentials(key_data["client_email"], local_key) ee.Initialize(creds) print(f"[INFO] GEE initialized from {local_key}") return # Neither HF nor Local ee.Initialize() print("[INFO] GEE initialized") except Exception as e: raise RuntimeError(f"GEE initialization failed : {e}") from e def mask_s2_clouds(image): """ Mask clouds and cirrus in Sentinel-2 images. """ qa = image.select('QA60') # Bits 10 and 11 are clouds and cirrus, respectively. cloud_bit_mask = 1 << 10 cirrus_bit_mask = 1 << 11 # Both flags should be set to zero, indicating clear conditions. mask = ( qa.bitwiseAnd(cloud_bit_mask) .eq(0) .And(qa.bitwiseAnd(cirrus_bit_mask).eq(0)) ) masked = image.updateMask(mask).divide(10000) masked = masked.copyProperties( source=image, properties=[ "system:time_start", "system:time_end", "CLOUDY_PIXEL_PERCENTAGE", "SPACECRAFT_NAME" ] ) return masked def mask_edge(image): """ Mask pixels at the edge in Sentinel-1 images. """ edge = image.lt(-30.0) masked_image = image.mask().And(edge.Not()) return image.updateMask(masked_image) def days_since_utc(utc_iso: str) -> int: """ Compute the number of days since the image was taken. """ t_img = time.mktime(time.strptime(utc_iso[:19], "%Y-%m-%dT%H:%M:%S")) return int((time.time() - t_img) // 86400) def lonlat_to_utm_epsg(lon: float, lat: float) -> int: """ Convert longitude and latitude to UTM EPSG code. """ zone = int((lon + 180) // 6) + 1 if lat >= 0: return 32600 + zone # WGS84 UTM N return 32700 + zone # WGS84 UTM S def projected_xy(lon: float, lat: float): """ Convert lon/lat to projected coordinates (easting, northing). """ epsg = f"EPSG:{lonlat_to_utm_epsg(lon, lat)}" pt = ee.Geometry.Point([lon, lat]) proj = ee.Projection(epsg) xy = ee.List(pt.transform(proj, 1).coordinates()).getInfo() return float(xy[0]), float(xy[1]) def extract_from_gee(lat: float, lon: float, radius_m: int = 30): """ Extract data from GEE for given lat/lon. """ pt = ee.Geometry.Point([float(lon), float(lat)]) roi = pt.buffer(radius_m).bounds() end_date = datetime.now() start_date = end_date - timedelta(days=31) start = start_date.strftime('%Y-%m-%d') end = end_date.strftime('%Y-%m-%d') S2 = ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED") S1 = ee.ImageCollection("COPERNICUS/S1_GRD") DEM = ee.Image("USGS/SRTMGL1_003") TIME_KEY = "system:time_start" s2 = (S2.filterBounds(roi) .filterDate(start, end) .filter(ee.Filter.lt("CLOUDY_PIXEL_PERCENTAGE", 40)) .map(mask_s2_clouds) .select(s2_bands)) img_s2 = s2.sort(TIME_KEY, False).first() if img_s2 is None: return None, {"error": "No Sentinel-2 image available on ROI/time window."} cloud_cover = ee.Number(img_s2.get("CLOUDY_PIXEL_PERCENTAGE")).getInfo() acq_iso_s2 = ee.Date(img_s2.get(TIME_KEY)).format().getInfo() days_s2 = days_since_utc(acq_iso_s2) s1 = (S1.filterBounds(roi) .filterDate(start, end) .filter(ee.Filter.eq('instrumentMode', 'IW')) .filter(ee.Filter.listContains('transmitterReceiverPolarisation', 'VV')) .filter(ee.Filter.listContains('transmitterReceiverPolarisation', 'VH')) .map(mask_edge) .select(s1_bands)) img_s1 = s1.sort(TIME_KEY, False).first() acq_iso_s1 = ee.Date(img_s1.get(TIME_KEY)).format().getInfo() days_s1 = days_since_utc(acq_iso_s1) if days_s1 > days_s2: estimation_date = acq_iso_s2 else: estimation_date = acq_iso_s1 # DEM s1_proj = img_s1.projection() elevation = DEM.reproject(s1_proj).clip(roi) s1_s2_dem_image = ( img_s1 .addBands(img_s2) .addBands(elevation) ) # Convert the image to a dictionary image_dict = s1_s2_dem_image.reduceRegion( reducer=ee.Reducer.mean(), geometry=roi, scale=10, maxPixels=1e8 ).getInfo() if image_dict: init_dict = {k: image_dict.get(k) for k in (s2_bands + s1_bands + ['elevation'])} data = pd.DataFrame([init_dict]) data["lon"] = lon data["lat"] = lat easting, northing = projected_xy(lon, lat) data["latitude_proj"] = northing data["longitude_proj"] = easting data = add_s2_indices(data) data = add_s1_indices(data) ndvi_mean = data["NDVI"].values[0] if "NDVI" in data else None else: data = None ndvi_mean = None return { "X": data, "cloud": float(cloud_cover), "estimation_date": estimation_date.split("T")[0], "ndvi_mean": ndvi_mean }, None