#!/usr/bin/env python
############################################################################
#
# MODULE:       v.isochrones
# AUTHOR:       Moritz Lennert
# PURPOSE:      Takes a map of roads and starting points and creates
#               isochrones around the starting points
#
# COPYRIGHT:    (c) 2014 Moritz Lennert, and the GRASS Development Team
#               This program is free software under the GNU General Public
#               License (>=v2). Read the file COPYING that comes with GRASS
#               for details.
#
#############################################################################

#%module
#% description: Creates isochrones from a road map and starting points
#% keyword: vector
#% keyword: network
#% keyword: isochrones
#%end
#%option G_OPT_V_MAP
#% label: Roads with speed attribute
#% required: yes
#%end
#%option G_OPT_V_FIELD
#% key: roads_layer
#% label: Layer number of the roads with relevant attributes
#% required: yes
#%end
#%option G_OPT_V_FIELD
#% key: node_layer
#% label: Layer number of the nodes on the network (for method=v.net.iso)
#% required: no
#% answer: 2
#% guisection: v.net.iso
#%end
#%option G_OPT_DB_COLUMN
#% key: cost_column
#% description: Name of speed attribute column (in km/h) or cost column (in minutes)
#% required: yes
#%end
#%option G_OPT_V_INPUT
#% key: start_points
#% label: Vector map with starting points for isochrones
#% required: yes
#%end
#%option G_OPT_V_OUTPUT
#% key: isochrones
#% label: Output vector map with isochrone polygons (Output prefix with flag -i)
#% required: yes
#%end
#%option
#% key: time_steps
#% type: double
#% description: Time steps of isochrones (in minutes)
#% multiple: yes
#% required: yes
#%end
#%option G_OPT_R_OUTPUT
#% key: timemap
#% label: Optional output raster map with continuous time from starting points
#% required: no
#% guisection: r.cost
#%end
#%option
#% key: offroad_speed
#% type: double
#% description: Speed for off-road areas (in km/h > 0)
#% required: no
#% answer: 5.0
#% guisection: r.cost
#%end
#%option
#% key: memory
#% description: Amount of memory (in MB) use
#% type: integer
#% required: no
#% answer: 300
#% guisection: r.cost
#%end
#%option
#% key: max_distance
#% type: double
#% description: Maximum distance (m) from network to include into isochrones
#% required: no
#% guisection: v.net.iso
#%end
#%option
#% key: method
#% description: Method to use for isochrone calculation
#% type: string
#% required: yes
#% options: v.net.iso,r.cost
#% answer: v.net.iso
#%end
#%flag
#% key: i
#% description: Create individual isochrone map for each starting point
#% guisection: v.net.iso
#%end



import os
import atexit
import math
import grass.script as grass

global isoraw
global isos_extract
global isos_extract_rast
global isos_grow_cat
global isos_grow_cat_int
global isos_grow_distance
global isos_grow_distance_recode
global isos_poly_all
global isos_final

isoraw = None
isos_extract = None
isos_extract_rast = None
isos_grow_cat = None
isos_grow_cat_int = None
isos_grow_distance = None
isos_grow_distance_recode = None
isos_poly_all = None
isos_final = None


def cleanup():

    if method == 'r.cost':
        # remove temporary cost column from road map
        if grass.vector_db(roads)[int(layer)]['driver'] == 'dbf':
            grass.run_command('v.db.dropcolumn',
                              layer=layer,
                              map=roads,
                              column=tmp_cost_column,
                              quiet=True)

        if grass.find_file(tmp_cost_map, element='cell')['name']:
            grass.run_command('g.remove', flags='f', type='raster', name=tmp_cost_map, quiet=True)
        if grass.find_file(tmp_time_map, element='cell')['name']:
            grass.run_command('g.remove', flags='f', type='raster', name=tmp_time_map, quiet=True)
        if grass.find_file(tmp_region_map, element='cell')['name']:
            grass.run_command('g.remove', flags='f', type='raster', name=tmp_region_map, quiet=True)

    elif method == 'v.net.iso':
        if grass.find_file(isoraw, element='vector')['name']:
            grass.run_command('g.remove', flags='f', type='vector', name=isoraw, quiet=True)
        if grass.find_file(isos_extract, element='vector')['name']:
            grass.run_command('g.remove', flags='f', type='vector', name=isos_extract, quiet=True)
        if grass.find_file(isos_extract_rast, element='cell')['name']:
            grass.run_command('g.remove', flags='f', type='raster', name=isos_extract_rast, quiet=True)
        if grass.find_file(isos_grow_cat, element='cell')['name']:
            grass.run_command('g.remove', flags='f', type='raster', name=isos_grow_cat, quiet=True)
        if grass.find_file(isos_grow_cat_int, element='cell')['name']:
            grass.run_command('g.remove', flags='f', type='raster',
                    name=isos_grow_cat_int, quiet=True)
        if grass.find_file(isos_grow_distance, element='cell')['name']:
            grass.run_command('g.remove', flags='f', type='raster', name=isos_grow_distance, quiet=True)
        if grass.find_file(isos_grow_distance_recode, element='cell')['name']:
            grass.run_command('g.remove', flags='f', type='raster', name=isos_grow_distance_recode, quiet=True)
        if grass.find_file(isos_poly_all, element='vector')['name']:
            grass.run_command('g.remove', flags='f', type='vector', name=isos_poly_all, quiet=True)
        if grass.find_file(isos_final, element='vector')['name']:
            grass.run_command('g.remove', flags='f', type='vector', name=isos_final, quiet=True)
            

