JSON / app.py
UjjwalKGupta's picture
update KML
1dfb603 verified
raw
history blame
5.66 kB
import gradio as gr
import geopandas as gpd
import requests
import io
import re
from fastapi import FastAPI
import kml2geojson # Using this library for robust KML parsing
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.
Args:
file_url: The URL of the KML or GeoJSON file.
Returns:
A GeoDataFrame containing the data from the file.
"""
# --- Handle different Google Drive URL formats ---
if "drive.google.com" in file_url:
# Use regex to extract the file ID robustly
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
# --- Download the file content ---
response = requests.get(download_url, timeout=30)
response.raise_for_status()
bytes_data = io.BytesIO(response.content)
# Decode for sniffing, handle potential encoding issues
try:
string_data = response.content.decode('utf-8')
except UnicodeDecodeError:
string_data = response.content.decode('latin-1')
# --- Determine file type and parse ---
# If it looks like XML, it's likely KML.
if string_data.strip().startswith("<?xml"):
# kml2geojson requires the file path or a stream of the file
bytes_data.seek(0) # Reset stream position
geojson_data = kml2geojson.convert(bytes_data)
# Extract features from the converted GeoJSON
# The output of kml2geojson is a list of geojson objects
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:
# Assume it's GeoJSON or another format GeoPandas can read directly
bytes_data.seek(0) # Reset stream position
input_gdf = gpd.read_file(bytes_data)
return input_gdf
def calculate_geometry(file_url: str) -> tuple[str, str]:
"""
Main function for the Gradio interface. It orchestrates the downloading,
parsing, validation, and calculation of area and perimeter.
Args:
file_url: The URL of the KML or GeoJSON file.
Returns:
A tuple containing the formatted area and perimeter strings.
"""
if not file_url or not file_url.strip():
return "Error: Please provide a URL.", ""
try:
# 1. Get GeoDataFrame from the URL
input_gdf = get_gdf_from_file_url(file_url)
if input_gdf.empty:
return "Error: Could not find any geometric features in the file.", ""
# 2. Find the first valid polygon in the GeoDataFrame
geometry_gdf = None
for i, row in input_gdf.iterrows():
if row.geometry.geom_type in ['Polygon', 'MultiPolygon']:
# Create a new GDF with just this valid geometry
geometry_gdf = gpd.GeoDataFrame([row], crs=input_gdf.crs)
break
if geometry_gdf is None:
return "Error: No valid Polygon or MultiPolygon geometry found in the file.", ""
# 3. Reproject for accurate measurement
# Ensure the initial CRS is set correctly (WGS84) before reprojecting
if geometry_gdf.crs is None or geometry_gdf.crs.to_epsg() != 4326:
geometry_gdf = geometry_gdf.set_crs(epsg=4326, allow_override=True)
# Determine the correct UTM zone for accurate measurements in meters
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)
# 4. Calculate area and perimeter
area_m2 = gdf_proj.geometry.iloc[0].area
perimeter_m = gdf_proj.geometry.iloc[0].length
# 5. Format output
area_km2 = area_m2 / 1_000_000
perimeter_km = perimeter_m / 1_000
area_str = f"{area_m2:,.2f} square meters ({area_km2:,.2f} km²)"
perimeter_str = f"{perimeter_m:,.2f} meters ({perimeter_km:,.2f} km)"
return area_str, perimeter_str
except Exception as e:
return f"An error occurred: {e}", ""
# Create the Gradio user interface
gradio_interface = gr.Interface(
fn=calculate_geometry,
inputs=gr.Textbox(
label="Google Drive KML/GeoJSON File URL",
placeholder="Paste the URL to your file here..."
),
outputs=[
gr.Textbox(label="Calculated Area", interactive=False),
gr.Textbox(label="Calculated Perimeter", interactive=False)
],
title="Polygon Area & Perimeter Calculator 🗺️",
description=(
"Enter the public Google Drive URL of a KML or GeoJSON file that contains a polygon. "
"The tool will find the first valid polygon and calculate its area and perimeter."
),
examples=[
["https://drive.google.com/file/d/123KCak3o1VUcrYQO6v-HxVPYDrYLw4Ft/view?usp=drivesdk"]
],
allow_flagging="never"
)
# Initialize the FastAPI app and mount the Gradio interface
app = FastAPI()
app = gr.mount_gradio_app(app, gradio_interface, path="/")