import pandas as pd
import numpy as np
import math
import geopandas
from geopandas import GeoSeries
from geopandas import GeoDataFrame
from shapely.geometry import Point
import time
import datetime
import glob
import gc
from sqlalchemy import create_engine
pd.set_option('display.max_columns', None)
    
def leer_todos_los_archivos():
    lista_archivos=glob.glob("archivos/*.csv")
    i=0 
    for archivo in lista_archivos:   
        print('**archivo '+ str(i+1)+'/'+str(len(lista_archivos))+': '+archivo)
        print('hora inicio lectura:')
        hora()     
        reader=pd.read_csv(archivo,
                           index_col=False,
                           header=None,
                           iterator=True,
                           chunksize=10000000,
                           names=['dia','hora','A','ID','C','D','E','X','Y','ciudad'],
                           usecols=['dia','hora','ID','X','Y','ciudad'])
        viajes(reader)
        print('hora termino lectura de archivo' + archivo + ':')
        hora()
        i=i+1
        gc.collect()

#Funcion que cuenta la cantidad de viajes de mas de 100 km para cada par de ciudades:
def viajes(reader):
    #Se inicializan las variables auxiliares: 
    data=pd.DataFrame()
    ultimo_valor=pd.DataFrame()
    i=1
    #Se opera para cada chunk:
    for chunk in reader:
        print('leyendo datos...')
        #Se crea una copia para operar sobre ella: 
        temp_chunk=chunk.copy()
        #Se eliminan los valores nulos en X e Y:
        temp_chunk.dropna(subset=['X', 'Y'],
                          axis=0,
                          how='any',
                          inplace=True)
        #Se reemplazan los valores NaN con ' ':
        temp_chunk.fillna(' ',
                          inplace=True)
        ### Prueba de eliminar horas fuera de intervalo buscado:
        ###
        #Se juntan las coordenadas X e Y en un unico valor (X,Y), asignandose a X:
        temp_chunk.X=tuple(map(tuple,np.column_stack((temp_chunk.X,temp_chunk.Y))))
        #Se renombran las coordenadas recien creadas en X:
        temp_chunk.rename(columns={'X':'coordenadas'},inplace=True)
        #Se elimina la coordenada Y:
        temp_chunk.drop('Y',inplace=True,axis=1)
        #Se agrega el ultimo valor anterior, para evitar perder viajes al dividir en chunks:
        temp_chunk=ultimo_valor.append(temp_chunk,ignore_index=True)
        #Se guarda el ultimo valor para evitar perder viajes al dividir en chunks:
        ultimo_valor=temp_chunk.tail(1).copy()  
        #Se juntan las horas de inicio y termino correspondientes a cada tramo de viaje:
        temp_chunk.hora=tuple(map(tuple,np.column_stack((temp_chunk.hora[:-1],
                                                          temp_chunk.hora[1:]))))+(np.nan,)
        #Se computan los tramos de viaje, creando una tupla ((X1,Y1),(X2,Y2)) y se asigna a la columna coordenadas:
        temp_chunk.coordenadas=tuple(map(tuple,np.column_stack((temp_chunk.coordenadas[:-1],
                                                          temp_chunk.coordenadas[1:]))))+(np.nan,)
        #Se computan las ciudades de viaje, creando una tupla (Ciudad1,Ciudad2) y se asigna a la columa ciudad:
        temp_chunk.ciudad=tuple(map(tuple,np.column_stack((temp_chunk.ciudad[:-1],
                                                           temp_chunk.ciudad[1:]))))+(np.nan,)
        #Se eliminan los valores nulos:
        temp_chunk.dropna(axis=0,how='any')
        #Se eliminan la ultima fila de cada ID, para no mezclar coordenadas de distintos usuarios:
        temp_chunk=temp_chunk.groupby('ID',
                                      as_index=False,
                                      sort=False).apply(lambda x: x.iloc[:-1])
        print('25%')
        temp_chunk=temp_chunk[temp_chunk.coordenadas.apply(haver)>0]
        temp_chunk['distancia']=temp_chunk.coordenadas.apply(haver)
        data=pd.concat([data,temp_chunk])
        data.reset_index(inplace=True,drop=True)
        print('40%')
        (origen_region,destino_region)=determinar_region(data)
        (origen_comuna,destino_comuna)=determinar_comuna(data)
        print('60%')
        #'region y comuna determinadas'
        bd_region=estructuracion_datos_region(origen_region,destino_region,data)
        bd_comuna=estructuracion_datos_comuna(origen_comuna,destino_comuna,data)
        print('80%')
        #'datos estructurados'
        agregar_region_a_bd(bd_region)
        agregar_comuna_a_bd(bd_comuna)
        print('100%')
        #'agregado a base de datos'
        temp_chunk=None
        data=None
        bd_region=None
        bd_comuna=None; origen_region=None; origen_comuna=None; destino_region=None; destino_comuna=None
        print('Se han leido en total '+str(i*10000000)+(' de datos'))
        i=i+1      
        gc.collect()
        #'agregado a bd'
        #####

