###################################################################################################
# Repository: https://github.com/lgervasoni/urbansprawl
# MIT License
###################################################################################################

from scipy import spatial
import numpy as np
import pandas as pd
import time

from osmnx import log

##############################################################
### Dispersion indices methods
##############################################################

def closest_building_distance_median( point_ref, tree, df_closest_d, radius_search ):
	""" 
	Dispersion metric at point_ref
	Computes the median of the closest distance to another building for each building within a radius search
	Uses the input KDTree to accelerate calculations

	Parameters
	----------
	point_ref : shapely.Point
		calculate index at input point
	tree : scipy.spatial.KDTree
		KDTree of buildings centroid
	df : pandas.DataFrame 
		data frame of buildings with closest distance calculation
	radius_search : float
		circle radius to consider the dispersion calculation at a local point

	Returns
	----------
	float
		value of dispersion at input point
	"""
	# Query buildings within radius search
	indices = tree.query_ball_point( point_ref, radius_search )
	# No dispersion value
	if (len(indices) == 0): return np.NaN
	# Calculate median of closest distance values. If no information is available, NaN is set
	return df_closest_d.loc[ indices ].median()

def closest_building_distance_average( point_ref, tree, df_closest_d, radius_search ):
	""" 
	Dispersion metric at point_ref
	Computes the mean of the closest distance to another building for each building within a radius search
	Uses the input KDTree to accelerate calculations

	Parameters
	----------
	point_ref : shapely.Point
		calculate index at input point
	tree : scipy.spatial.KDTree
		KDTree of buildings centroid
	df : pandas.DataFrame 
		data frame of buildings with closest distance calculation
	radius_search : int
		circle radius to consider the dispersion calculation at a local point

	Returns
	----------
	float
		value of dispersion at input point
	"""
	# Query buildings within radius search
	indices = tree.query_ball_point( point_ref, radius_search )
	# No dispersion value
	if (len(indices) == 0): return np.NaN
	# Calculate mean of closest distance values. If no information is available, NaN is set
	return df_closest_d.loc[ indices ].mean()


##############################################################
### Dispersion indices calculation
##############################################################

def compute_grid_dispersion(df_indices, df_osm_built, kwargs={"radius_search":750, "use_median":True, "K_nearest":50} ):
	""" 
	Creates grid and calculates dispersion indices.

	Parameters
	----------
	df_indices : geopandas.GeoDataFrame
		data frame containing the (x,y) reference points to calculate indices
	df_osm_built : geopandas.GeoDataFrame
		data frame containing the building's geometries
	kw_args: dict
		additional keyword arguments for the indices calculation
			radius_search: int
				circle radius to consider the dispersion calculation at a local point
			use_median : bool
				denotes whether the median or mean should be used to calculate the indices
			K_nearest : int
				number of neighboring buildings to consider in evaluation

	Returns
	----------
	geopandas.GeoDataFrame
		data frame with the added column for dispersion indices
	"""
	log("Calculating dispersion indices")
	start = time.time()

	# Get radius search: circle radius to consider the dispersion calculation at a local point
	radius_search = kwargs["radius_search"]
	# Use the median or mean computation ?
	use_median = kwargs["use_median"]

	# Assign dispersion calculation method
	if (kwargs["use_median"]):
		_calculate_dispersion = closest_building_distance_median
	else:
		_calculate_dispersion = closest_building_distance_average
	
	# Calculate the closest distance for each building within K_nearest centroid buildings
	_apply_polygon_closest_distance_neighbor(df_osm_built, K_nearest = kwargs["K_nearest"] )
	
	# For dispersion calculation approximation, create KDTree with buildings centroid
	coords_data = [ point.coords[0] for point in df_osm_built.loc[ df_osm_built.closest_d.notnull() ].geometry.apply(lambda x: x.centroid) ]
	# Create KDTree
	tree = spatial.KDTree( coords_data )
	
	# Compute dispersion indices
	index_column = "dispersion"
	df_indices[index_column] = df_indices.geometry.apply(lambda x: _calculate_dispersion(x, tree, df_osm_built.closest_d, radius_search ) )
	
	# Remove added column
	df_osm_built.drop('closest_d', axis=1, inplace=True)

	log("Done: Dispersion indices. Elapsed time (H:M:S): " + time.strftime("%H:%M:%S", time.gmtime(time.time()-start)) )
	

def _apply_polygon_closest_distance_neighbor(df_osm_built, K_nearest = 50):
	""" 
	Computes for each polygon, the distance to the (approximated) nearest neighboring polygon
	Approximation is done using distance between centroids to K nearest neighboring polygons, then evaluating the real polygon distance
	A column `closest_d` is added in the data frame

	Parameters
	----------
	df_osm_built: geopandas.GeoDataFrame
		data frame containing the building's geometries
	K_nearest: int
		number of neighboring polygons to evaluate

	Returns
	----------

	"""
	def get_closest_indices(tree, x, K_nearest):
		# Query the closest buidings considering their centroid
		return tree.query( x.centroid.coords[0] , k=K_nearest+1)[1][1:]
	def compute_closest_distance(x, buildings):
		# Minimum distance of all distances between reference building 'x' and the other buildings
		return (buildings.apply(lambda b: x.distance(b) ) ).min()

	# Use all elements to get the exact closest neighbor?
	if ( (K_nearest == -1) or (K_nearest >= len(df_osm_built)) ): K_nearest = len(df_osm_built)-1

	# Get separate list for coordinates
	coords_data = [ geom.centroid.coords[0] for geom in df_osm_built.geometry ]
	# Create KD Tree using polygon's centroid
	tree = spatial.KDTree( coords_data )

	# Get the closest buildings indices
	df_osm_built['closest_buildings'] = df_osm_built.geometry.apply(lambda x: get_closest_indices(tree, x, K_nearest) )
	# Compute the minimum real distance for the closest buildings
	df_osm_built['closest_d'] = df_osm_built.apply(lambda x: compute_closest_distance(x.geometry,df_osm_built.geometry.loc[x.closest_buildings]), axis=1 )
	# Drop unnecessary column
	df_osm_built.drop('closest_buildings', axis=1, inplace=True)