JSON / app.py
UjjwalKGupta's picture
Add API
3d0838a verified
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("<?xml"):
bytes_data.seek(0)
geojson_data = kml2geojson.convert(bytes_data)
all_features = []
for gj in geojson_data:
all_features.extend(gj.get('features', []))
if not all_features:
raise ValueError("KML file parsed, but no features were found.")
input_gdf = gpd.GeoDataFrame.from_features(all_features, crs="EPSG:4326")
else:
bytes_data.seek(0)
input_gdf = gpd.read_file(bytes_data)
return input_gdf
def calculate_geometry_data(file_url: str) -> 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=<your_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="/")