# Copyright (c) 2001, 2002, 2003, 2004, 2007 by Intevation GmbH
# Authors:
# Bernhard Herzog <bh@intevation.de>
# Frank Koormann <frank@intevation.de>
#
# This program is free software under the GPL (>=v2)
# Read the file COPYING coming with Thuban for details.

"""
Classes for display of a map and interaction with it
"""

from __future__ import generators

__version__ = "$Revision: 2885 $"
# $Source$
# $Id: view.py 2885 2009-08-18 13:35:30Z dpinte $

import os.path
import time
import traceback

import wx

# Export related stuff
if wx.Platform == '__WXMSW__':
    from wx import MetaFileDC

from Thuban import _

from Thuban.Model.messages import MAP_LAYERS_CHANGED, LAYER_CHANGED, \
     LAYER_VISIBILITY_CHANGED
from Thuban.UI import internal_from_wxstring

from renderer import ScreenRenderer, ExportRenderer, PrinterRenderer

import labeldialog

from viewport import ViewPort, PanTool, output_transform

class CanvasPanTool(PanTool):

    """The Canvas Pan Tool"""

    def MouseMove(self, event):
        if self.dragging:
            PanTool.MouseMove(self, event)
            sx, sy = self.start
            x, y = self.current
            width, height = self.view.GetSizeTuple()

            bitmapdc = wx.MemoryDC()
            bitmapdc.SelectObject(self.view.PreviewBitmap())

            dc = self.view.drag_dc
            dc.Blit(0, 0, width, height, bitmapdc, sx - x, sy - y)

class MapPrintout(wx.Printout):

    """
    wxPrintout class for printing Thuban maps
    """

    def __init__(self, canvas, map, region, selected_layer, selected_shapes):
        wx.Printout.__init__(self)
        self.canvas = canvas
        self.map = map
        self.region = region
        self.selected_layer = selected_layer
        self.selected_shapes = selected_shapes

    def GetPageInfo(self):
        return (1, 1, 1, 1)

    def HasPage(self, pagenum):
        return pagenum == 1

    def OnPrintPage(self, pagenum):
        if pagenum == 1:
            self.draw_on_dc(self.GetDC())

    def draw_on_dc(self, dc):
        width, height = self.GetPageSizePixels()
        scale, offset, mapregion = output_transform(self.canvas.scale,
                                                    self.canvas.offset,
                                                    self.canvas.GetSizeTuple(),
                                                    self.GetPageSizePixels())
        resx, resy = self.GetPPIPrinter()
        canvas_scale = self.canvas.scale
        x, y, width, height = self.region
        renderer = PrinterRenderer(dc, self.map, scale, offset,
                                   region = (mapregion[0], mapregion[1],
                                             (width/canvas_scale)*scale,
                                             (height/canvas_scale)*scale),
                                   resolution = resy,
                                   destination_region = mapregion)
        renderer.RenderMap(self.selected_layer, self.selected_shapes)
        return True


