import gradio as gr import geopandas as gpd import pandas as pd import os import matplotlib.pyplot as plt from arcgis.features import FeatureLayer from arcgis.geometry import Geometry from shapely.geometry import shape from shapely.ops import unary_union #from datasets import load_dataset #ds = load_dataset('psalama/NYC_sensitive_sites', data_files=data_files) def get_gdf_from_feature_layer(url): # Access the ArcGIS feature layer feature_layer = FeatureLayer(url) # Use the query() method to get all features where 'Borough' is 'MN' sdf = feature_layer.query(where="Borough='MN'", out_sr=4326, as_df=True) # Convert the 'SHAPE' column from ArcGIS's JSON-based format into a Shapely geometry sdf['geometry'] = sdf['SHAPE'].apply(lambda x: Geometry(x).as_shapely) # Convert the SpatialDataFrame to a GeoDataFrame gdf = gpd.GeoDataFrame(sdf, geometry='geometry') return gdf def process_buildings(input_gdf, sensitive_sites_gdf, default_building_height_m, multiplier_factor): # List to store all intersected sensitive sites intersected_sites = [] # List to store all buffers buffers = [] intersection_desc = "" # Iterate over each building in the input file for idx, building in input_gdf.iterrows(): building_name = building.get('building_name', 'Unnamed building') # If the 'building_height' field exists and its value is not null or zero for this building, # use it as the building height. Otherwise, use the default building height provided by the user. if 'building_height' in building and pd.notnull(building['building_height']) and building['building_height'] != 0: building_height_m = building['building_height'] * 0.3048 else: building_height_m = default_building_height_m buffer_distance_m = building_height_m * multiplier_factor # Convert building's geometry to EPSG:3857 for accurate meter-based distance measurement building_geometry = gpd.GeoSeries([building['geometry']], crs="EPSG:4326") building_geometry_m = building_geometry.to_crs("EPSG:3857") # Create a buffer around the building and convert it to a GeoDataFrame building_buffer = building_geometry_m.buffer(buffer_distance_m) building_buffer_gdf = gpd.GeoDataFrame(geometry=building_buffer, crs="EPSG:3857") building_buffer_gdf = building_buffer_gdf.to_crs("EPSG:4326") # Convert back to feet for storing and printing, rounding to the nearest foot building_height_ft = round(building_height_m / 0.3048) buffer_distance_ft = round(buffer_distance_m / 0.3048) # Assign additional attributes building_buffer_gdf['building_name'] = building_name building_buffer_gdf['building_height'] = building_height_ft building_buffer_gdf['buffer_distance'] = buffer_distance_ft buffers.append(building_buffer_gdf) # Check if the buffer intersects with any sensitive sites intersects = gpd.overlay(building_buffer_gdf, sensitive_sites_gdf, how='intersection') if not intersects.empty: building_intersect_desc = f"Building {idx} ({building_name}), height: {building_height_ft}, buffer distance: {buffer_distance_ft} is in the vicinity of a sensitive site." intersected_sites.append(intersects) else: building_intersect_desc = f"Building {idx} ({building_name}), height: {building_height_ft}, buffer distance: {buffer_distance_ft} is not in the vicinity of any sensitive sites." if intersection_desc == "": intersection_desc = building_intersect_desc else: intersection_desc += "\n" + building_intersect_desc return buffers, intersected_sites, intersection_desc def get_max_extent(*gdfs): # takes in unlimited number of gdfs and calculates max/min xy extents minx = min(gdf.total_bounds[0] for gdf in gdfs) miny = min(gdf.total_bounds[1] for gdf in gdfs) maxx = max(gdf.total_bounds[2] for gdf in gdfs) maxy = max(gdf.total_bounds[3] for gdf in gdfs) return minx, miny, maxx, maxy def create_plot(filename, extent, *gdfs): # takes in unlimited number of gdfs fig, ax = plt.subplots(figsize=(10, 8)) #Sets image size by width & height (in inches) colors = ['tan', 'mediumseagreen', 'thistle', 'lightcoral', 'sienna', 'yellow'] # Extend/improve this list as needed for idx, gdf in enumerate(gdfs): gdf.plot(ax=ax, color=colors[idx % len(colors)]) # Cycle through colors ax.set_xlim(extent[0], extent[2]) ax.set_ylim(extent[1], extent[3]) # Hide axes ax.axis('off') plt.savefig(filename, bbox_inches='tight', pad_inches=0) # remove padding def ss_intersect(geojson1, ss_geoselect, multiplier_factor, default_building_height): # Read the GeoJSON files input_gdf = gpd.read_file(geojson1.name) # Check that CRS is EPSG:4326 if input_gdf.crs.to_epsg() != 4326: raise ValueError("Input GeoJSON files must be in CRS EPSG:4326") if ss_geoselect==0: sensitive_sites_gdf = gpd.read_file("sensitive_sites/NYC_Parks_Properties.geojson") else: sensitive_sites_gdf = gpd.read_file("sensitive_sites/NYC_Parks_Zones.geojson") default_building_height_m = default_building_height * 0.3048 buffers, intersected_sites, intersection_desc = process_buildings(input_gdf, sensitive_sites_gdf, default_building_height_m, multiplier_factor) # Concatenate all buffer GeoDataFrames and save as a GeoJSON file buffers_gdf = pd.concat(buffers, ignore_index=True) buffers_gdf = buffers_gdf.to_crs("EPSG:4326") buffers_gdf.to_file("building_buffers.geojson", driver='GeoJSON') # Concatenate all intersected sensitive sites and save as a GeoJSON file if intersected_sites: intersected_sites_gdf = pd.concat(intersected_sites, ignore_index=True) intersected_sites_gdf = intersected_sites_gdf.to_crs("EPSG:4326") else: #if there aren't any intersections, return an empty geojson intersected_sites_gdf = gpd.read_file("files/No_intersecting_buildings.geojson") print("No buildings are in the vicinity of any sensitive sites.") intersected_sites_gdf.to_file("intersected_sensitive_sites.geojson", driver='GeoJSON') # Perform the union operation if there is more than one buffer if len(buffers) > 1: # Perform a unary union on the geometry column of the GeoDataFrame buffer_union = unary_union(buffers_gdf['geometry']) # Create a new GeoDataFrame from the union result buffer_union_gdf = gpd.GeoDataFrame(geometry=[buffer_union], crs="EPSG:4326") # Save the union GeoDataFrame as a GeoJSON file buffer_union_gdf.to_file("buffer_union.geojson", driver='GeoJSON') # Calculate the maximum extent extent = get_max_extent(input_gdf, buffers_gdf) lots_url = "https://services5.arcgis.com/GfwWNkhOj9bNBqoJ/arcgis/rest/services/MAPPLUTO/FeatureServer/0" # Access MapPLUTO # Eventually should be a checkbox lots_gdf = get_gdf_from_feature_layer(lots_url) # Create and save the plot - which is the output image create_plot('output_image.png', extent, lots_gdf, sensitive_sites_gdf, buffer_union_gdf, intersected_sites_gdf, input_gdf) # Return the image, geojson files, and text description return 'output_image.png', "building_buffers.geojson", "buffer_union.geojson", intersection_desc iface = gr.Interface( fn=ss_intersect, inputs=[ gr.inputs.File(label="Building Footprints GeoJSON"), gr.Radio(["Parks Properties", "Park Zones"], label="Which Sensitive Sites?", info="From NYC DPR", type="index"), gr.inputs.Slider(minimum=0.0, maximum=10.0, default=4.3, label="Building Height Multiplier"), gr.inputs.Number(default=200, label="Default Building Height"), #Can I make this optional? ], outputs=[ gr.outputs.Image(type="pil", label="Result Image"), gr.outputs.File(label="Building Buffers"), gr.outputs.File(label="Union of Building Buffers"), gr.outputs.Textbox(label="Building intersection descriptions"), ], examples=[ ["files/building4test.geojson", "Parks Properties", 4.3, 200], ["files/building4test.geojson", "Park Zones", 4.3, 900], ], title="Shadow Proximity", description="Upload proposed building footprints in a GeoJSON file and select a numeric value to get the building proximity prediction.", ) iface.launch()