# ******************************************************
## Revision "$LastChangedDate: 2013-04-18 08:54:18 +0200 (Thu, 18 Apr 2013) $"
## Date "$LastChangedRevision: 341 $"
## Author "$LastChangedBy: arthurbeusen $"
## URL "$HeadURL: https://pbl.sliksvn.com/globalnutrients/generalcode/trunk/ascbitmap.py $"
# ******************************************************
'''
ascbitmap.py

A Python library containing functions to read and write bitmap files.

Initialy created on 26 mei 2009
@author: warrinka
'''
try:
    from PIL import Image
    PILavailable = True
except ImportError:
    PILavailable = False

import os
import math

if PILavailable:
    class Asciibitmap:
        '''
        Creates a graphic representation of an asciigrid.
        '''
        def __init__(self,mode=None):
            self.nrows = None
            self.ncols = None
            self.values = None
            self.nodata = None
            # Set which colorband (gray, RGB or CMYK) is used.
            if (mode == None):
                self.mode = "L"
            else:
                self.mode = mode.upper()
            self.nodatacolor = None

        def createbitmap(self, nrows, ncols, values, filename, nodata = None, minV = None, maxV = None,\
                         color=None, number_of_classes = None, classtype = None, nodatacolor=None):
            '''
            Creates a bitmap-file with a graphic representation of the given values.
            @param nrows: Number of rows
            @type nrows: INTEGER
            @param ncols: Number of columns
            @type ncols: INTEGER
            @param values: List with values (INTEGER or FLOAT)
            @type values: LIST
            @param filename: Path including filename for output file
            @type filename: STRING
            @param nodata: (Optional) No_data value.
            @type nodata: FLOAT/INTEGER
            @param color: (Optional) Tuple with active colors.
            @type nodata: Tuple of three integers. (red, green, blue)
            @param number_of_classes: (Optional) Number of classes
            @type number_of_classes: INTEGER
            @param classtype: (Optional) Classtype specification
            @type classtype: Text with LOG for logaritmic distribution of the classes.
                             Text with filename, with on each line value and colorcode.
                             The value is max boundary (boundary included) for that color. 
                             (in case of RGB three integers otherwise one integer) 
            @nodatacolor: Color for the nodata
            @type nodatacolor: Tuple of three integers (in RBG mode) 
            '''
            lfixed_class = False
            self.nrows = nrows
            self.ncols = ncols
            self.values = values
            self.nodata = nodata
            self.nodatacolor = nodatacolor
            self.size = (self.ncols, self.nrows)
            self.img = Image.new(self.mode, self.size)
            if (maxV == None):
                self.maxV = max(filter(self.isno_nodata,self.values))
            else:
                self.maxV = maxV
            if (minV == None):
                self.minV = min(filter(self.isno_nodata,self.values))
            else:
                self.minV = minV
            
            if (classtype != None):
                if (classtype.upper() == "LOG"):
                    # Shift everything by one to avoid zero's.
                    self.minV = math.log(self.minV+1)
                    self.maxV = math.log(self.maxV+1)
                    classtype = classtype.upper()
                else:
                    # Classtype is a filename
                    # Number of classes is not used in this situation.
                    if (os.path.isfile(classtype)):
                        lfixed_class = True 
                        fp = open(classtype,"r")
                        lines = fp.readlines()
                        fp.close()
                        boundary = []
                        color_class = []
                        for item in xrange(len(lines)):
                            fields = lines[item].split()
                            if (len(fields) == 0):
                                continue
                            try:
                                if (self.mode == "L"):
                                    boundary.append(float(fields[0]))
                                    color_class.append(int(fields[1]))
                                elif (self.mode == "RGB"):
                                    boundary.append(float(fields[0]))
                                    color_class.append((int(fields[1]),int(fields[2]),int(fields[3])))
                            except IndexError:
                                print "***** ERROR *****"
                                print "File " + classtype + " is not correct."
                                print "Last line read: ",lines[item] 
                                raise IOError
                            
                    else:
                        print "***** ERROR *****"
                        print "File " + classtype + " is not found." 
                        raise IOError
                
            if (lfixed_class):
                for r in range(self.nrows):
                    for c in range(self.ncols):
                        val_index = (r*self.ncols)+c
                        if self.values[val_index] == nodata or self.values[val_index] == None:
                            if (self.nodatacolor != None):
                                value = self.nodatacolor
                            else:
                                if (self.mode == "L"):
                                    value = 0
                                elif (self.mode == "RGB"):
                                    value = (0,0,0)
                        else:
                            for iclass in  range(len(boundary)):
                                if (self.values[val_index] <= boundary[iclass]):
                                    value = color_class[iclass]
                                    break
                            else:
                                # Value is bigger than the last given number.
                                if (self.mode == "L"):
                                    value = 255
                                elif (self.mode == "RGB"):
                                    value = (255,255,255)

                        self.img.putpixel((c,r), value)           
               
            elif (self.mode == "L"):
                for r in range(self.nrows):
                    for c in range(self.ncols):
                        val_index = (r*self.ncols)+c
                        if self.values[val_index] == nodata or self.values[val_index] == None:
                            if (self.nodatacolor != None):
                                value = self.nodatacolor
                            else:
                                value = 0
                        else:
                            if (classtype == "LOG"):
                                val = math.log(self.values[val_index]+1)

                            else:
                                val = self.values[val_index]
                            value = self._get_value_255(self.maxV, self.minV, val,\
                                                        number_of_classes = number_of_classes)

                        self.img.putpixel((c,r), value)
            elif (self.mode == "RGB"):
                if (int(color[0]) == 1):
                    red = 1
                else:
                    red = None
                if (int(color[1]) == 1):
                    green = 1
                else:
                    green = None
                if (int(color[2]) == 1):
                    blue = 1
                else:
                    blue = None

                for r in range(self.nrows):
                    for c in range(self.ncols):
                        val_index = (r*self.ncols)+c
                        if self.values[val_index] == nodata or self.values[val_index] == None:
                            if (self.nodatacolor != None):
                                value = self.nodatacolor
                            else:
                                value = (0,0,0)
                        else: 
                            if (classtype == "LOG"):
                                val = math.log(self.values[val_index]+1)
                            else:
                                val = self.values[val_index]
                            value = self._get_value_RGB(self.maxV, self.minV, val,\
                                                        red=red,blue=blue,green=green,\
                                                        number_of_classes = number_of_classes)
                        self.img.putpixel((c,r), value)
            
            self._savebitmap(filename)


        def _savebitmap(self, filename):
            '''
            Saves the image under the given filename. The format is determined from the filename extension.
            @param filename: Path including filename for output file
            @type filename: STRING
            '''
            self.img.save(filename)

        def _get_value_RGB(self, maxV, minV, val,red=None,blue=None,green=None,\
                           number_of_classes = None):
            '''
            Scales a given value on a scale from 0 - 255
            @param maxV: Maximum value from values in data
            @type maxV: FLOAT/INTEGER
            @param minV: Minimum value from values in data
            @type minV: FLOAT/INTEGER
            @param val: Value 
            @type val: FLOAT/INTEGER
            @param red: a value. With a value, the red range is used.
            @type red: FLOAT/INTEGER
            @param blue: a value. With a value, the blue range is used. 
            @type blue: FLOAT/INTEGER
            @param green: a value. With a value, the green range is used. 
            @type green: FLOAT/INTEGER
            '''
            dif = maxV - minV
            if (number_of_classes == None):
                try:
                    step = 200.0/dif
                except ZeroDivisionError:
                    step = 1
            else:
                try:
                    step = float(number_of_classes)/dif
                except ZeroDivisionError:
                    step = 1

            if (red != None):
                if (blue != None):
                    if (green != None):
                        # Go from blue to green to red
                        if (val - minV > 0.5*dif):
                            value = (min(max(0,int((val-0.5*dif-minV)*0.5*step)),200)+55,\
                                    min(max(0,int(200 -(val-0.5*dif-minV)*0.5*step)),200)+55,\
                                    0)
                        else:
                            value = (0,\
                                    min(max(0,int((val-0.5*dif-minV)*0.5*step)),200)+55,\
                                    min(max(0,int(200 -(val-0.5*dif-minV)*0.5*step)),200)+55)
                    else:
                        # For blue to red
                        value = (self.interpol_up(step,minV,val),\
                                 0,\
                                 self.interpol_down(step,minV,val))
              
                else:
                    # It is red, not blue
                    if (green != None):
                       # For blue to red
                        value = (self.interpol_up(step,minV,val),\
                                 self.interpol_down(step,minV,val),\
                                 0)
                    else:
                       # Only red (from black to red).
                        value = (self.interpol_up(step,minV,val),\
                                 0,\
                                 0)
            else:
                # No red
                if (blue != None):
                    if (green != None):
                        # Go from blue to green
                        value = (0,\
                                 self.interpol_up(step,minV,val),\
                                 self.interpol_down(step,minV,val))
                    else:
                        # For blue (from black to blue)
                        value = (0,\
                                 0,\
                                 self.interpol_up(step,minV,val))
              
                else:
                    # It is not red, not blue
                    if (green != None):
                       # For green (from black to green)
                        value = (0,\
                                 self.interpol_up(step,minV,val),\
                                 0)
                    else:
                       # Gray from light to dark.
                        value = (self.interpol_up(step,minV,val),\
                                 self.interpol_up(step,minV,val),\
                                 self.interpol_up(step,minV,val))


            return value

        def interpol_up(self,step,minV,val):
            return min(max(0,int((val-minV)*step)),200) + 55

        def interpol_down(self,step,minV,val):
            return min(max(0,int(200-(val-minV)*step)),200) + 55
            
        def _get_value_255(self, maxV, minV, val, number_of_classes = None):
            '''
            Scales a given value on a scale from 0 - 255
            @param maxV: Maximum value from values in data
            @type maxV: FLOAT/INTEGER
            @param minV: Minimum value from values in data
            @type minV: FLOAT/INTEGER
            @param val: Value
            @type val: FLOAT/INTEGER
            '''
            dif = maxV - minV
            if (number_of_classes == None):
                try:
                    step = 200.0/dif
                except ZeroDivisionError:
                    step = 1
            else:
                try:
                    step = float(number_of_classes)/dif
                except ZeroDivisionError:
                    step = 1

            value = (val - minV)*step
            return min(max(0,int(value)),200) + 55
        
        def isno_nodata(self,val):
            return (val != self.nodata)
        
        
        def showbitmap(self):
            '''
            Saves the image to a temporary BMP file, and uses the standard BMP display utility to show it.
            '''
            self.img.show()
else: print "WARNING:\nPython Imaging Library (PIL) is not installed.\nAsciibitmap class will not be available."
