import gradio as gr import geopandas as gpd import requests import io import re from fastapi import FastAPI, HTTPException from urllib.parse import unquote import kml2geojson # All helper functions from the previous version remain the same. def get_gdf_from_file_url(file_url: str): """ Downloads a file from a URL (handling Google Drive links), determines if it's KML or GeoJSON, and reads it into a GeoDataFrame. """ if "drive.google.com" in file_url: match = re.search(r'/d/([a-zA-Z0-9_-]+)', file_url) or re.search(r'id=([a-zA-Z0-9_-]+)', file_url) if match: file_id = match.group(1) download_url = f'https://drive.google.com/uc?export=download&id={file_id}' else: raise ValueError("Could not extract file ID from Google Drive URL.") else: download_url = file_url response = requests.get(download_url, timeout=30) response.raise_for_status() bytes_data = io.BytesIO(response.content) try: string_data = response.content.decode('utf-8') except UnicodeDecodeError: string_data = response.content.decode('latin-1') if string_data.strip().startswith(" dict: """ Performs the core calculation and returns structured data (a dictionary). This is used by both the API and the Gradio UI. """ if not file_url or not file_url.strip(): raise ValueError("No URL provided.") input_gdf = get_gdf_from_file_url(file_url) if input_gdf.empty: raise ValueError("Could not find any geometric features in the file.") geometry_gdf = None for i, row in input_gdf.iterrows(): if row.geometry.geom_type in ['Polygon', 'MultiPolygon']: geometry_gdf = gpd.GeoDataFrame([row], crs=input_gdf.crs) break if geometry_gdf is None: raise ValueError("No valid Polygon or MultiPolygon geometry found in the file.") if geometry_gdf.crs is None or geometry_gdf.crs.to_epsg() != 4326: geometry_gdf = geometry_gdf.set_crs(epsg=4326, allow_override=True) centroid = geometry_gdf.geometry.iloc[0].centroid utm_zone = int((centroid.x + 180) / 6) + 1 epsg_code = 32600 + utm_zone if centroid.y >= 0 else 32700 + utm_zone gdf_proj = geometry_gdf.to_crs(epsg=epsg_code) area_m2 = gdf_proj.geometry.iloc[0].area perimeter_m = gdf_proj.geometry.iloc[0].length return { "area": {"value": area_m2, "unit": "square_meters"}, "perimeter": {"value": perimeter_m, "unit": "meters"}, "area_km2": {"value": area_m2 / 1_000_000, "unit": "square_kilometers"}, "perimeter_km": {"value": perimeter_m / 1_000, "unit": "kilometers"} } def calculate_geometry_for_ui(file_url: str) -> tuple[str, str]: """ Main calculation logic for the Gradio UI. Formats the data from calculate_geometry_data into human-readable strings. """ if not file_url or not file_url.strip(): return "Error: No URL provided.", "" try: data = calculate_geometry_data(file_url) area_str = f"{data['area']['value']:,.2f} sq meters ({data['area_km2']['value']:,.2f} km²)" perimeter_str = f"{data['perimeter']['value']:,.2f} meters ({data['perimeter_km']['value']:,.2f} km)" return area_str, perimeter_str except Exception as e: return f"An error occurred: {e}", "" # Initialize the FastAPI app first app = FastAPI() # --- NEW: Add a dedicated API endpoint --- @app.get("/api/geometry") def get_geometry_api(file_url: str): """ API endpoint to calculate geometry from a KML/GeoJSON file URL. Returns data in JSON format. """ if not file_url: raise HTTPException(status_code=400, detail="Missing 'file_url' query parameter.") try: # Decode the URL in case it's URL-encoded decoded_url = unquote(file_url) data = calculate_geometry_data(decoded_url) return data except Exception as e: raise HTTPException(status_code=500, detail=str(e)) # --- Gradio Interface using Blocks for more control --- with gr.Blocks() as demo: gr.Markdown( "# Polygon Area & Perimeter Calculator 🗺️\n" "Enter the public Google Drive URL of a KML or GeoJSON file, or load this page with " "`?file_url=` to process automatically." ) # ... (rest of the Gradio UI code is the same) with gr.Column(): url_input = gr.Textbox( label="Google Drive KML/GeoJSON File URL", placeholder="Paste URL here or load from page URL..." ) submit_button = gr.Button("Calculate", variant="primary") with gr.Row(): area_output = gr.Textbox(label="Calculated Area", interactive=False) perimeter_output = gr.Textbox(label="Calculated Perimeter", interactive=False) gr.Examples( examples=[ ["https://drive.google.com/file/d/123KCak3o1VUcrYQO6v-HxVPYDrYLw4Ft/view?usp=drivesdk"] ], inputs=url_input ) def process_url_on_load(request: gr.Request): file_url = request.query_params.get("file_url") if file_url: area, perimeter = calculate_geometry_for_ui(file_url) return { url_input: file_url, area_output: area, perimeter_output: perimeter } return { url_input: "", area_output: "", perimeter_output: "" } submit_button.click( fn=calculate_geometry_for_ui, inputs=url_input, outputs=[area_output, perimeter_output] ) demo.load( fn=process_url_on_load, inputs=None, outputs=[url_input, area_output, perimeter_output] ) # Mount the Gradio app onto the FastAPI app app = gr.mount_gradio_app(app, demo, path="/")