#Funcion de distancia:
def haver(viaje):
    #Se asigna las variables de la tupla
    lon1=viaje[0][1]
    lat1=viaje[0][0]
    lon2=viaje[1][1]
    lat2=viaje[1][0]
    #Se Convierte a radianes
    lon1, lat1, lon2, lat2 = map(math.radians, [lon1, lat1, lon2, lat2])
    # Se aplica la formula de haversine
    dlon = (lon2 - lon1)
    dlat = (lat2 - lat1 )
    a = math.sin(dlat/2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon/2)**2
    c = 2 * math.asin(math.sqrt(a)) 
    r = 6372.8 # Radio de la tierra en kilometros
    return c * r
    
##Determinacion de region de cada punto:
def determinar_region(datos): 
    regiones_shp=geopandas.read_file('resources/Capas_shp/regiones/regiones.shp')
    regiones_shp['geometry']=regiones_shp.geometry.apply(swap_xy)
    regiones_shp['centroide']=GeoSeries(regiones_shp['geometry'].centroid)
    
    coordenadas=GeoDataFrame()
    coordenadas.crs = {'init' :'epsg:4326'}
    coordenadas[['origen','destino']]=pd.DataFrame(datos['coordenadas'].values.tolist())
    
    origen=GeoDataFrame()
    origen.crs={'init' :'epsg:4326'}
    origen['geometry']=coordenadas['origen'].transform(lambda x: Point(x))
    origen=geopandas.sjoin(origen, regiones_shp,   op='within',how='left')
   
    destino=GeoDataFrame()
    destino.crs={'init' :'epsg:4326'}
    destino['geometry']=coordenadas['destino'].transform(lambda x: Point(x))
    destino=geopandas.sjoin(destino, regiones_shp,   op='within',how='left')

    NA_en_origen=origen[origen.isnull().any(1)].index
    NA_en_destino=destino[destino.isnull().any(1)].index
    destino=destino.drop(NA_en_origen)
    origen=origen.drop(NA_en_destino)
    destino.dropna(axis=0,how='any',inplace=True)
    origen.dropna(axis=0,how='any',inplace=True)
    
    return(origen,destino)

##Determinacion de comuna de cada punto:
def determinar_comuna(datos):
    comunas_shp=geopandas.read_file('resources/Capas_shp/comunas/comunas.shp',encoding="utf-8")
    comunas_shp['geometry']=comunas_shp.geometry.apply(swap_xy)
    comunas_shp=comunas_shp.loc[:,['NOM_COM','geometry']]
    
    coordenadas=GeoDataFrame()
    coordenadas.crs = {'init' :'epsg:4326'}
    coordenadas[['origen','destino']]=pd.DataFrame(datos['coordenadas'].values.tolist())
    
    origen=GeoDataFrame()
    origen.crs={'init' :'epsg:4326'}
    origen['geometry']=coordenadas['origen'].transform(lambda x: Point(x))
    origen=geopandas.sjoin(origen, comunas_shp,   op='within',how='left')
   

    destino=GeoDataFrame()
    destino.crs={'init' :'epsg:4326'}
    destino['geometry']=coordenadas['destino'].transform(lambda x: Point(x))
    destino=geopandas.sjoin(destino, comunas_shp,   op='within',how='left')

    NA_en_origen=origen[origen.isnull().any(1)].index
    NA_en_destino=destino[destino.isnull().any(1)].index
    destino=destino.drop(NA_en_origen)
    origen=origen.drop(NA_en_destino)
    destino.dropna(axis=0,how='any',inplace=True)
    origen.dropna(axis=0,how='any',inplace=True)
    
    origen=origen.NOM_COM.rename('origen')
    destino=destino.NOM_COM.rename('destino')
    comunas_origen_destino=pd.DataFrame()
    comunas_origen_destino['origen']=origen
    comunas_origen_destino['destino']=destino
    
    return(origen,destino)
    
##Estructura de los datos:

