""" Load and process administrative boundaries for Cox's Bazar district. The geoBoundaries ADM4 file has a flat structure (shapeName only, no parent hierarchy columns). We use spatial filtering with the Cox's Bazar bounding box to extract the relevant unions. """ import geopandas as gpd import logging from shapely.geometry import box from src.config import ( ADMIN_BOUNDARIES_FILE, PROCESSED_UNIONS, PROCESSED_DIR, COX_BAZAR_BBOX ) logger = logging.getLogger(__name__) # Known Cox's Bazar upazila-level bounding boxes for spatial classification # These help assign upazila names to unions that lack hierarchy info UPAZILA_BOXES = { "Cox's Bazar Sadar": (21.35, 91.90, 21.60, 92.15), "Ramu": (21.25, 92.05, 21.55, 92.30), "Chakaria": (21.55, 91.90, 21.95, 92.20), "Kutubdia": (21.70, 91.80, 21.95, 91.92), "Maheshkhali": (21.50, 91.80, 21.80, 91.95), "Pekua": (21.75, 91.92, 21.95, 92.10), "Teknaf": (20.55, 92.15, 21.20, 92.40), "Ukhia": (21.10, 92.05, 21.35, 92.25), } def load_and_filter_boundaries(): """ Load admin boundaries GeoJSON, spatially filter to Cox's Bazar district, assign upazila names based on centroid location, and save processed output. Returns: GeoDataFrame of Cox's Bazar unions. """ logger.info("Loading administrative boundaries...") if not ADMIN_BOUNDARIES_FILE.exists(): raise FileNotFoundError( f"Admin boundaries file not found: {ADMIN_BOUNDARIES_FILE}\n" "Download from: https://data.humdata.org/dataset/" "geoboundaries-admin-boundaries-for-bangladesh" ) gdf = gpd.read_file(ADMIN_BOUNDARIES_FILE) logger.info(f"Loaded {len(gdf)} total administrative units") logger.info(f"Columns: {list(gdf.columns)}") # Ensure WGS84 if gdf.crs is None or gdf.crs.to_epsg() != 4326: gdf = gdf.to_crs(epsg=4326) # ── Spatial filter to Cox's Bazar bbox ──────────────────────────────── south, west, north, east = COX_BAZAR_BBOX cox_box = box(west, south, east, north) unions = gdf[gdf.geometry.intersects(cox_box)].copy() if len(unions) == 0: raise ValueError( f"No unions found within Cox's Bazar bbox {COX_BAZAR_BBOX}. " f"Dataset bounds: {gdf.total_bounds}" ) logger.info(f"Found {len(unions)} unions in Cox's Bazar bounding box") # ── Build standardized columns ──────────────────────────────────────── # Rename shapeName → union_name if "shapeName" in unions.columns: unions = unions.rename(columns={"shapeName": "union_name"}) else: # Fallback — use whatever name column exists for col in unions.columns: if "name" in col.lower() and col != "geometry": unions = unions.rename(columns={col: "union_name"}) break # Assign upazila names from centroid location centroids = unions.geometry.centroid upazila_names = [] for cent in centroids: assigned = "Cox's Bazar (unclassified)" for upazila, (s, w, n, e) in UPAZILA_BOXES.items(): if w <= cent.x <= e and s <= cent.y <= n: assigned = upazila break upazila_names.append(assigned) unions["upazila_name"] = upazila_names unions["district_name"] = "Cox's Bazar" # Keep useful columns keep = ["union_name", "upazila_name", "district_name", "geometry"] for col in ["shapeID", "shapeISO"]: if col in unions.columns: keep.append(col) unions = unions[[c for c in keep if c in unions.columns]].copy() # ── Save ────────────────────────────────────────────────────────────── PROCESSED_DIR.mkdir(parents=True, exist_ok=True) unions.to_file(PROCESSED_UNIONS, driver="GeoJSON") logger.info(f"Saved {len(unions)} unions → {PROCESSED_UNIONS}") # Summary ups = sorted(unions["upazila_name"].unique()) logger.info(f"Upazilas ({len(ups)}): {ups}") for up in ups: n = len(unions[unions["upazila_name"] == up]) logger.info(f" {up}: {n} unions") return unions if __name__ == "__main__": logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") load_and_filter_boundaries()