diff --git a/public/common/common.py b/public/common/common.py index 3d0a9af5..d7d92b48 100644 --- a/public/common/common.py +++ b/public/common/common.py @@ -2440,6 +2440,64 @@ def from_path(path: str): return obj + +def parse_date(date_str: str, is_end: bool = False): + """ + Parse a partial or full date string and return ISO8601 with UTC 'Z'. + Handles year-only, year-month, and full dates. + """ + from datetime import timedelta + import pandas as pd + + # Handle partial dates + if len(date_str) == 4: # Year only + date_str = f"{date_str}-12-31" if is_end else f"{date_str}-01-01" + elif len(date_str) == 7: # Year-month + y, m = date_str.split('-') + if is_end: + last_day = pd.Timestamp(f"{y}-{m}-01").days_in_month + date_str = f"{y}-{m}-{last_day:02d}" + else: + date_str = f"{y}-{m}-01" + + # Parse with pandas (handles most formats automatically) + dt = pd.to_datetime(date_str, errors='coerce', utc=True) + if pd.isna(dt): + raise ValueError(f"Could not parse date: {date_str}") + + # Set end_date-of-day if needed + if is_end and dt.hour == 0 and dt.minute == 0 and dt.second == 0: + dt = dt.replace(hour=23, minute=59, second=59) + + return dt + + +def split_datetime(start_date: str, end_date: str, n: int = 10, gap_milliseconds:int = 1000): + """ + Split start_date/end_date dates into n chunks, returning list of (start_iso, end_iso) strings. + """ + from datetime import timedelta + start_dt = parse_date(start_date, is_end=False) + end_dt = parse_date(end_date, is_end=True) + + total_seconds = (end_dt - start_dt).total_seconds() + chunk_seconds = total_seconds / n + + chunks = [] + for i in range(n): + chunk_start = start_dt + timedelta(seconds=i * chunk_seconds) + chunk_end = start_dt + timedelta(seconds=(i + 1) * chunk_seconds) + if i < n - 1: + chunk_end = chunk_end - timedelta(milliseconds=gap_milliseconds) + + chunks.append(( + chunk_start.strftime("%Y-%m-%dT%H:%M:%SZ"), + chunk_end.strftime("%Y-%m-%dT%H:%M:%SZ") + )) + + return chunks + + def split_gdf(gdf, n: int = None, zoom: int = None, clip: bool = False, return_type: str = "gdf"): import geopandas as gpd import pickle @@ -3669,7 +3727,7 @@ def estimate_zoom(bounds, target_num_tiles=1): def get_tiles( - bounds=None, target_num_tiles=1, zoom=None, max_tile_recursion=6, as_gdf=True, clip=False, verbose=False + bounds=None, target_num_tiles=1, zoom=None, max_tile_recursion=6, as_gdf=True, clip=False, add_bounds=False, verbose=False ): bounds = to_gdf(bounds) import mercantile @@ -3724,6 +3782,8 @@ def get_tiles( if verbose: print(f"Generated {len(gdf)} tiles at zoom level {zoom_level}") + if add_bounds: + gdf["bounds"] = gdf["geometry"].map(lambda x: list(x.bounds)) if clip: return gdf.clip(bounds) if as_gdf else gdf[["x", "y", "z"]].values else: