# Copyright (c) 2001-2007 by Intevation GmbH
# Authors:
# Bernhard Herzog <bh@intevation.de> (2001-2003)
# Jonathan Coles <jonathan@intevation.de> (2003)
# Frank Koormann <frank.koormann@intevation.de> (2003)
# Jan-Oliver Wagner <jan@intevation.de> (2003-2005)
# Bernhard Reiter <bernhard@intevation.de> (2006-2007)
#
# This program is free software under the GPL (>=v2)
# Read the file COPYING coming with Thuban for details.

from __future__ import generators

__version__ = "$Revision: 2774 $"
# $Source$
# $Id: renderer.py 2774 2007-06-22 22:14:15Z bernhard $

import cStringIO

import array

import traceback

from Thuban import _

import wx

from wxproj import draw_polygon_shape, draw_polygon_init

from Thuban.UI.common import Color2wxColour
from Thuban.UI.classifier import ClassDataPreviewer
from Thuban.UI.scalebar import ScaleBar

from Thuban.Model.data import SHAPETYPE_POLYGON, SHAPETYPE_ARC, \
     SHAPETYPE_POINT, RAW_SHAPEFILE

from Thuban.Model.color import Transparent
import Thuban.Model.resource

from baserenderer import BaseRenderer

from math import floor

from types import StringType

from Thuban.version import versions

if Thuban.Model.resource.has_gdal_support():
    from gdalwarp import ProjectRasterFile

verbose = 0 # whether to talk more on stdout

# Map the strings used for the format parameter of the draw_raster_data
# method to the appropriate wxWindows constants
raster_format_map = {
    "BMP": wx.BITMAP_TYPE_BMP,
    "JPEG": wx.BITMAP_TYPE_JPEG,
    "PNG": wx.BITMAP_TYPE_PNG,
    "TIFF": wx.BITMAP_TYPE_TIF,
    "GIF": wx.BITMAP_TYPE_GIF,
    }

class MapRenderer(BaseRenderer):

    """Class to render a map onto a wxDC"""

    TRANSPARENT_PEN = wx.TRANSPARENT_PEN
    TRANSPARENT_BRUSH = wx.TRANSPARENT_BRUSH

    def make_point(self, x, y):
        return wx.Point(int(round(x)), int(round(y)))

    def tools_for_property(self, prop):
        fill = prop.GetFill()
        if fill is Transparent:
            brush = self.TRANSPARENT_BRUSH
        else:
            brush = wx.Brush(Color2wxColour(fill), wx.SOLID)

        stroke = prop.GetLineColor()
        if stroke is Transparent:
            pen = self.TRANSPARENT_PEN
        else:
            pen = wx.Pen(Color2wxColour(stroke), prop.GetLineWidth(), wx.SOLID)
        return pen, brush

    def low_level_renderer(self, layer):
        """Override inherited method to provide more efficient renderers

        If the underlying data format is not a shapefile or the layer
        contains points shapes, simply use what the inherited method
        returns.

        Otherwise, i.e. for arc and polygon use the more efficient
        wxproj.draw_polygon_shape and its corresponding parameter
        created with wxproj.draw_polygon_init.
        """
        if (layer.ShapeStore().RawShapeFormat() == RAW_SHAPEFILE
            and layer.ShapeType() in (SHAPETYPE_ARC, SHAPETYPE_POLYGON)):
            offx, offy = self.offset
            x = lambda a, b, c, d: None
            #return (True, x, None)
            return (True, draw_polygon_shape,
                    draw_polygon_init(layer.ShapeStore().Shapefile(),
                                      self.dc, self.map.projection,
                                      layer.projection,
                                      self.scale, -self.scale, offx, offy))
        else:
            return BaseRenderer.low_level_renderer(self, layer)

    def label_font(self):
        return wx.Font(int(round(self.resolution * 10)), wx.SWISS, wx.NORMAL,
                      wx.NORMAL)

    def projected_raster_layer(self, layer, srcProj, dstProj, extents,
                               resolution, dimensions, options):
        """Returns a raster layer image in projected space

        Based on a given filename.  This method must be implemented in
        classes derived from BaseRenderer.
        """

        ret = None

        if Thuban.Model.resource.has_gdal_support():

            try:
                if verbose > 0:
                    print "doing ProjectRasterFile '%s' -> '%s'" % \
                                (srcProj, dstProj)
                    print "extents:", extents, "resolution:", resolution
                    print "dimensions:", dimensions, "options:", options
                ret = ProjectRasterFile(layer.GetImageFilename(),
                                        srcProj, dstProj,
                                        extents, resolution, dimensions,
                                        options)
            except (MemoryError, IOError, AttributeError, ValueError):
                # Why does this catch AttributeError and ValueError?
                # FIXME: The exception should be communicated to the user
                # better.
                traceback.print_exc()

        return ret

    def draw_raster_data(self, x,y, data, format = 'BMP', opacity=1.0):

        mask = None
        alpha = None
        width = data[0]
        height = data[1]
        image_data, mask_data, alpha_data = data[2]

        if versions['wxPython-tuple'] < (2,5,3):
            alpha_data = None

        if format == 'RAW':
            image = wx.EmptyImage(width, height)
            image.SetData(image_data)
            if mask_data is not None:
                mask = wx.BitmapFromBits(mask_data, width, height, 1)
                mask = wx.Mask(mask)
            elif alpha_data is not None:
                # alpha_data is already in the right format
                alpha = alpha_data

        else:
            stream = cStringIO.StringIO(image_data)
            image = wx.ImageFromStream(stream, raster_format_map[format])

            if mask_data is not None:
                stream = cStringIO.StringIO(mask_data)
                mask = wx.ImageFromStream(stream, raster_format_map[format])
                mask = wx.Mask(wx.BitmapFromImage(mask, 1))
            elif alpha_data is not None:
                stream = cStringIO.StringIO(alpha_data)
                alpha = wx.ImageFromStream(stream, raster_format_map[format])
                alpha = alpha.GetData() #[:] # XXX: do we need to copy this?
            elif image.HasAlpha():
                alpha = image.GetAlphaData()

        #
        # scale down the alpha values the opacity level using a string 
        # translation table for efficiency.
        #
        if alpha is not None:
            if opacity == 0:
                return
            elif opacity == 1:
                a = alpha
            else:
                tr = [int(i*opacity) for i in range(256)]
                table = array.array('B', tr).tostring()
                a = alpha.translate(table)

            image.SetAlphaData(a)

        bitmap = wx.BitmapFromImage(image)

        if mask is not None:
            bitmap.SetMask(mask)

        self.dc.DrawBitmap(bitmap, int(round(x)), int(round(y)), True)


class ScreenRenderer(MapRenderer):

    # On the screen we want to see only visible layers by default
    honor_visibility = 1

    def RenderMap(self, selected_layer, selected_shapes):
        """Render the map.

        Only the given region (a tuple in window coordinates as returned
        by a wxrect's asTuple method) needs to be redrawn. Highlight the
        shapes given by the ids in selected_shapes in the
        selected_layer.
        """
        self.selected_layer = selected_layer
        self.selected_shapes = selected_shapes
        self.render_map()

    def RenderMapIncrementally(self):
        """Render the map.

        Only the given region (a tuple in window coordinates as returned
        by a wxrect's asTuple method) needs to be redrawn. Highlight the
        shapes given by the ids in selected_shapes in the
        selected_layer.
        """
        return self.render_map_incrementally()

    def draw_selection_incrementally(self, layer, selected_shapes):
        """Draw the selected shapes in a emphasized way (i.e.
        with a special pen and brush.
        The drawing is performed incrementally, that means every
        n shapes, the user can have interactions with the map.
        n is currently fixed to 500.

        layer -- the layer where the shapes belong to.
        selected_shapes -- a list of the shape-ids representing the
                           selected shapes for the given layer.
        """
        pen = wx.Pen(wx.BLACK, 3, wx.SOLID)
        brush = wx.Brush(wx.BLACK, wx.CROSS_HATCH)

        shapetype = layer.ShapeType()
        useraw, func, param = self.low_level_renderer(layer)
        args = (pen, brush)

        # for point shapes we need to find out the properties
        # to determine the size. Based on table and field,
        # we can find out the properties for object - see below.
        if shapetype == SHAPETYPE_POINT:
            lc = layer.GetClassification()
            field = layer.GetClassificationColumn()
            table = layer.ShapeStore().Table()

        count = 0
        for index in selected_shapes:
            count += 1
            shape = layer.Shape(index)

            # Get the size of the specific property for this
            # point
            if shapetype == SHAPETYPE_POINT:
                if field is not None:
                    value = table.ReadValue(shape.ShapeID(), field)
                    group = lc.FindGroup(value)
                    size = group.GetProperties().GetSize()
                else:
                    size = lc.GetDefaultGroup().GetProperties().GetSize()
                args = (pen, brush, size)

            if useraw:
                data = shape.RawData()
            else:
                data = shape.Points()
            func(param, data, *args)
            if count % 500 == 0:
                yield True

    def layer_shapes(self, layer):
        """Return the shapeids covered by the region that has to be redrawn

        Call the layer's ShapesInRegion method to determine the ids so
        that it can use the quadtree.
        """
        # FIXME: the quad-tree should be built from the projected
        # coordinates not the lat-long ones because it's not trivial to
        # determine an appropriate rectangle in lat-long for a given
        # rectangle in projected coordinates which we have to start from
        # here.
        proj = self.map.projection
        if proj is not None:
            inverse = proj.Inverse
        else:
            inverse = None

        scale = self.scale
        offx, offy = self.offset
        xs = []
        ys = []
        x, y, width, height = self.region
        for winx, winy in ((x, y), (x + width, y),
                           (x + width, y + height), (x, y + height)):
            px = (winx - offx) / scale
            py = -(winy - offy) / scale
            if inverse:
                px, py = inverse(px, py)
            xs.append(px)
            ys.append(py)
        left = min(xs)
        right = max(xs)
        top = max(ys)
        bottom = min(ys)

        return layer.ShapesInRegion((left, bottom, right, top))