def isocalc(isoraw):

    global isos_extract
    global isos_extract_rast
    global isos_grow_cat
    global isos_grow_cat_int
    global isos_grow_distance
    global isos_grow_distance_recode
    global isos_poly_all

    isos_extract = 'isos_extract_%d' % os.getpid()
    isos_extract_rast = 'isos_extract_rast_%d' % os.getpid()
    isos_grow_cat = 'isos_grow_cat_%d' % os.getpid()
    isos_grow_cat_int = 'isos_grow_cat_int_%d' % os.getpid()
    isos_grow_distance = 'isos_grow_distance_%d' % os.getpid()
    isos_grow_distance_recode = 'isos_grow_distance_recode_%d' % os.getpid()
    isos_poly_all = 'isos_poly_all_%d' % os.getpid()

    grass.use_temp_region()
    
    grass.run_command('v.extract', input_=isoraw,
            cat=output_cats[0:-1], output=isos_extract,
            overwrite=True)
    grass.run_command('g.region', vect=isos_extract,
            flags='a')
    grass.run_command('v.to.rast', input_=isos_extract,
            use='cat', output=isos_extract_rast, overwrite=True)
    grass.run_command('r.grow.distance', input=isos_extract_rast,
            value=isos_grow_cat, distance=isos_grow_distance,
            flags='m', overwrite=True)
    grass.mapcalc(isos_grow_cat_int + ' = int(' + isos_grow_cat + ')')
    if max_distance:
        recode_str = '0:%f:1\n%f:*:0' % (max_distance, max_distance)
        grass.write_command('r.recode', input_=isos_grow_distance,
            output=isos_grow_distance_recode, rules='-',
            stdin=recode_str, overwrite=True)
        grass.run_command('r.mask', raster=isos_grow_distance_recode,
            maskcats=1, overwrite=True)
    grass.run_command('r.to.vect', input_=isos_grow_cat_int,
            output=isos_poly_all, type_='area', flags='sv',
            overwrite=True)
    grass.run_command('v.extract', input_=isos_poly_all,
            output=isos_final, cats=output_cats[0:-1])
    if max_distance:
        grass.run_command('r.mask', flags='r')



def main():

    # Input for all methods
    global roads
    roads = options['map']
    global layer
    layer = options['roads_layer']
    node_layer = options['node_layer']
    cost_column = options['cost_column']
    start_points = options['start_points']
    time_steps = options['time_steps'].split(',')
    isochrones = options['isochrones']
    global method
    method = options['method']

    if method == 'r.cost':
        offroad_speed = float(options['offroad_speed'])
        if offroad_speed == 0:
            grass.message(_('Offroad speed has to be > 0. Set to 0.00001.'))
            offroad_speed = 0.00001

        memory = int(options['memory'])
        # Output
        if options['timemap']:
            timemap = options['timemap']
        else:
            timemap = None

        global tmp_cost_map
        global tmp_time_map
        global tmp_region_map
        global tmp_cost_column

        tmp_cost_map = 'cost_map_tmp_%d' % os.getpid()
        tmp_time_map = 'time_map_tmp_%d' % os.getpid()
        tmp_region_map = 'region_map_tmp_%d' % os.getpid()

        # get current resolution
        region = grass.region()
        resolution = math.sqrt(float(region['nsres']) * float(region['ewres']))

        if grass.vector_db(roads)[int(layer)]['driver'] == 'dbf':
            # add cost column to road vector
            tmp_cost_column = 'tmp%d' % os.getpid()
            def_cost_column = tmp_cost_column + ' DOUBLE PRECISION'
            grass.run_command('v.db.addcolumn',
                              map=roads,
                              layer=layer,
                              column=def_cost_column,
                              quiet=True)

            # calculate cost (in minutes) depending on speed
            # (resolution/(speed (in km/h) * 1000 / 60))
            query_value = "%s / (%s * 1000 / 60)" % (resolution, cost_column)
            grass.run_command('v.db.update',
                              map=roads,
                              column=tmp_cost_column,
                              qcolumn=query_value)
        else:
            tmp_cost_column = "%s / (%s * 1000 / 60)" % (resolution, cost_column)

        # transform to raster
        grass.run_command('v.to.rast',
                          input=roads,
                          output=tmp_cost_map,
                          use='attr',
                          attrcolumn=tmp_cost_column,
                          type='line',
                          memory=memory)

        # replace null values with cost for off-road areas
        # (resolution/(off-road speed * 1000 / 60))
        null_value = resolution / (offroad_speed * 1000 / 60)
        grass.run_command('r.null', map=tmp_cost_map, null=null_value)

        # limit the cumulated cost surface calculation to the max time distance
        # requested
        max_cost = time_steps[-1]

        # calculate time distance from starting points
        grass.run_command('r.cost',
                          input=tmp_cost_map,
                          start_points=start_points,
                          output=tmp_time_map,
                          max_cost=max_cost,
                          memory=memory)

        if timemap:
            grass.run_command('g.copy', raster=(tmp_time_map, timemap))
            grass.run_command('r.colors', map=timemap, color='grey', flags='ne')

        # recode time distance to time steps
        recode_rules = '0:%s:%s\n' % (time_steps[0], time_steps[0])
        for count in range(1, len(time_steps)):
            recode_rules += time_steps[count-1] + ':'
            recode_rules += time_steps[count] + ':'
            recode_rules += time_steps[count] + '\n'

        grass.write_command('r.recode',
                            input=tmp_time_map,
                            output=tmp_region_map,
                            rules='-',
                            stdin=recode_rules)

        # transform to vector areas
        grass.run_command('r.to.vect',
                          input=tmp_region_map,
                          output=isochrones,
                          type='area',
                          column='traveltime',
                          flags='s')

        # give the polygons a default color table
        grass.run_command('v.colors',
                          map=isochrones,
                          use='attr',
                          column='traveltime',
                          color='grey')

    elif method == 'v.net.iso':
        global max_distance
        if(options['max_distance']):
            max_distance = float(options['max_distance'])
        else:
            max_distance = None
        global output_cats
        output_cats = []
        for i in range(1,len(time_steps)+2):
            output_cats.append(i)
        startpoints=grass.read_command('v.distance', from_=start_points, 
                to=roads, to_type='point', to_layer=node_layer, 
                upload='cat', flags='p', quiet=True).split('\n')[1:-1]

        global isoraw
        isoraw = 'isoraw_temp_%d' % os.getpid()
        global isos_final
        isos_final = 'isos_final_%d' % os.getpid()

        if flags['i']:
            for point in startpoints:
                startpoint_cat = point.split('|')[0]
                startnode_cat = point.split('|')[1]
                grass.run_command('v.net.iso', input_=roads, output=isoraw,
                        center_cats=startnode_cat, costs=time_steps,
                        arc_column=cost_column, overwrite=True)
                isocalc(isoraw)
                outname = isochrones + '_' + startpoint_cat
                grass.run_command('g.rename', vect=isos_final+','+outname)
                # give the polygons a default color table
                grass.run_command('v.colors',
                                  map=outname,
                                  use='cat',
                                  color='grey')

        else:
            startnodes = []
            for point in startpoints:
                startnodes.append(point.split('|')[1])
            grass.run_command('v.net.iso', input_=roads, output=isoraw, 
                    center_cats=startnodes, costs=time_steps, 
                    arc_column=cost_column, overwrite=True)
            isocalc(isoraw)
            grass.run_command('g.rename', vect=isos_final+','+isochrones)
            # give the polygons a default color table
            grass.run_command('v.colors',
                              map=isochrones,
                              use='cat',
                              color='grey')

    else:
        grass.fatal(_("You need to chose at least one of the methods"))


if __name__ == "__main__":
    options, flags = grass.parser()
    atexit.register(cleanup)
    main()