class MapCanvas(wx.Window, ViewPort):

    """A widget that displays a map and offers some interaction"""

    def __init__(self, parent, winid):
        wx.Window.__init__(self, parent, winid)
        ViewPort.__init__(self)

        self.SetBackgroundColour(wx.Colour(255, 255, 255))

        # the bitmap serving as backing store
        self.bitmap = None
        # the monochrome bitmap with the selection if any
        self.selection_bitmap = None

        self.backgroundColor = wx.WHITE_BRUSH

        # The rendering iterator object. Used when rendering
        # incrementally
        self.render_iter = None

        # subscribe the WX events we're interested in
        self.Bind(wx.EVT_PAINT, self.OnPaint)
        self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
        self.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDoubleClick)
        self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
        self.Bind(wx.EVT_MIDDLE_DOWN, self.OnMiddleDown)
        self.Bind(wx.EVT_MIDDLE_UP, self.OnMiddleUp)
        self.Bind(wx.EVT_MOTION, self.OnMotion)
        self.Bind(wx.EVT_MOUSEWHEEL, self.OnMouseWheel)
        self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeaveWindow)
        self.Bind(wx.EVT_SIZE, self.OnSize)
        self.Bind(wx.EVT_IDLE, self.OnIdle)

    def __del__(self):
        #wx.Window.__del__(self)
        ViewPort.__del__(self)

    def PreviewBitmap(self):
        return self.bitmap

    def PanTool(self):
        """Start the canvas pan tool"""
        self.SelectTool(CanvasPanTool(self))

    def SetMap(self, map):
        redraw_channels = (MAP_LAYERS_CHANGED, LAYER_CHANGED,
                           LAYER_VISIBILITY_CHANGED)
        if self.Map() is not None:
            for channel in redraw_channels:
                self.Map().Unsubscribe(channel, self.full_redraw)

        ViewPort.SetMap(self, map)

        if self.Map() is not None:
            for channel in redraw_channels:
                self.Map().Subscribe(channel, self.full_redraw)

        # force a redraw. If map is not empty, it's already been called
        # by FitMapToWindow but if map is empty it hasn't been called
        # yet so we have to explicitly call it.
        self.full_redraw()

    def OnPaint(self, event):
        dc = wx.PaintDC(self)
        if self.Map() is not None and self.Map().HasLayers():
            if self.bitmap is not None:
                dc.BeginDrawing()
                dc.DrawBitmap(self.bitmap, 0, 0)
                if self.selection_bitmap is not None:
                    dc.DrawBitmap(self.selection_bitmap, 0, 0, True)
                dc.EndDrawing()
        else:
            # If we've got no map or if the map is empty, simply clear
            # the screen.

            # XXX it's probably possible to get rid of this. The
            # background color of the window is already white and the
            # only thing we may have to do is to call self.Refresh()
            # with a true argument in the right places.
            dc.BeginDrawing()
            dc.SetBackground(self.backgroundColor)
            dc.Clear()
            dc.EndDrawing()

    def OnIdle(self, event):
        """Idle handler. Redraw the bitmap if necessary"""
        if (self.Map() is not None
            and (self.bitmap is None
                 or self.render_iter is not None
                 or (self.HasSelectedShapes()
                     and self.selection_bitmap is None))):
            event.RequestMore(self._do_redraw())

    def _do_redraw(self):
        """Redraw a bit and return whether this method has to be called again.

        Called by OnIdle to handle the actual redraw. Redraw is
        incremental for both the bitmap with the normal layers and the
        bitmap with the selection.
        """
        finished = False
        if self.render_iter is not None:
            try:
                if self.render_iter.next():
                    # Redraw if the last preview redraw was some time
                    # ago and the user is not currently dragging the
                    # mouse because redrawing would interfere with what
                    # the current tool is drawing on the window.
                    if not self.dragging \
                           and time.time() - self.render_last_preview > 0.5:
                        client_dc = wx.ClientDC(self)
                        client_dc.BeginDrawing()
                        client_dc.DrawBitmap(self.bitmap, 0, 0)
                        client_dc.EndDrawing()
                        self.render_last_preview = time.time()
                else:
                    self.render_iter = None
                    # Redraw if not dragging because redrawing would
                    # interfere with what the current tool is drawing on
                    # the window.
                    if not self.dragging:
                        self.redraw()
                    finished = True
            except StopIteration:
                finished = True
                self.render_iter = None
            except:
                finished = True
                self.render_iter = None
                traceback.print_exc()
        else:
            self.render_iter = self._render_iterator()
            self.render_last_preview = time.time()
        return not finished

    def _render_iterator(self):
        width, height = self.GetSizeTuple()
        dc = wx.MemoryDC()

        render_start = time.time()

        if self.bitmap is None:
            self.bitmap = wx.EmptyBitmap(width, height)
            dc.SelectObject(self.bitmap)
            dc.BeginDrawing()

            dc.SetBackground(self.backgroundColor)
            dc.Clear()

            # draw the map into the bitmap
            renderer = ScreenRenderer(dc, self.Map(), self.scale, self.offset,
                                      (0, 0, width, height))
            for cont in renderer.RenderMapIncrementally():
                yield True

            dc.EndDrawing()
            dc.SelectObject(wx.NullBitmap)

        if self.HasSelectedShapes() and self.selection_bitmap is None:
            bitmap = wx.EmptyBitmap(width, height)
            dc.SelectObject(bitmap)
            dc.BeginDrawing()
            dc.SetBackground(wx.WHITE_BRUSH)
            dc.Clear()

            renderer = ScreenRenderer(dc, self.Map(), self.scale, self.offset,
                                      (0, 0, width, height))
            layer = self.SelectedLayer()
            shapes = self.selection.SelectedShapes()
            for cont in renderer.draw_selection_incrementally(layer, shapes):
                yield True

            dc.EndDrawing()
            dc.SelectObject(wx.NullBitmap)

            bitmap.SetMask(wx.Mask(bitmap, wx.WHITE))
            self.selection_bitmap = bitmap

        yield False

    def Export(self):

        if hasattr(self, "export_path"):
            export_path = self.export_path
        else:
            export_path="."
        dlg = wx.FileDialog(self, _("Export Map"), export_path, "",
                           "Enhanced Metafile (*.wmf)|*.wmf",
                           wx.SAVE|wx.OVERWRITE_PROMPT)
        if dlg.ShowModal() == wx.ID_OK:
            path = internal_from_wxstring(dlg.GetPath())
            self.export_path = os.path.dirname(path)
            dc = wx.MetaFileDC(path)

            scale, offset, mapregion = output_transform(self.scale,
                                                        self.offset,
                                                        self.GetSizeTuple(),
                                                        dc.GetSizeTuple())

            selected_layer = self.selection.SelectedLayer()
            selected_shapes = self.selection.SelectedShapes()

            width, height = self.GetSizeTuple()
            renderer = ExportRenderer(dc, self.Map(), scale, offset,
                                      region = (0, 0,
                                                (width/self.scale)*scale,
                                                (height/self.scale)*scale),
                                      destination_region = mapregion)
            renderer.RenderMap(selected_layer, selected_shapes)

            dc.EndDrawing()
            dc.Close()
        dlg.Destroy()

    def Print(self):
        # experiments have shown that the next three lines make
        # printing work with python-wxgtk2.6 2.6.3.2.0bpo1 on Debian Sarge
        # Inspired from the PrintingFramework demo of wxPython2.6
        pdd = wx.PrintDialogData()
        pdd.SetToPage(1)
        printer = wx.Printer(pdd)
        # the following line used to be enough before, unkown when it broke
        #printer = wx.Printer()
        width, height = self.GetSizeTuple()
        selected_layer = self.selection.SelectedLayer()
        selected_shapes = self.selection.SelectedShapes()

        printout = MapPrintout(self, self.Map(), (0, 0, width, height),
                               selected_layer, selected_shapes)
        if not printer.Print(self, printout, True):
            print "printing canceled or failed" # FIXME: add real handling
        printout.Destroy()

    def redraw(self, *args):
        self.Refresh(False)

    def full_redraw(self, *args):
        self.bitmap = None
        self.selection_bitmap = None
        self.render_iter = None
        self.redraw()

    def redraw_selection(self, *args):
        self.selection_bitmap = None
        self.render_iter = None
        self.redraw()

    def map_projection_changed(self, map, old_proj):
        ViewPort.map_projection_changed(self, map, old_proj)
        self.full_redraw()

    def layer_projection_changed(self, *args):
        ViewPort.layer_projection_changed(self, args)
        self.full_redraw()

    def set_view_transform(self, scale, offset):
        ViewPort.set_view_transform(self, scale, offset)
        self.full_redraw()

    def GetPortSizeTuple(self):
        return self.GetSizeTuple()

    def OnMiddleDown(self, event):
        self.remembertool = self.tool
        if self.Map() is not None and self.Map().HasLayers():
            self.PanTool()
            self.OnLeftDown(event)

    def OnMiddleUp(self, event):
        self.OnLeftUp(event)
        if self.remembertool:
            self.SelectTool(self.remembertool)

    def OnLeftDown(self, event):
        self.MouseLeftDown(event)
        if self.tool is not None:
            self.drag_dc = wx.ClientDC(self)
            self.drag_dc.SetLogicalFunction(wx.INVERT)
            self.drag_dc.SetBrush(wx.TRANSPARENT_BRUSH)
            self.tool.Show(self.drag_dc)
            self.CaptureMouse()
            self.dragging = 1

    def OnLeftUp(self, event):
        """Handle EVT_LEFT_UP

        Release the mouse if it was captured, if a tool is active call
        its Hide method and call self.MouseLeftUp.
        """
        # It's important that ReleaseMouse is called before MouseLeftUp.
        # MouseLeftUp may pop up modal dialogs which leads to an
        # effectively frozen X session because the user can only
        # interact with the dialog but the mouse is still grabbed by the
        # canvas.
        if self.dragging:
            if self.HasCapture():
                self.ReleaseMouse()
            try:
                self.tool.Hide(self.drag_dc)
            finally:
                self.drag_dc = None
                self.dragging = 0
        self.MouseLeftUp(event)

    def OnLeftDoubleClick(self, event):
        """ Handle EVT_LEFT_DCLICK
        """
        self.MouseDoubleClick(event)

    def OnMotion(self, event):
        if self.dragging:
            self.tool.Hide(self.drag_dc)

        self.MouseMove(event)

        if self.dragging:
            self.tool.Show(self.drag_dc)

    def OnLeaveWindow(self, event):
        self.set_current_position(None)
        
    
    def OnMouseWheel(self, event):
        """
        Manages the zoom on wheel event
        """
        if event.GetWheelRotation() > 0:
            factor = 2
        else: factor = 1.0/2.0
        self.ZoomFactor(factor, center=(event.m_x, event.m_y))

    def OnSize(self, event):
        # the window's size has changed. We have to get a new bitmap. If
        # we want to be clever we could try to get by without throwing
        # everything away. E.g. when the window gets smaller, we could
        # either keep the bitmap or create the new one from the old one.
        # Even when the window becomes larger some parts of the bitmap
        # could be reused.
        self.full_redraw()

    def shape_selected(self, layer, shape):
        """Receiver for the SHAPES_SELECTED messages. Redraw the map."""
        # The selection object takes care that it only issues
        # SHAPES_SELECTED messages when the set of selected shapes has
        # actually changed, so we can do a full redraw of the
        # selection_bitmap unconditionally.
        ViewPort.shape_selected(self, layer, shape)
        self.redraw_selection()

    def GetTextExtent(self, text):
        dc = wx.ClientDC(self)
        font = wx.Font(10, wx.SWISS, wx.NORMAL, wx.NORMAL)
        dc.SetFont(font)
        return dc.GetTextExtent(text.decode('iso-8859-1'))

    def LabelShapeAt(self, x, y, text=None):
        """Add or remove a label at window position x, y.

        If there's a label at the given position, remove it. Otherwise
        determine the shape at the position, run the label dialog and
        unless the user cancels the dialog, add a label.
        """
        layer, shape_index = self.find_shape_at(x, y, select_labels = 1)
        if layer is None and shape_index is not None:
            ViewPort.LabelShapeAt(self, x, y)
        elif layer is not None:
            text = labeldialog.run_label_dialog(self,
                                                layer.ShapeStore().Table(),
                                                shape_index)
            ViewPort.LabelShapeAt(self, x, y, text)