class ExportRenderer(ScreenRenderer):

    honor_visibility = 1

    def __init__(self, *args, **kw):
        """Initialize the ExportRenderer.

        In addition to all parameters of the the ScreenRender
        constructor, this class requires and additional keyword argument
        destination_region with a tuple (minx, miny, maxx, maxy) giving
        the region in dc coordinates which is to contain the map.
        """
        self.destination_region = kw["destination_region"]
        del kw["destination_region"]
        ScreenRenderer.__init__(self, *args, **kw)

    def RenderMap(self, selected_layer, selected_shapes):
        """Render the map.

        The rendering device has been specified during initialisation.
        The device border distance was set in
        Thuban.UI.viewport.output_transform().

        RenderMap renders a frame set (one page frame, one around
        legend/scalebar and one around the map), the map, the legend and
        the scalebar on the given DC. The map is rendered with the
        region displayed in the canvas view, centered on the area
        available for map display.
        """

        self.selected_layer = selected_layer
        self.selected_shapes = selected_shapes

        # Get some dimensions
        llx, lly, urx, ury = self.region
        mminx, mminy, mmaxx, mmaxy = self.destination_region

        # Manipulate the offset to position the map
        offx, offy = self.offset
        # 1. Shift to corner of map drawing area
        offx = offx + mminx
        offy = offy + mminy

        # 2. Center the map on the map drawing area:
        # region identifies the region on the canvas view:
        # center of map drawing area - half the size of region: rendering origin
        self.shiftx = (mmaxx - mminx)*0.5 - (urx - llx)*0.5
        self.shifty = (mmaxy - mminy)*0.5 - (ury - lly)*0.5

        self.offset = (offx+self.shiftx, offy+self.shifty)
        self.region = (llx + self.shiftx, lly + self.shifty, urx, ury)

        # Draw the map
        self.dc.BeginDrawing()
        self.dc.DestroyClippingRegion()
        self.dc.SetClippingRegion(mminx+self.shiftx, mminy+self.shifty,
                                  urx, ury)
        self.render_map()
        self.dc.EndDrawing()

        # Draw the rest (frames, legend, scalebar)
        self.dc.BeginDrawing()
        self.dc.DestroyClippingRegion()

        # Force the font for Legend drawing
        font = wx.Font(self.resolution * 10, wx.SWISS, wx.NORMAL, wx.NORMAL)
        self.dc.SetFont(font)

        self.render_frame()
        self.render_legend()
        self.render_scalebar()
        self.dc.EndDrawing()

    def render_frame(self):
        """Render the frames for map and legend/scalebar."""

        dc = self.dc
        dc.SetPen(wx.BLACK_PEN)
        dc.SetBrush(wx.TRANSPARENT_BRUSH)

        # Dimension stuff
        width, height = dc.GetSizeTuple()
        mminx, mminy, mmaxx, mmaxy = self.destination_region

        # Page Frame
        dc.DrawRectangle(15,15,width-30, (mmaxy-mminy)+10)

        # Map Frame
        llx, lly, urx, ury = self.region
        dc.DrawRectangle(mminx + self.shiftx, mminy + self.shifty, urx, ury)

        # Legend Frame
        dc.DrawRectangle(mmaxx+10,mminy,(width-20) - (mmaxx+10), mmaxy-mminy)

        dc.DestroyClippingRegion()
        dc.SetClippingRegion(mmaxx+10,mminy,
                             (width-20) - (mmaxx+10), mmaxy-mminy)

    def render_legend(self):
        """Render the legend on the Map."""

        previewer = ClassDataPreviewer()
        dc = self.dc
        dc.SetPen(wx.BLACK_PEN)
        dc.SetBrush(wx.TRANSPARENT_BRUSH)

        # Dimension stuff
        width, height = dc.GetSizeTuple()
        mminx, mminy, mmaxx, mmaxy = self.destination_region
        textwidth, textheight = dc.GetTextExtent("0")
        iconwidth  = textheight
        iconheight = textheight
        stepy = textheight+3
        dx = 10
        posx = mmaxx + 10 + 5   # 10 pix distance mapframe/legend frame,
                                # 5 pix inside legend frame
        posy = mminy + 5        # 5 pix inside legend frame

        # Render the legend
        dc.SetTextForeground(wx.BLACK)
        if self.map.HasLayers():
            layers = self.map.Layers()[:]
            layers.reverse()
            for l in layers:
                if l.Visible():
                    # Render title
                    dc.DrawText(l.Title(), posx, posy)
                    posy+=stepy
                    if l.HasClassification():
                        # Render classification
                        clazz = l.GetClassification()
                        shapeType = l.ShapeType()
                        for g in clazz:
                            if g.IsVisible():
                                previewer.Draw(dc,
                                    wx.Rect(posx+dx, posy,
                                           iconwidth, iconheight),
                                    g.GetProperties(), shapeType)
                                dc.DrawText(g.GetDisplayText(),
                                            posx+2*dx+iconwidth, posy)
                                posy+=stepy

    def render_scalebar(self):
        """Render the scalebar."""

        scalebar = ScaleBar(self.map)

        # Dimension stuff
        width, height = self.dc.GetSizeTuple()
        mminx, mminy, mmaxx, mmaxy = self.destination_region

        # Render the scalebar
        scalebar.DrawScaleBar(self.scale, self.dc,
                             (mmaxx+10+5, mmaxy-25),
                             ((width-15-5) - (mmaxx+10+5),20)
                            )
        # 10 pix between map and legend frame, 5 pix inside legend frame
        # 25 pix from the legend frame bottom line
        # Width: 15 pix from DC border, 5 pix inside frame, 10, 5 as above
        # Height: 20

class PrinterRenderer(ExportRenderer):

    # Printing as well as Export / Screen display only the visible layer.
    honor_visibility = 1

