#!/usr/bin/env python
#
#       plot.py
#       
#       Copyright 2008 Sven Festersen <sven@sven-festersen.de>
#       
#       This program is free software; you can redistribute it and/or modify
#       it under the terms of the GNU General Public License as published by
#       the Free Software Foundation; either version 2 of the License, or
#       (at your option) any later version.
#       
#       This program is distributed in the hope that it will be useful,
#       but WITHOUT ANY WARRANTY; without even the implied warranty of
#       MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#       GNU General Public License for more details.
#       
#       You should have received a copy of the GNU General Public License
#       along with this program; if not, write to the Free Software
#       Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#       MA 02110-1301, USA.

"""
Module Contents
===============
This is the main module. It contains the base classes for chart widgets.
 - class Chart: base class for all chart widgets.
 - class Background: background of a chart widget.
 - class Title: title of a chart.

Colors
------
All colors that pygtkChart uses are gtk.gdk.Colors as used by PyGTK.

Author: Sven Festersen (sven@sven-festersen.de)
"""
__docformat__ = "epytext"
import cairo
import gobject
import gtk
import os
import pango
import pangocairo
import pygtk

from pygtk_chart.chart_object import ChartObject
from pygtk_chart.basics import *
from pygtk_chart import label

COLOR_AUTO = 0
AREA_CIRCLE = 0
AREA_RECTANGLE = 1
CLICK_SENSITIVE_AREAS = []


def init_sensitive_areas():
    global CLICK_SENSITIVE_AREAS
    CLICK_SENSITIVE_AREAS = []
    
def add_sensitive_area(type, coords, data):
    global CLICK_SENSITIVE_AREAS
    CLICK_SENSITIVE_AREAS.append((type, coords, data))
    
def get_sensitive_areas(x, y):
    res = []
    global CLICK_SENSITIVE_AREAS
    for type, coords, data in CLICK_SENSITIVE_AREAS:
        if type == AREA_CIRCLE:
            ax, ay, radius = coords
            if (ax - x) ** 2 + (ay - y) ** 2 <= radius ** 2:
                res.append(data)
        elif type == AREA_RECTANGLE:
            ax, ay, width, height = coords
            if ax <= x <= ax + width and ay <= y <= ay + height:
                res.append(data)
    return res


class Chart(gtk.DrawingArea):
    """
    This is the base class for all chart widgets.
    
    Properties
    ==========
    The Chart class inherits properties from gtk.DrawingArea.
    Additional properties:
     - padding (the amount of free white space between the chart's
       content and its border in px, type: int in [0, 100].
       
    Signals
    =======
    The Chart class inherits signals from gtk.DrawingArea.
    """
    
    __gproperties__ = {"padding": (gobject.TYPE_INT, "padding",
                                    "The chart's padding.", 0, 100, 0,
                                    gobject.PARAM_READWRITE)}
    
    def __init__(self):
        gtk.DrawingArea.__init__(self)
        #private properties:
        self._padding = 0
        #objects needed for every chart:
        self.background = Background()
        self.background.connect("appearance-changed", self._cb_appearance_changed)
        self.title = Title()
        self.title.connect("appearance-changed", self._cb_appearance_changed)
        
        self.add_events(gtk.gdk.BUTTON_PRESS_MASK|gtk.gdk.SCROLL_MASK|gtk.gdk.POINTER_MOTION_MASK)
        self.connect("expose_event", self._cb_expose_event)
        self.connect("button_press_event", self._cb_button_pressed)
        self.connect("motion-notify-event", self._cb_motion_notify)
        
    def do_get_property(self, property):
        if property.name == "padding":
            return self._padding
        else:
            raise AttributeError, "Property %s does not exist." % property.name

    def do_set_property(self, property, value):
        if property.name == "padding":
            self._padding = value
        else:
            raise AttributeError, "Property %s does not exist." % property.name
        
    def _cb_appearance_changed(self, object):
        """
        This method is called after the appearance of an object changed
        and forces a redraw.
        """
        self.queue_draw()
        
    def _cb_button_pressed(self, widget, event):
        pass
    
    def _cb_motion_notify(self, widget, event):
        pass
        
    def _cb_expose_event(self, widget, event):
        """
        This method is called when an instance of Chart receives
        the gtk expose_event.
        
        @type widget: gtk.Widget
        @param widget: The widget that received the event.
        @type event: gtk.Event
        @param event: The event.
        """
        self.context = widget.window.cairo_create()
        self.context.rectangle(event.area.x, event.area.y, \
                                event.area.width, event.area.height)
        self.context.clip()
        self.draw(self.context)
        return False
        
    def draw_basics(self, context, rect):
        """
        Draw basic things that every plot has (background, title, ...).
        
        @type context: cairo.Context
        @param context: The context to draw on.
        @type rect: gtk.gdk.Rectangle
        @param rect: A rectangle representing the charts area.
        """
        self.background.draw(context, rect)
        self.title.draw(context, rect)
        
        #calculate the rectangle that's available for drawing the chart
        title_height = self.title.get_real_dimensions()[1]  ## This may be why it's cutting off the bottom of the chart
        title_height = 0

        rect_height = int(rect.height - 3 * self._padding - title_height)
        rect_width = int(rect.width - 2 * self._padding)
        rect_x = int(rect.x + self._padding)
        rect_y = int(rect.y + title_height + 2 * self._padding)
        return gtk.gdk.Rectangle(rect_x, rect_y, rect_width, rect_height)
        
    def draw(self, context):
        """
        Draw the widget. This method is called automatically. Don't call it
        yourself. If you want to force a redrawing of the widget, call
        the queue_draw() method.
        
        @type context: cairo.Context
        @param context: The context to draw on.
        """
        rect = self.get_allocation()
        rect = gtk.gdk.Rectangle(0, 0, rect.width, rect.height) #transform rect to context coordinates
        context.set_line_width(1)
        rect = self.draw_basics(context, rect)
        
    def export_svg(self, filename, size=None):
        """
        Saves the contents of the widget to svg file. The size of the image
        will be the size of the widget.
        
        @type filename: string
        @param filename: The path to the file where you want the chart to be saved.
        @type size: tuple
        @param size: Optional parameter to give the desired height and width of the image.
        """
        if size is None:
            rect = self.get_allocation()
            width = rect.width
            height = rect.height
        else:
            width, height = size
            old_alloc = self.get_allocation
            self.get_allocation = lambda: gtk.gdk.Rectangle(0, 0, width, height)
        surface = cairo.SVGSurface(filename, width, height)
        ctx = cairo.Context(surface)
        context = pangocairo.CairoContext(ctx)
        self.draw(context)
        surface.finish()
        if size is not None:
            self.get_allocation = old_alloc
        
    def export_png(self, filename, size=None):
        """
        Saves the contents of the widget to png file. The size of the image
        will be the size of the widget.
        
        @type filename: string
        @param filename: The path to the file where you want the chart to be saved.
        @type size: tuple
        @param size: Optional parameter to give the desired height and width of the image.
        """
        if size is None:
            rect = self.get_allocation()
            width = rect.width
            height = rect.height
        else:
            width, height = size
            old_alloc = self.get_allocation
            self.get_allocation = lambda: gtk.gdk.Rectangle(0, 0, width, height)
        surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
        ctx = cairo.Context(surface)
        context = pangocairo.CairoContext(ctx)
        self.set_size_request(width, height)
        self.draw(context)
        surface.write_to_png(filename)
        if size is not None:
            self.get_allocation = old_alloc
        
        
    def set_padding(self, padding):
        """
        Set the chart's padding.
        
        @param padding: the padding in px
        @type padding: int in [0, 100] (default: 16).
        """
        self.set_property("padding", padding)
        self.queue_draw()
        
    def get_padding(self):
        """
        Returns the chart's padding.
        
        @return: int in [0, 100].
        """
        return self.get_property("padding")
    
        
class Background(ChartObject):
    """
    The background of a chart.
    
    Properties
    ==========
    This class inherits properties from chart_object.ChartObject.
    Additional properties:
     - color (the background color, type: gtk.gdk.Color)
     - gradient (the background gradient, type: a pair of gtk.gdk.Color)
     - image (path to the background image file, type: string)
     
    Signals
    =======
    The Background class inherits signals from chart_object.ChartObject.
    """    
    
    __gproperties__ = {"color": (gobject.TYPE_PYOBJECT,
                                    "background color",
                                    "The color of the backround.",
                                    gobject.PARAM_READWRITE),
                        "gradient": (gobject.TYPE_PYOBJECT,
                                    "background gradient",
                                    "A background gardient. (first_color, second_color)",
                                    gobject.PARAM_READWRITE),
                        "image": (gobject.TYPE_STRING,
                                    "background image file",
                                    "Path to the image file to use as background.",
                                    "", gobject.PARAM_READWRITE)}
    
    def __init__(self):
        ChartObject.__init__(self)
        self._color = gtk.gdk.color_parse("#ffffff") #the backgound is filled white by default
        self._gradient = None
        self._image = ""
        self._pixbuf = None
        
    def do_get_property(self, property):
        if property.name == "visible":
            return self._show
        elif property.name == "antialias":
            return self._antialias
        elif property.name == "gradient":
            return self._gradient
        elif property.name == "color":
            return self._color
        elif property.name == "image":
            return self._image
        else:
            raise AttributeError, "Property %s does not exist." % property.name

    def do_set_property(self, property, value):
        if property.name == "visible":
            self._show = value
        elif property.name == "antialias":
            self._antialias = value
        elif property.name == "gradient":
            self._gradient = value
        elif property.name == "color":
            self._color = value
        elif property.name == "image":
            self._image = value
        else:
            raise AttributeError, "Property %s does not exist." % property.name
        
    def _do_draw(self, context, rect):
        """
        Do all the drawing stuff.
        
        @type context: cairo.Context
        @param context: The context to draw on.
        @type rect: gtk.gdk.Rectangle
        @param rect: A rectangle representing the charts area.
        """
        if self._color != None:
            #set source color
            context.set_source_rgb(*color_gdk_to_cairo(self._color))
        elif self._gradient != None:
            #set source gradient
            cs = color_gdk_to_cairo(self._gradient[0])
            ce = color_gdk_to_cairo(self._gradient[1])
            gradient = cairo.LinearGradient(0, 0, 0, rect.height)
            gradient.add_color_stop_rgb(0, cs[0], cs[1], cs[2])
            gradient.add_color_stop_rgb(1, ce[0], ce[1], ce[2])
            context.set_source(gradient)
        elif self._pixbuf:
            context.set_source_pixbuf(self._pixbuf, 0, 0)
        else:
            context.set_source_rgb(1, 1, 1) #fallback to white bg
        #create the background rectangle and fill it:
        context.rectangle(0, 0, rect.width, rect.height)
        context.fill()
        
    def set_color(self, color):
        """
        The set_color() method can be used to change the color of the
        background.
        
        @type color: gtk.gdk.Color
        @param color: Set the background to be filles with this color.
        """
        self.set_property("color", color)
        self.set_property("gradient", None)
        self.set_property("image", "")
        self.emit("appearance_changed")
        
    def get_color(self):
        """
        Returns the background's color.
        
        @return: gtk.gdk.Color.
        """
        return self.get_property("color")
        
    def set_gradient(self, color_start, color_end):
        """
        Use set_gradient() to define a vertical gradient as the background.
        
        @type color_start: gtk.gdk.Color
        @param color_start: The starting (top) color of the gradient.
        @type color_end: gtk.gdk.Color
        @param color_end: The ending (bottom) color of the gradient.
        """
        self.set_property("color", None)
        self.set_property("gradient", (color_start, color_end))
        self.set_property("image", "")
        self.emit("appearance_changed")
        
    def get_gradient(self):
        """
        Returns the gradient of the background or None.
        
        @return: A (gtk.gdk.Color, gtk.gdk.Color) tuple or None.
        """
        return self.get_property("gradient")
        
    def set_image(self, filename):
        """
        The set_image() method sets the background to be filled with an
        image.
        
        @type filename: string
        @param filename: Path to the file you want to use as background
        image. If the file does not exists, the background is set to white.
        """
        try:
            self._pixbuf = gtk.gdk.pixbuf_new_from_file(filename)
        except:
            self._pixbuf = None
        
        self.set_property("color", None)
        self.set_property("gradient", None)
        self.set_property("image", filename)
        self.emit("appearance_changed")
        
    def get_image(self):
        return self.get_property("image")
        
        
