from fastapi import FastAPI, HTTPException, Query from fastapi.responses import Response, FileResponse, JSONResponse from fastapi.middleware.gzip import GZipMiddleware import geopandas as gpd import cbor2 from pathlib import Path import pygeohash as pgh from shapely.geometry import Point, LineString, MultiLineString from pyproj import Geod import json app = FastAPI() # ✅ Add Gzip compression middleware app.add_middleware(GZipMiddleware, minimum_size=500) # Directory containing Parquet files GEOHASH_FOLDER = Path("geohash") @app.get("/get_geodata/") async def get_geodata(geohash: str, format: str = "cbor"): """Fetch geospatial data by Geohash and return in the requested format with HTTP compression.""" if len(geohash) < 4: raise HTTPException(status_code=400, detail="Geohash must be at least 4 characters long.") base_geohash = geohash[:4] parquet_file = GEOHASH_FOLDER / f"{base_geohash}.parquet" if not parquet_file.exists(): raise HTTPException(status_code=404, detail=f"No data found for geohash '{base_geohash}'.") if format == "parquet": return FileResponse(parquet_file, media_type="application/octet-stream", filename=f"{base_geohash}.parquet") gdf = gpd.read_parquet(parquet_file) if format == "geojson": geojson_data = gdf.to_json() return Response(content=geojson_data, media_type="application/geo+json", headers={"Content-Disposition": f"attachment; filename={base_geohash}.geojson"}) elif format == "cbor": geojson_dict = gdf.to_json() cbor_data = cbor2.dumps(geojson_dict) return Response(content=cbor_data, media_type="application/cbor", headers={"Content-Disposition": f"attachment; filename={base_geohash}.cbor"}) raise HTTPException(status_code=400, detail="Invalid format. Use 'parquet', 'geojson', or 'cbor'.") @app.get("/lookup/") async def lookup( latitude: float = Query(..., description="Latitude of the point"), longitude: float = Query(..., description="Longitude of the point"), distance: float = Query(10, description="Distance threshold in meters for nearby lines") ): """ Finds polygons containing the given point and lines within a threshold (in meters). """ # Compute geohash (precision can be adjusted) geohash = pgh.encode(latitude, longitude, precision=4) # Adjust precision as needed parquet_file = GEOHASH_FOLDER / f"{geohash}.parquet" if not parquet_file.exists(): raise HTTPException(status_code=404, detail=f"No data found for geohash '{geohash}'.") try: # Load the Parquet file into a GeoDataFrame gdf = gpd.read_parquet(parquet_file) except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to read Parquet file: {str(e)}") point = Point(longitude, latitude) geod = Geod(ellps="WGS84") # Accurate geodetic distance calculations found_geometries = [] # Process each row for _, row in gdf.iterrows(): geom = row.geometry # Directly use the Shapely geometry object metadata = {} if isinstance(row.get("tags"), dict): metadata = {k: v for k, v in row["tags"].items() if v is not None} # Remove empty values if geom.contains(point): found_geometries.append({"type": "polygon", **metadata}) elif isinstance(geom, (LineString, MultiLineString)): try: if isinstance(geom, LineString): min_distance = min(geod.inv(point.x, point.y, p[0], p[1])[2] for p in geom.coords) elif isinstance(geom, MultiLineString): min_distance = min( min(geod.inv(point.x, point.y, p[0], p[1])[2] for p in line.coords) for line in geom.geoms ) if min_distance <= distance: found_geometries.append({"type": "line" if isinstance(geom, LineString) else "multi_line", **metadata}) except Exception as e: raise HTTPException(status_code=500, detail=f"Failed distance calculation: {str(e)}") # Return JSON result return JSONResponse(content=found_geometries)