Spaces:
Sleeping
Sleeping
| 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="/") | |