def estructuracion_datos_region(origen,destino,datos):
    #fecha y hora de salida y llegada
    fecha_origen=pd.to_datetime(datos.dia, format='%y%m%d').astype(str)
    #bloque horario de salida, corresponde al limite INFERIOR (por limitaciones de datetime) de cada bloque de 1 hora, no inclusivo.
    #por ejemplo, 15:00:00, contempla los viajes iniciados entre las 15:00:00 y las 15:59:59
    bloque_horario_origen=pd.to_datetime((((((pd.Series([x[0]for x in datos.hora])-1)/10000).astype(int)*10000)).astype(str).str.pad(6,side='left',fillchar='0')), format='%H%M%S').dt.time.astype(str)
    bloque_horario_origen=bloque_horario_origen.str.replace(':','-')
    #region_origen=origen.COD_REGI
    #region_destino=destino.COD_REGI
    region_origen=pd.to_numeric(origen.COD_REGI,downcast='signed')
    region_destino=pd.to_numeric(destino.COD_REGI,downcast='signed')
    
    bloque_distancia=((datos.distancia+1)/5).astype(int)*5+5
    base_datos=pd.DataFrame({'fecha':fecha_origen,'hora':bloque_horario_origen, 'region_origen':region_origen, 'region_destino':region_destino,'distancia':bloque_distancia, 'cantidad':0})
    base_datos=base_datos.groupby(['fecha','hora','region_origen','region_destino','distancia'],as_index=False,sort=False).count()
    base_datos=base_datos.sort_values(by=['fecha','hora'])
    base_datos.reset_index(inplace=True,drop=True)
    return(base_datos)
    
    
def estructuracion_datos_comuna(origen,destino,datos):
    #fecha y hora de salida y llegada
    fecha_origen=pd.to_datetime(datos.dia, format='%y%m%d').astype(str)
    #bloque horario de salida, corresponde al limite INFERIOR (por limitaciones de datetime) de cada bloque de 1 hora, no inclusivo.
    #por ejemplo, 15:00:00, contempla los viajes iniciados entre las 15:00:00 y las 15:59:59
    bloque_horario_origen=pd.to_datetime((((((pd.Series([x[0]for x in datos.hora])-1)/10000).astype(int)*10000)).astype(str).str.pad(6,side='left',fillchar='0')), format='%H%M%S').dt.time.astype(str)
    bloque_horario_origen=bloque_horario_origen.str.replace(':','-')
    comuna_origen=origen
    comuna_destino=destino

    bloque_distancia=((datos.distancia+1)/5).astype(int)*5+5    
    base_datos=pd.DataFrame({'fecha':fecha_origen,'hora':bloque_horario_origen, 'comuna_origen':comuna_origen, 'comuna_destino':comuna_destino, 'cantidad':0 , 'distancia':bloque_distancia})
    base_datos=base_datos.groupby(['fecha','hora','comuna_origen','comuna_destino','distancia'],as_index=False,sort=False).count()
    base_datos=base_datos.sort_values(by=['fecha','hora'])
    base_datos.reset_index(inplace=True,drop=True)
    return(base_datos)
    
    
## construccion base de datos ##
def agregar_region_a_bd(base_datos):
    engine = create_engine('sqlite:///resources/viajes.sqlite')
    base_datos.to_sql('regiones',con=engine,index=False, if_exists='append')
    
def agregar_comuna_a_bd(base_datos):
    engine = create_engine('sqlite:///resources/viajes.sqlite')
    base_datos.to_sql('comunas',con=engine,index=False, if_exists='append')

def hora():
    st = datetime.datetime.fromtimestamp(time.time()).strftime('%Y-%m-%d %H:%M:%S')
    print (st)

## Intercambiar x por y para mapa ####
def swap_xy(geom):
    if geom.is_empty:
        return geom
    ndim = 3 if geom.has_z else 2

    def swap_xy_coords(coords):
        if ndim == 2:
            for x, y in coords:
                yield (y, x)
        elif ndim == 3:
            for x, y, z in coords:
                yield (y, x, z)
    # Procesa coordenadas para cada geometria
    if geom.type in ('Point', 'LineString', 'LinearRing'):
        return type(geom)(list(swap_xy_coords(geom.coords)))
    elif geom.type == 'Polygon':
        ring = geom.exterior
        shell = type(ring)(list(swap_xy_coords(ring.coords)))
        holes = list(geom.interiors)
        for pos, ring in enumerate(holes):
            holes[pos] = type(ring)(list(swap_xy_coords(ring.coords)))
        return type(geom)(shell, holes)
    elif geom.type.startswith('Multi') or geom.type == 'GeometryCollection':
        # Recursive call
        return type(geom)([swap_xy(part) for part in geom.geoms])
    else:
        raise ValueError('Type %r not recognized' % geom.type)

###
        
if __name__ == '__main__':
    leer_todos_los_archivos()