class Title(label.Label):
    """
    The title of a chart. The title will be drawn centered at the top of the
    chart.
    
    Properties
    ==========
    The Title class inherits properties from label.Label.
       
    Signals
    =======
    The Title class inherits signals from label.Label.
    """    
    
    def __init__(self, text=""):
        label.Label.__init__(self, (0, 0), text, weight=pango.WEIGHT_BOLD, anchor=label.ANCHOR_TOP_CENTER, fixed=True)
        
    def _do_draw(self, context, rect, top=-1):
        if top == -1: top = rect.height / 80
        self._size = max(8, int(rect.height / 50.0))
        self._position = rect.width / 2, top
        self._do_draw_label(context, rect)
        
        
class Area(ChartObject):
    """
    This is a base class for classes that represent areas, e.g. the
    pie_chart.PieArea class and the bar_chart.Bar class.
    
    Properties
    ==========
    The Area class inherits properties from chart_object.ChartObject.
    Additional properties:
     - name (a unique name for the area, type: string, read only)
     - value (the value of the area, type: float)
     - color (the area's color, type: gtk.gdk.Color)
     - label (a label for the area, type: string)
     - highlighted (set whether the area should be highlighted,
       type: boolean).
       
    Signals
    =======
    The Area class inherits signals from chart_object.ChartObject.
    """
    
    __gproperties__ = {"name": (gobject.TYPE_STRING, "area name",
                                "A unique name for the area.",
                                "", gobject.PARAM_READABLE),
                        "value": (gobject.TYPE_FLOAT,
                                    "value",
                                    "The value.",
                                    0.0, 9999999999.0, 0.0, gobject.PARAM_READWRITE),
                        "color": (gobject.TYPE_PYOBJECT, "area color",
                                    "The color of the area.",
                                    gobject.PARAM_READWRITE),
                        "label": (gobject.TYPE_STRING, "area label",
                                    "The label for the area.", "",
                                    gobject.PARAM_READWRITE),
                        "highlighted": (gobject.TYPE_BOOLEAN, "area is higlighted",
                                        "Set whether the area should be higlighted.",
                                        False, gobject.PARAM_READWRITE)}
    
    def __init__(self, name, value, title=""):
        ChartObject.__init__(self)
        self._name = name
        self._value = value
        self._label = title
        self._color = COLOR_AUTO
        self._highlighted = False
        
    def do_get_property(self, property):
        if property.name == "visible":
            return self._show
        elif property.name == "antialias":
            return self._antialias
        elif property.name == "name":
            return self._name
        elif property.name == "value":
            return self._value
        elif property.name == "color":
            return self._color
        elif property.name == "label":
            return self._label
        elif property.name == "highlighted":
            return self._highlighted
        else:
            raise AttributeError, "Property %s does not exist." % property.name

    def do_set_property(self, property, value):
        if property.name == "visible":
            self._show = value
        elif property.name == "antialias":
            self._antialias = value
        elif property.name == "value":
            self._value = value
        elif property.name == "color":
            self._color = value
        elif property.name == "label":
            self._label = value
        elif property.name == "highlighted":
            self._highlighted = value
        else:
            raise AttributeError, "Property %s does not exist." % property.name
            
    def set_value(self, value):
        """
        Set the value of the area.
        
        @type value: float.
        """
        self.set_property("value", value)
        self.emit("appearance_changed")
        
    def get_value(self):
        """
        Returns the current value of the area.
        
        @return: float.
        """
        return self.get_property("value")
        
    def set_color(self, color):
        """
        Set the color of the area.
        
        @type color: gtk.gdk.Color.
        """
        self.set_property("color", color)
        self.emit("appearance_changed")
        
    def get_color(self):
        """
        Returns the current color of the area or COLOR_AUTO.
        
        @return: gtk.gdk.Color or COLOR_AUTO.
        """
        return self.get_property("color")
        
    def set_label(self, label):
        """
        Set the label for the area.
        
        @param label: the new label
        @type label: string.
        """
        self.set_property("label", label)
        self.emit("appearance_changed")
        
    def get_label(self):
        """
        Returns the current label of the area.
        
        @return: string.
        """
        return self.get_property("label")
        
    def set_highlighted(self, highlighted):
        """
        Set whether the area should be highlighted.
        
        @type highlighted: boolean.
        """
        self.set_property("highlighted", highlighted)
        self.emit("appearance_changed")
        
    def get_highlighted(self):
        """
        Returns True if the area is currently highlighted.
        
        @return: boolean.
        """
        return self.get_property("highlighted")