"""
@package psmap.frame

@brief GUI for ps.map

Classes:
 - frame::PsMapFrame
 - frame::PsMapBufferedWindow

(C) 2011-2012 by Anna Kratochvilova, 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.

@author Anna Kratochvilova <kratochanna gmail.com> (bachelor's project)
@author Martin Landa <landa.martin gmail.com> (mentor)
"""

import os
import sys

if sys.version_info.major == 2:
    import Queue
else:
    import queue as Queue
from math import sin, cos, pi, sqrt

import wx

try:
    import wx.lib.agw.flatnotebook as FN
except ImportError:
    import wx.lib.flatnotebook as FN

import grass.script as grass

from core import globalvar
from gui_core.menu import Menu
from core.gconsole import CmdThread, EVT_CMD_DONE
from psmap.toolbars import PsMapToolbar
from core.gcmd import RunCommand, GError, GMessage
from core.settings import UserSettings
from core.utils import PilImageToWxImage
from gui_core.forms import GUI
from gui_core.widgets import GNotebook
from gui_core.dialogs import HyperlinkDialog
from gui_core.ghelp import ShowAboutDialog
from gui_core.wrap import ClientDC, PseudoDC, Rect, StockCursor, EmptyBitmap
from psmap.menudata import PsMapMenuData
from gui_core.toolbars import ToolSwitcher

from psmap.dialogs import *
from psmap.instructions import *
from psmap.utils import *


class PsMapFrame(wx.Frame):
    def __init__(
        self, parent=None, id=wx.ID_ANY, title=_("Cartographic Composer"), **kwargs
    ):
        """Main window of ps.map GUI

        :param parent: parent window
        :param id: window id
        :param title: window title

        :param kwargs: wx.Frames' arguments
        """
        self.parent = parent

        wx.Frame.__init__(
            self, parent=parent, id=id, title=title, name="PsMap", **kwargs
        )
        self.SetIcon(
            wx.Icon(os.path.join(globalvar.ICONDIR, "grass.ico"), wx.BITMAP_TYPE_ICO)
        )
        # menubar
        self.menubar = Menu(
            parent=self, model=PsMapMenuData().GetModel(separators=True)
        )
        self.SetMenuBar(self.menubar)
        # toolbar

        self._toolSwitcher = ToolSwitcher()
        self.toolbar = PsMapToolbar(parent=self, toolSwitcher=self._toolSwitcher)
        # workaround for http://trac.wxwidgets.org/ticket/13888
        if sys.platform != "darwin":
            self.SetToolBar(self.toolbar)

        self.iconsize = (16, 16)
        # satusbar
        self.statusbar = self.CreateStatusBar(number=1)

        # mouse attributes -- position on the screen, begin and end of
        # dragging, and type of drawing
        self.mouse = {
            "begin": [0, 0],  # screen coordinates
            "end": [0, 0],
            "use": "pointer",
        }
        # available cursors
        self.cursors = {
            "default": StockCursor(wx.CURSOR_ARROW),
            "cross": StockCursor(wx.CURSOR_CROSS),
            "hand": StockCursor(wx.CURSOR_HAND),
            "sizenwse": StockCursor(wx.CURSOR_SIZENWSE),
        }
        # pen and brush
        self.pen = {
            "paper": wx.Pen(colour="BLACK", width=1),
            "margins": wx.Pen(colour="GREY", width=1),
            "map": wx.Pen(colour=wx.Colour(86, 122, 17), width=2),
            "rasterLegend": wx.Pen(colour=wx.Colour(219, 216, 4), width=2),
            "vectorLegend": wx.Pen(colour=wx.Colour(219, 216, 4), width=2),
            "mapinfo": wx.Pen(colour=wx.Colour(5, 184, 249), width=2),
            "scalebar": wx.Pen(colour=wx.Colour(150, 150, 150), width=2),
            "image": wx.Pen(colour=wx.Colour(255, 150, 50), width=2),
            "northArrow": wx.Pen(colour=wx.Colour(200, 200, 200), width=2),
            "point": wx.Pen(colour=wx.Colour(100, 100, 100), width=2),
            "line": wx.Pen(colour=wx.Colour(0, 0, 0), width=2),
            "box": wx.Pen(colour="RED", width=2, style=wx.SHORT_DASH),
            "select": wx.Pen(colour="BLACK", width=1, style=wx.SHORT_DASH),
            "resize": wx.Pen(colour="BLACK", width=1),
        }
        self.brush = {
            "paper": wx.WHITE_BRUSH,
            "margins": wx.TRANSPARENT_BRUSH,
            "map": wx.Brush(wx.Colour(151, 214, 90)),
            "rasterLegend": wx.Brush(wx.Colour(250, 247, 112)),
            "vectorLegend": wx.Brush(wx.Colour(250, 247, 112)),
            "mapinfo": wx.Brush(wx.Colour(127, 222, 252)),
            "scalebar": wx.Brush(wx.Colour(200, 200, 200)),
            "image": wx.Brush(wx.Colour(255, 200, 50)),
            "northArrow": wx.Brush(wx.Colour(255, 255, 255)),
            "point": wx.Brush(wx.Colour(200, 200, 200)),
            "line": wx.TRANSPARENT_BRUSH,
            "box": wx.TRANSPARENT_BRUSH,
            "select": wx.TRANSPARENT_BRUSH,
            "resize": wx.BLACK_BRUSH,
        }

        # list of objects to draw
        self.objectId = []

        # we need isolated environment to handle region
        self.env = os.environ.copy()

        # instructions
        self.instruction = Instruction(
            parent=self, objectsToDraw=self.objectId, env=self.env
        )
        # open dialogs
        self.openDialogs = dict()

        self.pageId = NewId()
        # current page of GNotebook
        self.currentPage = 0
        # canvas for draft mode
        self.canvas = PsMapBufferedWindow(
            parent=self,
            mouse=self.mouse,
            pen=self.pen,
            brush=self.brush,
            cursors=self.cursors,
            instruction=self.instruction,
            openDialogs=self.openDialogs,
            pageId=self.pageId,
            objectId=self.objectId,
            preview=False,
            env=self.env,
        )

        self.canvas.SetCursor(self.cursors["default"])
        self.getInitMap()

        # image path
        env = grass.gisenv()
        self.imgName = grass.tempfile()

        # canvas for preview
        self.previewCanvas = PsMapBufferedWindow(
            parent=self,
            mouse=self.mouse,
            cursors=self.cursors,
            pen=self.pen,
            brush=self.brush,
            preview=True,
            env=self.env,
        )

        self.toolbar.SelectDefault()

        # create queues
        self.requestQ = Queue.Queue()
        self.resultQ = Queue.Queue()
        # thread
        self.cmdThread = CmdThread(self, self.requestQ, self.resultQ)

        self._layout()
        self.SetMinSize(wx.Size(775, 600))
        # workaround for http://trac.wxwidgets.org/ticket/13628
        self.SetSize(self.GetBestSize())

        self.Bind(FN.EVT_FLATNOTEBOOK_PAGE_CHANGED, self.OnPageChanged)
        self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)
        self.Bind(EVT_CMD_DONE, self.OnCmdDone)

        if not havePILImage:
            wx.CallAfter(self._showErrMsg)

    def _showErrMsg(self):
        """Show error message (missing preview)"""
        GError(
            parent=self,
            message=_(
                "Python Imaging Library is not available.\n"
                "'Preview' functionality won't work."
            ),
            showTraceback=False,
        )

    def _layout(self):
        """Do layout"""
        mainSizer = wx.BoxSizer(wx.VERTICAL)

        self.book = GNotebook(parent=self, style=globalvar.FNPageDStyle)
        self.book.AddPage(self.canvas, "Draft mode")
        self.book.AddPage(self.previewCanvas, "Preview")
        self.book.SetSelection(0)

        mainSizer.Add(self.book, 1, wx.EXPAND)

        self.SetSizer(mainSizer)
        mainSizer.Fit(self)

    def _checkMapFrameExists(self, type_id):
        """Check if map frame exists

        :param int type_id: type id (raster, vector,...)

        :return bool: False if map frame doesn't exists
        """
        if self.instruction.FindInstructionByType("map"):
            mapId = self.instruction.FindInstructionByType("map").id
        else:
            mapId = None

        if not type_id:
            if not mapId:
                GMessage(message=_("Please, create map frame first."))
                return False
        return True

    def _switchToPage(self, page_index=0):
        """Switch to page (default to Draft page)

        :param int page_index: page index where you want to switch
        """
        self.book.SetSelection(page_index)
        self.currentPage = page_index

    def _getGhostscriptProgramName(self):
        """Get Ghostscript program name

        :return: Ghostscript program name
        """
        import platform

        return "gswin64c" if "64" in platform.architecture()[0] else "gswin32c"

    def InstructionFile(self):
        """Creates mapping instructions"""

        text = str(self.instruction)
        try:
            text = text.encode("Latin_1")
        except UnicodeEncodeError as err:
            try:
                pos = str(err).split("position")[1].split(":")[0].strip()
            except IndexError:
                pos = ""
            if pos:
                message = (
                    _(
                        "Characters on position %s are not supported "
                        "by ISO-8859-1 (Latin 1) encoding "
                        "which is required by module ps.map."
                    )
                    % pos
                )
            else:
                message = _(
                    "Not all characters are supported "
                    "by ISO-8859-1 (Latin 1) encoding "
                    "which is required by module ps.map."
                )
            GMessage(message=message)
            return ""
        return text

    def OnPSFile(self, event):
        """Generate PostScript"""
        filename = self.getFile(
            wildcard="PostScript (*.ps)|*.ps|Encapsulated PostScript (*.eps)|*.eps"
        )
        if filename:
            self.PSFile(filename)

    def OnPsMapDialog(self, event):
        """Launch ps.map dialog"""
        GUI(parent=self).ParseCommand(cmd=["ps.map"])

    def OnPDFFile(self, event):
        """Generate PDF from PS with ps2pdf if available"""
        if not sys.platform == "win32":
            try:
                p = grass.Popen(["ps2pdf"], stderr=grass.PIPE)
                p.stderr.close()

            except OSError:
                GMessage(
                    parent=self,
                    message=_(
                        "Program ps2pdf is not available. Please install it first to create PDF."
                    ),
                )
                return

        filename = self.getFile(wildcard="PDF (*.pdf)|*.pdf")
        if filename:
            self.PSFile(filename, pdf=True)

    def OnPreview(self, event):
        """Run ps.map and show result"""
        self.PSFile()

    def PSFile(self, filename=None, pdf=False):
        """Create temporary instructions file and run ps.map with output = filename"""
        instrFile = grass.tempfile()
        instrFileFd = open(instrFile, mode="wb")
        content = self.InstructionFile()
        if not content:
            return
        instrFileFd.write(content)
        instrFileFd.flush()
        instrFileFd.close()

        temp = False
        regOld = grass.region(env=self.env)

        if pdf:
            pdfname = filename
        else:
            pdfname = None
        # preview or pdf
        if not filename or (filename and pdf):
            temp = True
            filename = grass.tempfile()
            if not pdf:  # lower resolution for preview
                if self.instruction.FindInstructionByType("map"):
                    mapId = self.instruction.FindInstructionByType("map").id
                    SetResolution(
                        dpi=100,
                        width=self.instruction[mapId]["rect"][2],
                        height=self.instruction[mapId]["rect"][3],
                        env=self.env,
                    )

        cmd = ["ps.map", "--overwrite"]
        if os.path.splitext(filename)[1] == ".eps":
            cmd.append("-e")
        if self.instruction[self.pageId]["Orientation"] == "Landscape":
            cmd.append("-r")
        cmd.append("input=%s" % instrFile)
        cmd.append("output=%s" % filename)
        if pdf:
            self.SetStatusText(_("Generating PDF..."), 0)
        elif not temp:
            self.SetStatusText(_("Generating PostScript..."), 0)
        else:
            self.SetStatusText(_("Generating preview..."), 0)

        self.cmdThread.RunCmd(
            cmd,
            env=self.env,
            userData={
                "instrFile": instrFile,
                "filename": filename,
                "pdfname": pdfname,
                "temp": temp,
                "regionOld": regOld,
            },
        )

    def OnCmdDone(self, event):
        """ps.map process finished"""

        if event.returncode != 0:
            GMessage(
                parent=self,
                message=_("Ps.map exited with return code %s") % event.returncode,
            )

            grass.try_remove(event.userData["instrFile"])
            if event.userData["temp"]:
                grass.try_remove(event.userData["filename"])
            return

        if event.userData["pdfname"]:
            if sys.platform == "win32":
                import platform

                arch = platform.architecture()[0]
                pdf_rendering_prog = "gswin64c"
                if "32" in arch:
                    pdf_rendering_prog = "gswin32c"
                command = [
                    pdf_rendering_prog,
                    "-P-",
                    "-dSAFER",
                    "-dCompatibilityLevel=1.4",
                    "-q",
                    "-P-",
                    "-dNOPAUSE",
                    "-dBATCH",
                    "-sDEVICE=pdfwrite",
                    "-dPDFSETTINGS=/prepress",
                    "-r1200",
                    "-sstdout=%stderr",
                    "-sOutputFile=%s" % event.userData["pdfname"],
                    "-P-",
                    "-dSAFER",
                    "-dCompatibilityLevel=1.4",
                    "-c",
                    "30000000",
                    "setvmthreshold",
                    "-f",
                    event.userData["filename"],
                ]
                title = _("Program {} is not available.").format(pdf_rendering_prog)
                message = _("{title} Please install it to create PDF.\n\n").format(
                    title=title
                )
            else:
                pdf_rendering_prog = "ps2pdf"
                command = [
                    pdf_rendering_prog,
                    "-dPDFSETTINGS=/prepress",
                    "-r1200",
                    event.userData["filename"],
                    event.userData["pdfname"],
                ]
                message = _(
                    "Program {} is not available."
                    " Please install it to create PDF.\n\n "
                ).format(pdf_rendering_prog)
            try:
                proc = grass.Popen(command)
                ret = proc.wait()
                if ret > 0:
                    GMessage(
                        parent=self,
                        message=_("%(prg)s exited with return code %(code)s")
                        % {"prg": command[0], "code": ret},
                    )
                else:
                    self.SetStatusText(_("PDF generated"), 0)
            except OSError as e:
                if sys.platform == "win32":
                    dlg = HyperlinkDialog(
                        self,
                        title=title,
                        message=message + str(e),
                        hyperlink="https://www.ghostscript.com/releases/gsdnld.html",
                        hyperlinkLabel=_("You can download {} version here.").format(
                            arch
                        ),
                    )
                    dlg.ShowModal()
                    dlg.Destroy()
                    return
                GError(parent=self, message=message + str(e))

        elif not event.userData["temp"]:
            self.SetStatusText(_("PostScript file generated"), 0)

        # show preview only when user doesn't want to create ps or pdf
        if havePILImage and event.userData["temp"] and not event.userData["pdfname"]:
            self.env["GRASS_REGION"] = grass.region_env(
                cols=event.userData["regionOld"]["cols"],
                rows=event.userData["regionOld"]["rows"],
                env=self.env,
            )
            # wx.BusyInfo does not display the message
            busy = wx.BusyInfo(_("Generating preview, wait please"), parent=self)
            wx.GetApp().Yield()
            try:
                im = PILImage.open(event.userData["filename"])
                if self.instruction[self.pageId]["Orientation"] == "Landscape":
                    import numpy as np

                    im_array = np.array(im)
                    im = PILImage.fromarray(np.rot90(im_array, 3))
                im.save(self.imgName, format="PNG")
            except (IOError, OSError):
                del busy
                program = self._getGhostscriptProgramName()
                dlg = HyperlinkDialog(
                    self,
                    title=_("Preview not available"),
                    message=_(
                        "Preview is not available probably because Ghostscript is not installed or not on PATH."
                    ),
                    hyperlink="https://www.ghostscript.com/releases/gsdnld.html",
                    hyperlinkLabel=_(
                        "You can download {program} {arch} version here."
                    ).format(
                        program=program,
                        arch="64bit" if "64" in program else "32bit",
                    ),
                )
                dlg.ShowModal()
                dlg.Destroy()
                return

            self.book.SetSelection(1)
            self.currentPage = 1
            rect = self.previewCanvas.ImageRect()
            self.previewCanvas.image = wx.Image(self.imgName, wx.BITMAP_TYPE_PNG)
            self.previewCanvas.DrawImage(rect=rect)

            del busy
            self.SetStatusText(_("Preview generated"), 0)

        grass.try_remove(event.userData["instrFile"])
        if event.userData["temp"]:
            grass.try_remove(event.userData["filename"])

        self.delayedCall = wx.CallLater(
            4000,
            lambda: self.SetStatusText("", 0) if self else None,
        )

    def getFile(self, wildcard):
        suffix = []
        for filter in wildcard.split("|")[1::2]:
            s = filter.strip("*").split(".")[1]
            if s:
                s = "." + s
            suffix.append(s)
        raster = self.instruction.FindInstructionByType("raster")
        if raster:
            rasterId = raster.id
        else:
            rasterId = None

        if rasterId and self.instruction[rasterId]["raster"]:
            mapName = self.instruction[rasterId]["raster"].split("@")[0] + suffix[0]
        else:
            mapName = ""

        filename = ""
        dlg = wx.FileDialog(
            self,
            message=_("Save file as"),
            defaultDir="",
            defaultFile=mapName,
            wildcard=wildcard,
            style=wx.FD_CHANGE_DIR | wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT,
        )
        if dlg.ShowModal() == wx.ID_OK:
            filename = dlg.GetPath()
            suffix = suffix[dlg.GetFilterIndex()]
            if not os.path.splitext(filename)[1]:
                filename = filename + suffix
            elif os.path.splitext(filename)[1] != suffix and suffix != "":
                filename = os.path.splitext(filename)[0] + suffix

        dlg.Destroy()
        return filename

    def OnInstructionFile(self, event):
        filename = self.getFile(
            wildcard="*.psmap|*.psmap|Text file(*.txt)|*.txt|All files(*.*)|*.*"
        )
        if filename:
            instrFile = open(filename, "wb")
            content = self.InstructionFile()
            if not content:
                return
            instrFile.write(content)
            instrFile.close()

    def OnLoadFile(self, event):
        """Launch file dialog and load selected file"""
        # find file
        filename = ""
        dlg = wx.FileDialog(
            self,
            message="Find instructions file",
            defaultDir="",
            defaultFile="",
            wildcard="All files (*.*)|*.*",
            style=wx.FD_CHANGE_DIR | wx.FD_OPEN,
        )
        if dlg.ShowModal() == wx.ID_OK:
            filename = dlg.GetPath()
        dlg.Destroy()
        if not filename:
            return
        # load instructions
        self.LoadFile(filename)

    def LoadFile(self, filename):
        """Load file and read instructions"""
        readObjectId = []
        readInstruction = Instruction(
            parent=self, objectsToDraw=readObjectId, env=self.env
        )
        ok = readInstruction.Read(filename)
        if not ok:
            GMessage(_("Failed to read file %s.") % filename)
        else:
            self.instruction = self.canvas.instruction = readInstruction
            self.objectId = self.canvas.objectId = readObjectId
            self.pageId = self.canvas.pageId = self.instruction.FindInstructionByType(
                "page"
            ).id
            self.canvas.UpdateMapLabel()
            self.canvas.dragId = -1
            self.canvas.Clear()
            self.canvas.SetPage()
            # self.canvas.ZoomAll()

            self.DialogDataChanged(self.objectId)

    def OnPageSetup(self, event=None):
        """Specify paper size, margins and orientation"""
        id = self.instruction.FindInstructionByType("page").id
        dlg = PageSetupDialog(self, id=id, settings=self.instruction, env=self.env)
        dlg.CenterOnParent()
        val = dlg.ShowModal()
        if val == wx.ID_OK:
            self.canvas.SetPage()
            self.getInitMap()
            self.canvas.RecalculatePosition(ids=self.objectId)
        dlg.Destroy()

    def OnPointer(self, event):
        self.mouse["use"] = "pointer"
        self.canvas.SetCursor(self.cursors["default"])
        self.previewCanvas.SetCursor(self.cursors["default"])

    def OnPan(self, event):
        self.mouse["use"] = "pan"
        self.canvas.SetCursor(self.cursors["hand"])
        self.previewCanvas.SetCursor(self.cursors["hand"])

    def OnZoomIn(self, event):
        self.mouse["use"] = "zoomin"
        self.canvas.SetCursor(self.cursors["cross"])
        self.previewCanvas.SetCursor(self.cursors["cross"])

    def OnZoomOut(self, event):
        self.mouse["use"] = "zoomout"
        self.canvas.SetCursor(self.cursors["cross"])
        self.previewCanvas.SetCursor(self.cursors["cross"])

    def OnZoomAll(self, event):
        self.mouseOld = self.mouse["use"]
        if self.currentPage == 0:
            self.cursorOld = self.canvas.GetCursor()
        else:
            self.cursorOld = self.previewCanvas.GetCursor()
            self.previewCanvas.GetCursor()
        self.mouse["use"] = "zoomin"
        if self.currentPage == 0:
            self.canvas.ZoomAll()
        else:
            self.previewCanvas.ZoomAll()
        self.mouse["use"] = self.mouseOld
        if self.currentPage == 0:
            self.canvas.SetCursor(self.cursorOld)
        else:
            self.previewCanvas.SetCursor(self.cursorOld)

    def OnAddMap(self, event, notebook=False):
        """Add or edit map frame"""
        if self.instruction.FindInstructionByType("map"):
            mapId = self.instruction.FindInstructionByType("map").id
        else:
            mapId = None
        id = [mapId, None, None]

        if notebook:
            if self.instruction.FindInstructionByType("vector"):
                vectorId = self.instruction.FindInstructionByType("vector").id
            else:
                vectorId = None
            if self.instruction.FindInstructionByType("raster"):
                rasterId = self.instruction.FindInstructionByType("raster").id
            else:
                rasterId = None
            id[1] = rasterId
            id[2] = vectorId

        if mapId:  # map exists
            self.toolbar.SelectDefault()

            if notebook:
                # check map, raster, vector and save, destroy them
                if "map" in self.openDialogs:
                    self.openDialogs["map"].OnOK(event=None)
                if "raster" in self.openDialogs:
                    self.openDialogs["raster"].OnOK(event=None)
                if "vector" in self.openDialogs:
                    self.openDialogs["vector"].OnOK(event=None)

                if "mapNotebook" not in self.openDialogs:
                    dlg = MapDialog(
                        parent=self,
                        id=id,
                        settings=self.instruction,
                        env=self.env,
                        notebook=notebook,
                    )
                    self.openDialogs["mapNotebook"] = dlg
                self.openDialogs["mapNotebook"].Show()
            else:
                if "mapNotebook" in self.openDialogs:
                    self.openDialogs["mapNotebook"].notebook.ChangeSelection(0)
                else:
                    if "map" not in self.openDialogs:
                        dlg = MapDialog(
                            parent=self,
                            id=id,
                            settings=self.instruction,
                            env=self.env,
                            notebook=notebook,
                        )
                        self.openDialogs["map"] = dlg
                    self.openDialogs["map"].Show()

        else:  # sofar no map
            self.mouse["use"] = "addMap"
            self.canvas.SetCursor(self.cursors["cross"])
            if self.currentPage == 1:
                self.book.SetSelection(0)
                self.currentPage = 0

    def OnAddRaster(self, event):
        """Add raster map"""
        if self.instruction.FindInstructionByType("raster"):
            id = self.instruction.FindInstructionByType("raster").id
        else:
            id = None

        if not self._checkMapFrameExists(type_id=id):
            return

        ##        dlg = RasterDialog(self, id = id, settings = self.instruction)
        # dlg.ShowModal()
        if "mapNotebook" in self.openDialogs:
            self.openDialogs["mapNotebook"].notebook.ChangeSelection(1)
        else:
            if "raster" not in self.openDialogs:
                dlg = RasterDialog(self, id=id, settings=self.instruction, env=self.env)
                self.openDialogs["raster"] = dlg
            self.openDialogs["raster"].Show()

    def OnAddVect(self, event):
        """Add vector map"""
        if self.instruction.FindInstructionByType("vector"):
            id = self.instruction.FindInstructionByType("vector").id
        else:
            id = None

        if not self._checkMapFrameExists(type_id=id):
            return

        ##        dlg = MainVectorDialog(self, id = id, settings = self.instruction)
        # dlg.ShowModal()
        if "mapNotebook" in self.openDialogs:
            self.openDialogs["mapNotebook"].notebook.ChangeSelection(2)
        else:
            if "vector" not in self.openDialogs:
                dlg = MainVectorDialog(
                    self, id=id, settings=self.instruction, env=self.env
                )
                self.openDialogs["vector"] = dlg
            self.openDialogs["vector"].Show()

    def OnAddScalebar(self, event):
        """Add scalebar"""
        if projInfo()["proj"] == "ll":
            GMessage(message=_("Scalebar is not appropriate for this projection"))
            return
        if self.instruction.FindInstructionByType("scalebar"):
            id = self.instruction.FindInstructionByType("scalebar").id
        else:
            id = None

        if "scalebar" not in self.openDialogs:
            dlg = ScalebarDialog(self, id=id, settings=self.instruction, env=self.env)
            self.openDialogs["scalebar"] = dlg
        self.openDialogs["scalebar"].Show()

    def OnAddLegend(self, event, page=0):
        """Add raster or vector legend"""
        if self.instruction.FindInstructionByType("rasterLegend"):
            idR = self.instruction.FindInstructionByType("rasterLegend").id
        else:
            idR = None
        if self.instruction.FindInstructionByType("vectorLegend"):
            idV = self.instruction.FindInstructionByType("vectorLegend").id
        else:
            idV = None

        if "rasterLegend" not in self.openDialogs:
            dlg = LegendDialog(
                self, id=[idR, idV], settings=self.instruction, env=self.env, page=page
            )
            self.openDialogs["rasterLegend"] = dlg
            self.openDialogs["vectorLegend"] = dlg
        self.openDialogs["rasterLegend"].notebook.ChangeSelection(page)
        self.openDialogs["rasterLegend"].Show()

    def OnAddMapinfo(self, event):
        if self.instruction.FindInstructionByType("mapinfo"):
            id = self.instruction.FindInstructionByType("mapinfo").id
        else:
            id = None

        if "mapinfo" not in self.openDialogs:
            dlg = MapinfoDialog(self, id=id, settings=self.instruction, env=self.env)
            self.openDialogs["mapinfo"] = dlg
        self.openDialogs["mapinfo"].Show()

    def OnAddImage(self, event, id=None):
        """Show dialog for image adding and editing"""
        position = None
        if "image" in self.openDialogs:
            position = self.openDialogs["image"].GetPosition()
            self.openDialogs["image"].OnApply(event=None)
            self.openDialogs["image"].Destroy()
        dlg = ImageDialog(self, id=id, settings=self.instruction, env=self.env)
        self.openDialogs["image"] = dlg
        if position:
            dlg.SetPosition(position)
        dlg.Show()

    def OnAddNorthArrow(self, event, id=None):
        """Show dialog for north arrow adding and editing"""
        if self.instruction.FindInstructionByType("northArrow"):
            id = self.instruction.FindInstructionByType("northArrow").id
        else:
            id = None

        if "northArrow" not in self.openDialogs:
            dlg = NorthArrowDialog(self, id=id, settings=self.instruction, env=self.env)
            self.openDialogs["northArrow"] = dlg
        self.openDialogs["northArrow"].Show()

    def OnAddText(self, event, id=None):
        """Show dialog for text adding and editing"""
        position = None
        if "text" in self.openDialogs:
            position = self.openDialogs["text"].GetPosition()
            self.openDialogs["text"].OnApply(event=None)
            self.openDialogs["text"].Destroy()
        dlg = TextDialog(self, id=id, settings=self.instruction, env=self.env)
        self.openDialogs["text"] = dlg
        if position:
            dlg.SetPosition(position)
        dlg.Show()

    def OnAddPoint(self, event):
        """Add point action selected"""
        self.mouse["use"] = "addPoint"
        self.canvas.SetCursor(self.cursors["cross"])
        self._switchToPage()

    def AddPoint(self, id=None, coordinates=None):
        """Add point and open property dialog.

        :param id: id point id (None if creating new point)
        :param coordinates: coordinates of new point
        """
        position = None
        if "point" in self.openDialogs:
            position = self.openDialogs["point"].GetPosition()
            self.openDialogs["point"].OnApply(event=None)
            self.openDialogs["point"].Destroy()
        dlg = PointDialog(
            self,
            id=id,
            settings=self.instruction,
            coordinates=coordinates,
            env=self.env,
        )
        self.openDialogs["point"] = dlg
        if position:
            dlg.SetPosition(position)
        if coordinates:
            dlg.OnApply(event=None)
        dlg.Show()

    def OnAddLine(self, event):
        """Add line action selected"""
        self.mouse["use"] = "addLine"
        self.canvas.SetCursor(self.cursors["cross"])
        self._switchToPage()

    def AddLine(self, id=None, coordinates=None):
        """Add line and open property dialog.

        :param id: id line id (None if creating new line)
        :param coordinates: coordinates of new line
        """
        position = None
        if "line" in self.openDialogs:
            position = self.openDialogs["line"].GetPosition()
            self.openDialogs["line"].OnApply(event=None)
            self.openDialogs["line"].Destroy()
        dlg = RectangleDialog(
            self,
            id=id,
            settings=self.instruction,
            type="line",
            coordinates=coordinates,
            env=self.env,
        )
        self.openDialogs["line"] = dlg
        if position:
            dlg.SetPosition(position)
        if coordinates:
            dlg.OnApply(event=None)
        dlg.Show()

    def OnAddRectangle(self, event):
        """Add rectangle action selected"""
        self.mouse["use"] = "addRectangle"
        self.canvas.SetCursor(self.cursors["cross"])
        self._switchToPage()

    def AddRectangle(self, id=None, coordinates=None):
        """Add rectangle and open property dialog.

        :param id: id rectangle id (None if creating new rectangle)
        :param coordinates: coordinates of new rectangle
        """
        position = None
        if "rectangle" in self.openDialogs:
            position = self.openDialogs["rectangle"].GetPosition()
            self.openDialogs["rectangle"].OnApply(event=None)
            self.openDialogs["rectangle"].Destroy()
        dlg = RectangleDialog(
            self,
            id=id,
            settings=self.instruction,
            type="rectangle",
            coordinates=coordinates,
            env=self.env,
        )
        self.openDialogs["rectangle"] = dlg
        if position:
            dlg.SetPosition(position)
        if coordinates:
            dlg.OnApply(event=None)
        dlg.Show()

    def OnAddLabels(self, event, id=None):
        """Show dialog for labels"""
        if self.instruction.FindInstructionByType("labels"):
            id = self.instruction.FindInstructionByType("labels").id
        else:
            id = None

        if not self._checkMapFrameExists(type_id=id):
            return

        if "labels" not in self.openDialogs:
            dlg = LabelsDialog(self, id=id, settings=self.instruction, env=self.env)
            self.openDialogs["labels"] = dlg
        self.openDialogs["labels"].Show()

    def getModifiedTextBounds(self, x, y, textExtent, rotation):
        """computes bounding box of rotated text, not very precisely"""
        w, h = textExtent
        rotation = float(rotation) / 180 * pi
        H = float(w) * sin(rotation)
        W = float(w) * cos(rotation)
        X, Y = x, y
        if pi / 2 < rotation <= 3 * pi / 2:
            X = x + W
        if 0 < rotation < pi:
            Y = y - H
        if rotation == 0:
            return Rect(x, y, *textExtent)
        else:
            return Rect(X, Y, abs(W), abs(H)).Inflate(h, h)

    def makePSFont(self, textDict):
        """creates a wx.Font object from selected postscript font. To be
        used for estimating bounding rectangle of text"""

        fontsize = round(textDict["fontsize"] * self.canvas.currScale)
        fontface = textDict["font"].split("-")[0]
        try:
            fontstyle = textDict["font"].split("-")[1]
        except IndexError:
            fontstyle = ""

        if fontface == "Times":
            family = wx.FONTFAMILY_ROMAN
            face = "times"
        elif fontface == "Helvetica":
            family = wx.FONTFAMILY_SWISS
            face = "helvetica"
        elif fontface == "Courier":
            family = wx.FONTFAMILY_TELETYPE
            face = "courier"
        else:
            family = wx.FONTFAMILY_DEFAULT
            face = ""

        style = wx.FONTSTYLE_NORMAL
        weight = wx.FONTWEIGHT_NORMAL

        if "Oblique" in fontstyle:
            style = wx.FONTSTYLE_SLANT

        if "Italic" in fontstyle:
            style = wx.FONTSTYLE_ITALIC

        if "Bold" in fontstyle:
            weight = wx.FONTWEIGHT_BOLD

        try:
            fn = wx.Font(
                pointSize=fontsize, family=family, style=style, weight=weight, face=face
            )
        except:
            fn = wx.Font(
                pointSize=fontsize,
                family=wx.FONTFAMILY_DEFAULT,
                style=wx.FONTSTYLE_NORMAL,
                weight=wx.FONTWEIGHT_NORMAL,
            )

        return fn

    def getTextExtent(self, textDict):
        """Estimates bounding rectangle of text"""
        # fontsize = str(fontsize if fontsize >= 4 else 4)
        # dc created because of method GetTextExtent, which pseudoDC lacks
        dc = ClientDC(self)

        fn = self.makePSFont(textDict)

        try:
            dc.SetFont(fn)
            w, h, lh = dc.GetFullMultiLineTextExtent(textDict["text"])
            return (w, h)
        except:
            return (0, 0)

    def getInitMap(self):
        """Create default map frame when no map is selected, needed for coordinates in map units"""
        instrFile = grass.tempfile()
        instrFileFd = open(instrFile, mode="wb")
        content = self.InstructionFile()
        if not content:
            return
        instrFileFd.write(content)
        instrFileFd.flush()
        instrFileFd.close()

        page = self.instruction.FindInstructionByType("page")
        mapInitRect = GetMapBounds(
            instrFile, portrait=(page["Orientation"] == "Portrait"), env=self.env
        )
        grass.try_remove(instrFile)

        region = grass.region(env=self.env)
        units = UnitConversion(self)
        realWidth = units.convert(
            value=abs(region["w"] - region["e"]), fromUnit="meter", toUnit="inch"
        )
        scale = mapInitRect.Get()[2] / realWidth

        initMap = self.instruction.FindInstructionByType("initMap")
        if initMap:
            id = initMap.id
        else:
            id = None

        if not id:
            id = NewId()
            initMap = InitMap(id, env=self.env)
            self.instruction.AddInstruction(initMap)
        self.instruction[id].SetInstruction(dict(rect=mapInitRect, scale=scale))

    def OnDelete(self, event):
        if self.canvas.dragId != -1 and self.currentPage == 0:
            if self.instruction[self.canvas.dragId].type == "map":
                self.deleteObject(self.canvas.dragId)
                self.getInitMap()
                self.canvas.RecalculateEN()
            else:
                self.deleteObject(self.canvas.dragId)

    def deleteObject(self, id):
        """Deletes object, his id and redraws"""
        # delete from canvas
        self.canvas.pdcObj.RemoveId(id)
        if id == self.canvas.dragId:
            self.canvas.pdcTmp.RemoveAll()
            self.canvas.dragId = -1
        self.canvas.Refresh()

        # delete from instructions
        del self.instruction[id]

    def DialogDataChanged(self, id):
        ids = id
        if isinstance(id, int):
            ids = [id]
        for id in ids:
            itype = self.instruction[id].type

            if itype in ("scalebar", "mapinfo", "image"):
                drawRectangle = self.canvas.CanvasPaperCoordinates(
                    rect=self.instruction[id]["rect"], canvasToPaper=False
                )
                self.canvas.UpdateLabel(itype=itype, id=id)
                self.canvas.Draw(
                    pen=self.pen[itype],
                    brush=self.brush[itype],
                    pdc=self.canvas.pdcObj,
                    drawid=id,
                    pdctype="rectText",
                    bb=drawRectangle,
                )
                self.canvas.RedrawSelectBox(id)
            if itype == "northArrow":
                self.canvas.UpdateLabel(itype=itype, id=id)
                drawRectangle = self.canvas.CanvasPaperCoordinates(
                    rect=self.instruction[id]["rect"], canvasToPaper=False
                )
                self.canvas.Draw(
                    pen=self.pen[itype],
                    brush=self.brush[itype],
                    pdc=self.canvas.pdcObj,
                    drawid=id,
                    pdctype="bitmap",
                    bb=drawRectangle,
                )
                self.canvas.RedrawSelectBox(id)

            if itype in ("point", "line", "rectangle"):
                drawRectangle = self.canvas.CanvasPaperCoordinates(
                    rect=self.instruction[id]["rect"], canvasToPaper=False
                )
                # coords only for line
                coords = None
                if itype == "line":
                    point1 = self.instruction[id]["where"][0]
                    point2 = self.instruction[id]["where"][1]
                    point1Coords = self.canvas.CanvasPaperCoordinates(
                        rect=Rect2DPS(point1, (0, 0)), canvasToPaper=False
                    )[:2]
                    point2Coords = self.canvas.CanvasPaperCoordinates(
                        rect=Rect2DPS(point2, (0, 0)), canvasToPaper=False
                    )[:2]
                    coords = (point1Coords, point2Coords)

                # fill color is not in line
                fcolor = None
                if "fcolor" in self.instruction[id].GetInstruction():
                    fcolor = self.instruction[id]["fcolor"]
                # width is not in point
                width = None
                if "width" in self.instruction[id].GetInstruction():
                    width = self.instruction[id]["width"]

                self.canvas.DrawGraphics(
                    drawid=id,
                    color=self.instruction[id]["color"],
                    shape=itype,
                    fcolor=fcolor,
                    width=width,
                    bb=drawRectangle,
                    lineCoords=coords,
                )

                self.canvas.RedrawSelectBox(id)

            if itype == "text":
                if self.instruction[id]["rotate"]:
                    rot = float(self.instruction[id]["rotate"])
                else:
                    rot = 0

                extent = self.getTextExtent(
                    textDict=self.instruction[id].GetInstruction()
                )
                rect = Rect2DPS(self.instruction[id]["where"], (0, 0))
                self.instruction[id]["coords"] = list(
                    self.canvas.CanvasPaperCoordinates(rect=rect, canvasToPaper=False)[
                        :2
                    ]
                )

                # computes text coordinates according to reference point, not
                # precisely
                if self.instruction[id]["ref"].split()[0] == "lower":
                    self.instruction[id]["coords"][1] -= extent[1]
                elif self.instruction[id]["ref"].split()[0] == "center":
                    self.instruction[id]["coords"][1] -= extent[1] / 2
                if self.instruction[id]["ref"].split()[1] == "right":
                    self.instruction[id]["coords"][0] -= extent[0] * cos(rot / 180 * pi)
                    self.instruction[id]["coords"][1] += extent[0] * sin(rot / 180 * pi)
                elif self.instruction[id]["ref"].split()[1] == "center":
                    self.instruction[id]["coords"][0] -= (
                        extent[0] / 2 * cos(rot / 180 * pi)
                    )
                    self.instruction[id]["coords"][1] += (
                        extent[0] / 2 * sin(rot / 180 * pi)
                    )

                self.instruction[id]["coords"][0] += self.instruction[id]["xoffset"]
                self.instruction[id]["coords"][1] -= self.instruction[id]["yoffset"]
                coords = self.instruction[id]["coords"]
                self.instruction[id]["rect"] = bounds = self.getModifiedTextBounds(
                    coords[0], coords[1], extent, rot
                )
                self.canvas.DrawRotText(
                    pdc=self.canvas.pdcObj,
                    drawId=id,
                    textDict=self.instruction[id].GetInstruction(),
                    coords=coords,
                    bounds=bounds,
                )
                self.canvas.RedrawSelectBox(id)

            if itype in ("map", "vector", "raster", "labels"):
                if itype == "raster":  # set resolution
                    try:
                        info = grass.raster_info(self.instruction[id]["raster"])
                        self.env["GRASS_REGION"] = grass.region_env(
                            nsres=info["nsres"], ewres=info["ewres"], env=self.env
                        )
                    except grass.CalledModuleError:  # fails after switching location
                        pass
                    # change current raster in raster legend

                if "rasterLegend" in self.openDialogs:
                    self.openDialogs["rasterLegend"].updateDialog()
                id = self.instruction.FindInstructionByType("map").id

                # check resolution
                if itype == "raster":
                    SetResolution(
                        dpi=self.instruction[id]["resolution"],
                        width=self.instruction[id]["rect"].width,
                        height=self.instruction[id]["rect"].height,
                        env=self.env,
                    )
                rectCanvas = self.canvas.CanvasPaperCoordinates(
                    rect=self.instruction[id]["rect"], canvasToPaper=False
                )
                self.canvas.RecalculateEN()
                self.canvas.UpdateMapLabel()

                self.canvas.Draw(
                    pen=self.pen["map"],
                    brush=self.brush["map"],
                    pdc=self.canvas.pdcObj,
                    drawid=id,
                    pdctype="rectText",
                    bb=rectCanvas,
                )
                # redraw select box
                self.canvas.RedrawSelectBox(id)
                self.canvas.pdcTmp.RemoveId(self.canvas.idZoomBoxTmp)
                # redraw to get map to the bottom layer
                # self.canvas.Zoom(zoomFactor = 1, view = (0, 0))

            if itype == "rasterLegend":
                if self.instruction[id]["rLegend"]:
                    self.canvas.UpdateLabel(itype=itype, id=id)
                    drawRectangle = self.canvas.CanvasPaperCoordinates(
                        rect=self.instruction[id]["rect"], canvasToPaper=False
                    )
                    self.canvas.Draw(
                        pen=self.pen[itype],
                        brush=self.brush[itype],
                        pdc=self.canvas.pdcObj,
                        drawid=id,
                        pdctype="rectText",
                        bb=drawRectangle,
                    )
                    self.canvas.RedrawSelectBox(id)
                else:
                    self.deleteObject(id)

            if itype == "vectorLegend":
                if not self.instruction.FindInstructionByType("vector"):
                    self.deleteObject(id)
                elif self.instruction[id]["vLegend"]:
                    self.canvas.UpdateLabel(itype=itype, id=id)
                    drawRectangle = self.canvas.CanvasPaperCoordinates(
                        rect=self.instruction[id]["rect"], canvasToPaper=False
                    )
                    self.canvas.Draw(
                        pen=self.pen[itype],
                        brush=self.brush[itype],
                        pdc=self.canvas.pdcObj,
                        drawid=id,
                        pdctype="rectText",
                        bb=drawRectangle,
                    )
                    self.canvas.RedrawSelectBox(id)

                else:
                    self.deleteObject(id)

    def OnPageChanged(self, event):
        """GNotebook page has changed"""
        self.currentPage = self.book.GetPageIndex(self.book.GetCurrentPage())
        if self.currentPage == 1:
            self.SetStatusText(
                _("Press button with green triangle icon to generate preview.")
            )
        else:
            self.SetStatusText("")

    def OnHelp(self, event):
        """Show help"""
        if self.parent and self.parent.GetName() == "LayerManager":
            log = self.parent.GetLogWindow()
            log.RunCmd(["g.manual", "entry=wxGUI.psmap"])
        else:
            RunCommand("g.manual", quiet=True, entry="wxGUI.psmap")

    def OnAbout(self, event):
        """Display About window"""
        ShowAboutDialog(prgName=_("wxGUI Cartographic Composer"), startYear="2011")

    def OnCloseWindow(self, event):
        """Close window"""
        try:
            os.remove(self.imgName)
        except OSError:
            pass
        grass.set_raise_on_error(False)
        if hasattr(self, "delayedCall") and self.delayedCall.IsRunning():
            self.delayedCall.Stop()
        self.Destroy()


class PsMapBufferedWindow(wx.Window):
    """A buffered window class."""

    def __init__(
        self, parent, id=wx.ID_ANY, style=wx.NO_FULL_REPAINT_ON_RESIZE, **kwargs
    ):
        """
        :param parent: parent window
        :param kwargs: other wx.Window parameters
        """
        wx.Window.__init__(self, parent, id=id, style=style)
        self.parent = parent

        self.FitInside()

        # store an off screen empty bitmap for saving to file
        self._buffer = None
        # indicates whether or not a resize event has taken place
        self.resize = False

        self.mouse = kwargs["mouse"]
        self.cursors = kwargs["cursors"]
        self.preview = kwargs["preview"]
        self.pen = kwargs["pen"]
        self.brush = kwargs["brush"]

        if "instruction" in kwargs:
            self.instruction = kwargs["instruction"]
        if "openDialogs" in kwargs:
            self.openDialogs = kwargs["openDialogs"]
        if "pageId" in kwargs:
            self.pageId = kwargs["pageId"]
        if "objectId" in kwargs:
            self.objectId = kwargs["objectId"]
        if "env" in kwargs:
            self.env = kwargs["env"]
        # labels
        self.itemLabelsDict = {
            "map": _("MAP FRAME"),
            "rasterLegend": _("RASTER LEGEND"),
            "vectorLegend": _("VECTOR LEGEND"),
            "mapinfo": _("MAP INFO"),
            "scalebar": _("SCALE BAR"),
            "image": _("IMAGE"),
            "northArrow": _("NORTH ARROW"),
        }
        self.itemLabels = {}

        # define PseudoDC
        self.pdc = PseudoDC()
        self.pdcObj = PseudoDC()
        self.pdcPaper = PseudoDC()
        self.pdcTmp = PseudoDC()
        self.pdcImage = PseudoDC()

        self.SetClientSize((700, 510))  # ?
        self._buffer = EmptyBitmap(*self.GetClientSize())

        self.idBoxTmp = NewId()
        self.idZoomBoxTmp = NewId()
        self.idResizeBoxTmp = NewId()
        # ids of marks for moving line vertices
        self.idLinePointsTmp = (NewId(), NewId())

        self.resizeBoxSize = wx.Size(8, 8)
        self.showResizeHelp = False  # helper for correctly working statusbar

        self.dragId = -1

        if self.preview:
            self.image = None
            self.imageId = 2000
            self.imgName = self.parent.imgName

        self.currScale = None

        self.Clear()

        self.Bind(wx.EVT_ERASE_BACKGROUND, lambda x: None)

        self.Bind(wx.EVT_PAINT, self.OnPaint)
        self.Bind(wx.EVT_SIZE, self.OnSize)
        self.Bind(wx.EVT_IDLE, self.OnIdle)
        # self.Bind(wx.EVT_MOUSE_EVENTS, self.OnMouse)
        self.Bind(wx.EVT_MOUSE_EVENTS, self.MouseActions)

    def Clear(self):
        """Clear canvas and set paper"""
        bg = wx.LIGHT_GREY_BRUSH
        self.pdcPaper.BeginDrawing()
        self.pdcPaper.SetBackground(bg)
        self.pdcPaper.Clear()
        self.pdcPaper.EndDrawing()

        self.pdcObj.RemoveAll()
        self.pdcTmp.RemoveAll()

        if not self.preview:
            self.SetPage()

    def CanvasPaperCoordinates(self, rect, canvasToPaper=True):
        """Converts canvas (pixel) -> paper (inch) coordinates and size and vice versa"""

        units = UnitConversion(self)

        fromU = "pixel"
        toU = "inch"
        pRect = self.pdcPaper.GetIdBounds(self.pageId)
        pRectx, pRecty = pRect.x, pRect.y
        scale = 1 / self.currScale
        if not canvasToPaper:  # paper -> canvas
            fromU = "inch"
            toU = "pixel"
            scale = self.currScale
            pRectx = (
                units.convert(value=-pRect.x, fromUnit="pixel", toUnit="inch") / scale
            )  # inch, real, negative
            pRecty = (
                units.convert(value=-pRect.y, fromUnit="pixel", toUnit="inch") / scale
            )
        Width = units.convert(value=rect.GetWidth(), fromUnit=fromU, toUnit=toU) * scale
        Height = (
            units.convert(value=rect.GetHeight(), fromUnit=fromU, toUnit=toU) * scale
        )
        X = (
            units.convert(value=(rect.GetX() - pRectx), fromUnit=fromU, toUnit=toU)
            * scale
        )
        Y = (
            units.convert(value=(rect.GetY() - pRecty), fromUnit=fromU, toUnit=toU)
            * scale
        )
        if canvasToPaper:
            return Rect2D(X, Y, Width, Height)
        return Rect2D(int(X), int(Y), int(Width), int(Height))

    def SetPage(self):
        """Sets and changes page, redraws paper"""

        page = self.instruction[self.pageId]
        if not page:
            page = PageSetup(id=self.pageId, env=self.env)
            self.instruction.AddInstruction(page)

        ppi = wx.ClientDC(self).GetPPI()
        cW, cH = self.GetClientSize()
        pW, pH = page["Width"] * ppi[0], page["Height"] * ppi[1]

        if self.currScale is None:
            self.currScale = min(cW / pW, cH / pH)
        pW = pW * self.currScale
        pH = pH * self.currScale

        x = cW / 2 - pW / 2
        y = cH / 2 - pH / 2
        self.DrawPaper(Rect(int(x), int(y), int(pW), int(pH)))

    def modifyRectangle(self, r):
        """Recalculates rectangle not to have negative size"""
        if r.GetWidth() < 0:
            r.SetX(r.GetX() + r.GetWidth())
        if r.GetHeight() < 0:
            r.SetY(r.GetY() + r.GetHeight())
        r.SetWidth(abs(r.GetWidth()))
        r.SetHeight(abs(r.GetHeight()))
        return r

    def RecalculateEN(self):
        """Recalculate east and north for texts (eps, points) after their or map's movement"""
        try:
            mapId = self.instruction.FindInstructionByType("map").id
        except AttributeError:
            mapId = self.instruction.FindInstructionByType("initMap").id

        for itemType in ("text", "image", "northArrow", "point", "line", "rectangle"):
            items = self.instruction.FindInstructionByType(itemType, list=True)
            for item in items:
                instr = self.instruction[item.id]
                if itemType in ("line", "rectangle"):
                    if itemType == "line":
                        e1, n1 = PaperMapCoordinates(
                            mapInstr=self.instruction[mapId],
                            x=instr["where"][0][0],
                            y=instr["where"][0][1],
                            paperToMap=True,
                            env=self.env,
                        )
                        e2, n2 = PaperMapCoordinates(
                            mapInstr=self.instruction[mapId],
                            x=instr["where"][1][0],
                            y=instr["where"][1][1],
                            paperToMap=True,
                            env=self.env,
                        )
                    else:
                        e1, n1 = PaperMapCoordinates(
                            mapInstr=self.instruction[mapId],
                            x=instr["rect"].GetLeft(),
                            y=instr["rect"].GetTop(),
                            paperToMap=True,
                            env=self.env,
                        )
                        e2, n2 = PaperMapCoordinates(
                            mapInstr=self.instruction[mapId],
                            x=instr["rect"].GetRight(),
                            y=instr["rect"].GetBottom(),
                            paperToMap=True,
                            env=self.env,
                        )
                    instr["east1"] = e1
                    instr["north1"] = n1
                    instr["east2"] = e2
                    instr["north2"] = n2
                else:
                    e, n = PaperMapCoordinates(
                        mapInstr=self.instruction[mapId],
                        x=instr["where"][0],
                        y=instr["where"][1],
                        paperToMap=True,
                        env=self.env,
                    )
                    instr["east"], instr["north"] = e, n

    def OnPaint(self, event):
        """Draw pseudo DC to buffer"""
        if not self._buffer:
            return
        dc = wx.BufferedPaintDC(self, self._buffer)
        # use PrepareDC to set position correctly
        # probably does nothing, removed from wxPython 2.9
        # self.PrepareDC(dc)

        dc.SetBackground(wx.LIGHT_GREY_BRUSH)
        dc.Clear()

        # draw paper
        if not self.preview:
            self.pdcPaper.DrawToDC(dc)
        # draw to the DC using the calculated clipping rect

        rgn = self.GetUpdateRegion()

        if not self.preview:
            self.pdcObj.DrawToDCClipped(dc, rgn.GetBox())
        else:
            self.pdcImage.DrawToDCClipped(dc, rgn.GetBox())
        self.pdcTmp.DrawToDCClipped(dc, rgn.GetBox())

    def MouseActions(self, event):
        """Mouse motion and button click notifier"""
        disable = self.preview and self.mouse["use"] in (
            "pointer",
            "resize",
            "addMap",
            "addPoint",
            "addLine",
            "addRectangle",
        )
        # zoom with mouse wheel
        if event.GetWheelRotation() != 0:
            self.OnMouseWheel(event)

        # left mouse button pressed
        elif event.LeftDown() and not disable:
            self.OnLeftDown(event)

        # left mouse button released
        elif event.LeftUp() and not disable:
            self.OnLeftUp(event)

        # dragging
        elif event.Dragging() and not disable:
            self.OnDragging(event)

        # double click
        elif event.ButtonDClick() and not disable:
            self.OnButtonDClick(event)

        # middle mouse button pressed
        elif event.MiddleDown():
            self.OnMiddleDown(event)

        elif event.Moving():
            self.OnMouseMoving(event)

    def OnMouseWheel(self, event):
        """Mouse wheel scrolled.

        Changes zoom."""
        if (
            UserSettings.Get(group="display", key="mouseWheelZoom", subkey="selection")
            == 2
        ):
            event.Skip()
            return

        zoom = event.GetWheelRotation()
        oldUse = self.mouse["use"]
        self.mouse["begin"] = event.GetPosition()

        if UserSettings.Get(group="display", key="scrollDirection", subkey="selection"):
            zoom *= -1

        if zoom > 0:
            self.mouse["use"] = "zoomin"
        else:
            self.mouse["use"] = "zoomout"

        zoomFactor, view = self.ComputeZoom(Rect(0, 0, 0, 0))
        self.Zoom(zoomFactor, view)
        self.mouse["use"] = oldUse

    def OnMouseMoving(self, event):
        """Mouse cursor moving.

        Change cursor when moving over resize marker.
        """
        if self.preview:
            return

        if self.mouse["use"] in ("pointer", "resize"):
            pos = event.GetPosition()
            foundResize = self.pdcTmp.FindObjects(pos[0], pos[1])
            if (
                foundResize
                and foundResize[0] in (self.idResizeBoxTmp,) + self.idLinePointsTmp
            ):
                self.SetCursor(self.cursors["sizenwse"])
                self.parent.SetStatusText(_("Click and drag to resize object"), 0)
                self.showResizeHelp = True
            else:
                if self.showResizeHelp:
                    self.parent.SetStatusText("", 0)
                    self.SetCursor(self.cursors["default"])
                    self.showResizeHelp = False

    def OnLeftDown(self, event):
        """Left mouse button pressed.

        Select objects, redraw, prepare for moving/resizing.
        """
        self.mouse["begin"] = event.GetPosition()
        self.begin = self.mouse["begin"]

        # select
        if self.mouse["use"] == "pointer":
            found = self.pdcObj.FindObjects(
                self.mouse["begin"][0], self.mouse["begin"][1]
            )
            foundResize = self.pdcTmp.FindObjects(
                self.mouse["begin"][0], self.mouse["begin"][1]
            )

            if (
                foundResize
                and foundResize[0] in (self.idResizeBoxTmp,) + self.idLinePointsTmp
            ):
                self.mouse["use"] = "resize"

                # when resizing, proportions match region
                if self.instruction[self.dragId].type == "map":
                    self.constraint = False
                    self.mapBounds = self.pdcObj.GetIdBounds(self.dragId)
                    if self.instruction[self.dragId]["scaleType"] in (0, 1, 2):
                        self.constraint = True
                        self.mapBounds = self.pdcObj.GetIdBounds(self.dragId)

                if self.instruction[self.dragId].type == "line":
                    self.currentLinePoint = self.idLinePointsTmp.index(foundResize[0])

            elif found:
                self.dragId = found[0]
                self.RedrawSelectBox(self.dragId)
                if self.instruction[self.dragId].type not in ("map", "rectangle"):
                    self.pdcTmp.RemoveId(self.idResizeBoxTmp)
                    self.Refresh()
                if self.instruction[self.dragId].type != "line":
                    for id in self.idLinePointsTmp:
                        self.pdcTmp.RemoveId(id)
                    self.Refresh()

            else:
                self.dragId = -1
                self.pdcTmp.RemoveId(self.idBoxTmp)
                self.pdcTmp.RemoveId(self.idResizeBoxTmp)
                for id in self.idLinePointsTmp:
                    self.pdcTmp.RemoveId(id)
                self.Refresh()

    def OnLeftUp(self, event):
        """Left mouse button released.

        Recalculate zooming/resizing/moving and redraw.
        """
        # zoom in, zoom out
        if self.mouse["use"] in ("zoomin", "zoomout"):
            zoomR = self.pdcTmp.GetIdBounds(self.idZoomBoxTmp)
            self.pdcTmp.RemoveId(self.idZoomBoxTmp)
            self.Refresh()
            zoomFactor, view = self.ComputeZoom(zoomR)
            self.Zoom(zoomFactor, view)

        # draw map frame
        if self.mouse["use"] == "addMap":
            rectTmp = self.pdcTmp.GetIdBounds(self.idZoomBoxTmp)
            # too small rectangle, it's usually some mistake
            if rectTmp.GetWidth() < 20 or rectTmp.GetHeight() < 20:
                self.pdcTmp.RemoveId(self.idZoomBoxTmp)
                self.Refresh()
                return
            rectPaper = self.CanvasPaperCoordinates(rect=rectTmp, canvasToPaper=True)

            dlg = MapDialog(
                parent=self.parent,
                id=[None, None, None],
                settings=self.instruction,
                env=self.env,
                rect=rectPaper,
            )
            self.openDialogs["map"] = dlg
            self.openDialogs["map"].Show()

            self.parent.toolbar.SelectDefault()
            return

        # resize resizable objects (map, line, rectangle)
        if self.mouse["use"] == "resize":
            mapObj = self.instruction.FindInstructionByType("map")
            if not mapObj:
                mapObj = self.instruction.FindInstructionByType("initMap")
            mapId = mapObj.id

            if self.dragId == mapId:
                # necessary to change either map frame (scaleType 0,1,2) or
                # region (scaletype 3)
                newRectCanvas = self.pdcObj.GetIdBounds(mapId)
                newRectPaper = self.CanvasPaperCoordinates(
                    rect=newRectCanvas, canvasToPaper=True
                )
                self.instruction[mapId]["rect"] = newRectPaper

                if self.instruction[mapId]["scaleType"] in (0, 1, 2):
                    if self.instruction[mapId]["scaleType"] == 0:
                        scale, foo, rect = AutoAdjust(
                            self,
                            scaleType=0,
                            map=self.instruction[mapId]["map"],
                            env=self.env,
                            mapType=self.instruction[mapId]["mapType"],
                            rect=self.instruction[mapId]["rect"],
                        )

                    elif self.instruction[mapId]["scaleType"] == 1:
                        scale, foo, rect = AutoAdjust(
                            self,
                            scaleType=1,
                            env=self.env,
                            region=self.instruction[mapId]["region"],
                            rect=self.instruction[mapId]["rect"],
                        )
                    else:
                        scale, foo, rect = AutoAdjust(
                            self,
                            scaleType=2,
                            rect=self.instruction[mapId]["rect"],
                            env=self.env,
                        )
                    self.instruction[mapId]["rect"] = rect
                    self.instruction[mapId]["scale"] = scale

                    rectCanvas = self.CanvasPaperCoordinates(
                        rect=rect, canvasToPaper=False
                    )
                    self.Draw(
                        pen=self.pen["map"],
                        brush=self.brush["map"],
                        pdc=self.pdcObj,
                        drawid=mapId,
                        pdctype="rectText",
                        bb=rectCanvas,
                    )

                elif self.instruction[mapId]["scaleType"] == 3:
                    ComputeSetRegion(
                        self,
                        mapDict=self.instruction[mapId].GetInstruction(),
                        env=self.env,
                    )
                # check resolution
                SetResolution(
                    dpi=self.instruction[mapId]["resolution"],
                    width=self.instruction[mapId]["rect"].width,
                    height=self.instruction[mapId]["rect"].height,
                    env=self.env,
                )

                self.RedrawSelectBox(mapId)
                self.Zoom(zoomFactor=1, view=(0, 0))

            elif self.instruction[self.dragId].type == "line":
                points = self.instruction[self.dragId]["where"]
                self.instruction[self.dragId]["rect"] = Rect2DPP(points[0], points[1])
                self.RecalculatePosition(ids=[self.dragId])

            elif self.instruction[self.dragId].type == "rectangle":
                self.RecalculatePosition(ids=[self.dragId])

            self.mouse["use"] = "pointer"

        # recalculate the position of objects after dragging
        if self.mouse["use"] in ("pointer", "resize") and self.dragId != -1:
            if self.mouse["begin"] != event.GetPosition():  # for double click
                self.RecalculatePosition(ids=[self.dragId])
                if self.instruction[self.dragId].type in self.openDialogs:
                    self.openDialogs[self.instruction[self.dragId].type].updateDialog()

        elif self.mouse["use"] in ("addPoint", "addLine", "addRectangle"):
            endCoordinates = self.CanvasPaperCoordinates(
                rect=Rect(event.GetX(), event.GetY(), 0, 0), canvasToPaper=True
            )[:2]

            diffX = event.GetX() - self.mouse["begin"][0]
            diffY = event.GetY() - self.mouse["begin"][1]

            if self.mouse["use"] == "addPoint":
                self.parent.AddPoint(coordinates=endCoordinates)
            elif self.mouse["use"] in ("addLine", "addRectangle"):
                # not too small lines/rectangles
                if sqrt(diffX * diffX + diffY * diffY) < 5:
                    self.pdcTmp.RemoveId(self.idZoomBoxTmp)
                    self.Refresh()
                    return

                beginCoordinates = self.CanvasPaperCoordinates(
                    rect=Rect(self.mouse["begin"][0], self.mouse["begin"][1], 0, 0),
                    canvasToPaper=True,
                )[:2]
                if self.mouse["use"] == "addLine":
                    self.parent.AddLine(coordinates=[beginCoordinates, endCoordinates])
                else:
                    self.parent.AddRectangle(
                        coordinates=[beginCoordinates, endCoordinates]
                    )
                self.pdcTmp.RemoveId(self.idZoomBoxTmp)
                self.Refresh()

    def OnButtonDClick(self, event):
        """Open object dialog for editing."""
        if self.mouse["use"] == "pointer" and self.dragId != -1:
            itemCall = {
                "text": self.parent.OnAddText,
                "mapinfo": self.parent.OnAddMapinfo,
                "scalebar": self.parent.OnAddScalebar,
                "image": self.parent.OnAddImage,
                "northArrow": self.parent.OnAddNorthArrow,
                "point": self.parent.AddPoint,
                "line": self.parent.AddLine,
                "rectangle": self.parent.AddRectangle,
                "rasterLegend": self.parent.OnAddLegend,
                "vectorLegend": self.parent.OnAddLegend,
                "map": self.parent.OnAddMap,
            }

            itemArg = {
                "text": dict(event=None, id=self.dragId),
                "mapinfo": dict(event=None),
                "scalebar": dict(event=None),
                "image": dict(event=None, id=self.dragId),
                "northArrow": dict(event=None, id=self.dragId),
                "point": dict(id=self.dragId),
                "line": dict(id=self.dragId),
                "rectangle": dict(id=self.dragId),
                "rasterLegend": dict(event=None),
                "vectorLegend": dict(event=None, page=1),
                "map": dict(event=None, notebook=True),
            }

            type = self.instruction[self.dragId].type
            itemCall[type](**itemArg[type])

    def OnDragging(self, event):
        """Process panning/resizing/drawing/moving."""
        if event.MiddleIsDown():
            # panning
            self.mouse["end"] = event.GetPosition()
            self.Pan(begin=self.mouse["begin"], end=self.mouse["end"])
            self.mouse["begin"] = event.GetPosition()

        elif event.LeftIsDown():
            # draw box when zooming, creating map
            if self.mouse["use"] in (
                "zoomin",
                "zoomout",
                "addMap",
                "addLine",
                "addRectangle",
            ):
                self.mouse["end"] = event.GetPosition()
                r = Rect(
                    self.mouse["begin"][0],
                    self.mouse["begin"][1],
                    self.mouse["end"][0] - self.mouse["begin"][0],
                    self.mouse["end"][1] - self.mouse["begin"][1],
                )
                r = self.modifyRectangle(r)

                if self.mouse["use"] in ("addLine", "addRectangle"):
                    if self.mouse["use"] == "addLine":
                        pdcType = "line"
                        lineCoords = (self.mouse["begin"], self.mouse["end"])
                    else:
                        pdcType = "rect"
                        lineCoords = None
                        if r[2] < 2 or r[3] < 2:
                            # to avoid strange behaviour
                            return

                    self.Draw(
                        pen=self.pen["line"],
                        brush=self.brush["line"],
                        pdc=self.pdcTmp,
                        drawid=self.idZoomBoxTmp,
                        pdctype=pdcType,
                        bb=r,
                        lineCoords=lineCoords,
                    )

                else:
                    self.Draw(
                        pen=self.pen["box"],
                        brush=self.brush["box"],
                        pdc=self.pdcTmp,
                        drawid=self.idZoomBoxTmp,
                        pdctype="rect",
                        bb=r,
                    )

            # panning
            if self.mouse["use"] == "pan":
                self.mouse["end"] = event.GetPosition()
                self.Pan(begin=self.mouse["begin"], end=self.mouse["end"])
                self.mouse["begin"] = event.GetPosition()

            # move object
            if self.mouse["use"] == "pointer" and self.dragId != -1:
                self.mouse["end"] = event.GetPosition()
                dx, dy = (
                    self.mouse["end"][0] - self.begin[0],
                    self.mouse["end"][1] - self.begin[1],
                )
                self.pdcObj.TranslateId(self.dragId, dx, dy)
                self.pdcTmp.TranslateId(self.idBoxTmp, dx, dy)
                self.pdcTmp.TranslateId(self.idResizeBoxTmp, dx, dy)
                for id in self.idLinePointsTmp:
                    self.pdcTmp.TranslateId(id, dx, dy)
                if self.instruction[self.dragId].type == "text":
                    self.instruction[self.dragId]["coords"] = (
                        self.instruction[self.dragId]["coords"][0] + dx,
                        self.instruction[self.dragId]["coords"][1] + dy,
                    )
                self.begin = event.GetPosition()
                self.Refresh()

            # resize object
            if self.mouse["use"] == "resize":
                pos = event.GetPosition()
                diffX = pos[0] - self.mouse["begin"][0]
                diffY = pos[1] - self.mouse["begin"][1]
                if self.instruction[self.dragId].type == "map":
                    x, y = self.mapBounds.GetX(), self.mapBounds.GetY()
                    width, height = (
                        self.mapBounds.GetWidth(),
                        self.mapBounds.GetHeight(),
                    )
                    # match given region
                    if self.constraint:
                        if width > height:
                            newWidth = width + diffX
                            newHeight = height + diffX * (float(height) / width)
                        else:
                            newWidth = width + diffY * (float(width) / height)
                            newHeight = height + diffY
                    else:
                        newWidth = width + diffX
                        newHeight = height + diffY

                    if newWidth < 10 or newHeight < 10:
                        return

                    bounds = Rect(x, y, newWidth, newHeight)
                    self.Draw(
                        pen=self.pen["map"],
                        brush=self.brush["map"],
                        pdc=self.pdcObj,
                        drawid=self.dragId,
                        pdctype="rectText",
                        bb=bounds,
                    )

                elif self.instruction[self.dragId].type == "rectangle":
                    instr = self.instruction[self.dragId]
                    rect = self.CanvasPaperCoordinates(
                        rect=instr["rect"], canvasToPaper=False
                    )
                    rect.SetWidth(rect.GetWidth() + diffX)
                    rect.SetHeight(rect.GetHeight() + diffY)

                    if rect.GetWidth() < 5 or rect.GetHeight() < 5:
                        return

                    self.DrawGraphics(
                        drawid=self.dragId,
                        shape="rectangle",
                        color=instr["color"],
                        fcolor=instr["fcolor"],
                        width=instr["width"],
                        bb=rect,
                    )

                elif self.instruction[self.dragId].type == "line":
                    instr = self.instruction[self.dragId]
                    points = instr["where"]
                    # moving point
                    if self.currentLinePoint == 0:
                        pPaper = points[1]
                    else:
                        pPaper = points[0]
                    pCanvas = self.CanvasPaperCoordinates(
                        rect=Rect2DPS(pPaper, (0, 0)), canvasToPaper=False
                    )[:2]
                    bounds = wx.Rect(pCanvas, pos)
                    self.DrawGraphics(
                        drawid=self.dragId,
                        shape="line",
                        color=instr["color"],
                        width=instr["width"],
                        bb=bounds,
                        lineCoords=(pos, pCanvas),
                    )

                    # update paper coordinates
                    points[self.currentLinePoint] = self.CanvasPaperCoordinates(
                        rect=Rect(pos[0], pos[1], 0, 0), canvasToPaper=True
                    )[:2]

                self.RedrawSelectBox(self.dragId)

    def OnMiddleDown(self, event):
        """Middle mouse button pressed."""
        self.mouse["begin"] = event.GetPosition()

    def Pan(self, begin, end):
        """Move canvas while dragging.

        :param begin: x,y coordinates of first point
        :param end: x,y coordinates of second point
        """
        view = begin[0] - end[0], begin[1] - end[1]
        zoomFactor = 1
        self.Zoom(zoomFactor, view)

    def RecalculatePosition(self, ids):
        for id in ids:
            itype = self.instruction[id].type
            if itype in ("map", "rectangle"):
                self.instruction[id]["rect"] = self.CanvasPaperCoordinates(
                    rect=self.pdcObj.GetIdBounds(id), canvasToPaper=True
                )
                self.RecalculateEN()

            elif itype in (
                "mapinfo",
                "rasterLegend",
                "vectorLegend",
                "image",
                "northArrow",
            ):
                self.instruction[id]["rect"] = self.CanvasPaperCoordinates(
                    rect=self.pdcObj.GetIdBounds(id), canvasToPaper=True
                )
                self.instruction[id]["where"] = self.CanvasPaperCoordinates(
                    rect=self.pdcObj.GetIdBounds(id), canvasToPaper=True
                )[:2]
                if itype in ("image", "northArrow"):
                    self.RecalculateEN()

            elif itype == "point":
                rect = self.pdcObj.GetIdBounds(id)
                self.instruction[id]["rect"] = self.CanvasPaperCoordinates(
                    rect=rect, canvasToPaper=True
                )
                rect.Offset(
                    dx=int(rect.GetWidth() / 2),
                    dy=int(rect.GetHeight() / 2),
                )
                self.instruction[id]["where"] = self.CanvasPaperCoordinates(
                    rect=rect, canvasToPaper=True
                )[:2]
                self.RecalculateEN()

            elif itype == "line":
                rect = self.pdcObj.GetIdBounds(id)
                oldRect = self.instruction[id]["rect"]
                newRect = self.CanvasPaperCoordinates(rect=rect, canvasToPaper=True)
                xDiff = newRect[0] - oldRect[0]
                yDiff = newRect[1] - oldRect[1]
                self.instruction[id]["rect"] = newRect

                point1 = wx.Point2D(*self.instruction[id]["where"][0])
                point2 = wx.Point2D(*self.instruction[id]["where"][1])
                point1 += wx.Point2D(xDiff, yDiff)
                point2 += wx.Point2D(xDiff, yDiff)
                self.instruction[id]["where"] = [point1, point2]

                self.RecalculateEN()

            elif itype == "scalebar":
                self.instruction[id]["rect"] = self.CanvasPaperCoordinates(
                    rect=self.pdcObj.GetIdBounds(id), canvasToPaper=True
                )

                self.instruction[id]["where"] = self.instruction[id]["rect"].GetCentre()

            elif itype == "text":
                x, y = (
                    self.instruction[id]["coords"][0] - self.instruction[id]["xoffset"],
                    self.instruction[id]["coords"][1] + self.instruction[id]["yoffset"],
                )
                extent = self.parent.getTextExtent(textDict=self.instruction[id])
                if self.instruction[id]["rotate"] is not None:
                    rot = float(self.instruction[id]["rotate"]) / 180 * pi
                else:
                    rot = 0

                if self.instruction[id]["ref"].split()[0] == "lower":
                    y += extent[1]
                elif self.instruction[id]["ref"].split()[0] == "center":
                    y += extent[1] / 2
                if self.instruction[id]["ref"].split()[1] == "right":
                    x += extent[0] * cos(rot)
                    y -= extent[0] * sin(rot)
                elif self.instruction[id]["ref"].split()[1] == "center":
                    x += extent[0] / 2 * cos(rot)
                    y -= extent[0] / 2 * sin(rot)

                self.instruction[id]["where"] = self.CanvasPaperCoordinates(
                    rect=Rect2D(x, y, 0, 0), canvasToPaper=True
                )[:2]
                self.RecalculateEN()

    def ComputeZoom(self, rect):
        """Computes zoom factor and scroll view"""
        zoomFactor = 1
        cW, cH = self.GetClientSize()
        cW = float(cW)
        if rect.IsEmpty():  # clicked on canvas
            zoomFactor = 1.5
            if self.mouse["use"] == "zoomout":
                zoomFactor = 1.0 / zoomFactor
            x, y = self.mouse["begin"]
            xView = x - x / zoomFactor  # x - cW/(zoomFactor * 2)
            yView = y - y / zoomFactor  # y - cH/(zoomFactor * 2)

        else:  # dragging
            rW, rH = float(rect.GetWidth()), float(rect.GetHeight())
            try:
                zoomFactor = 1 / max(rW / cW, rH / cH)
            except ZeroDivisionError:
                zoomFactor = 1
            # when zooming to full extent, in some cases, there was zoom
            # 1.01..., which causes problem
            if abs(zoomFactor - 1) > 0.01:
                zoomFactor = zoomFactor
            else:
                zoomFactor = 1.0

            if self.mouse["use"] == "zoomout":
                zoomFactor = min(rW / cW, rH / cH)
            try:
                if rW / rH > cW / cH:
                    yView = rect.GetY() - (rW * (cH / cW) - rH) / 2
                    xView = rect.GetX()

                    if self.mouse["use"] == "zoomout":
                        x, y = rect.GetX() + (rW - (cW / cH) * rH) / 2, rect.GetY()
                        xView, yView = -x, -y
                else:
                    xView = rect.GetX() - (rH * (cW / cH) - rW) / 2
                    yView = rect.GetY()
                    if self.mouse["use"] == "zoomout":
                        x, y = rect.GetX(), rect.GetY() + (rH - (cH / cW) * rW) / 2
                        xView, yView = -x, -y
            except ZeroDivisionError:
                xView, yView = rect.GetX(), rect.GetY()
        return zoomFactor, (int(xView), int(yView))

    def Zoom(self, zoomFactor, view):
        """Zoom to specified region, scroll view, redraw"""
        if not self.currScale:
            return
        self.currScale = self.currScale * zoomFactor

        if self.currScale > 10 or self.currScale < 0.1:
            self.currScale = self.currScale / zoomFactor
            return
        if not self.preview:
            # redraw paper
            pRect = self.pdcPaper.GetIdBounds(self.pageId)
            if globalvar.wxPythonPhoenix:
                pRect.Offset(-view[0], -view[1])
            else:
                pRect.OffsetXY(-view[0], -view[1])
            pRect = self.ScaleRect(rect=pRect, scale=zoomFactor)
            self.DrawPaper(pRect)

            # redraw objects
            for id in self.objectId:
                type = self.instruction[id].type
                if type == "labels":  # why it's here? it should not
                    continue
                oRect = self.CanvasPaperCoordinates(
                    rect=self.instruction[id]["rect"], canvasToPaper=False
                )

                if type == "text":
                    # recalculate coordinates, they are not equal to BB
                    coords = self.instruction[id]["coords"]
                    self.instruction[id]["coords"] = coords = [
                        (int(coord) - view[i]) * zoomFactor
                        for i, coord in enumerate(coords)
                    ]
                    extent = self.parent.getTextExtent(textDict=self.instruction[id])
                    if self.instruction[id]["rotate"]:
                        rot = float(self.instruction[id]["rotate"])
                    else:
                        rot = 0
                    self.instruction[id][
                        "rect"
                    ] = bounds = self.parent.getModifiedTextBounds(
                        coords[0], coords[1], extent, rot
                    )
                    self.DrawRotText(
                        pdc=self.pdcObj,
                        drawId=id,
                        textDict=self.instruction[id],
                        coords=coords,
                        bounds=bounds,
                    )

                    self.pdcObj.SetIdBounds(id, bounds)

                elif type == "northArrow":
                    self.Draw(
                        pen=self.pen[type],
                        brush=self.brush[type],
                        pdc=self.pdcObj,
                        drawid=id,
                        pdctype="bitmap",
                        bb=oRect,
                    )

                elif type in ("point", "line", "rectangle"):
                    instr = self.instruction[id]
                    color = self.instruction[id]["color"]
                    width = fcolor = coords = None

                    if type in ("point", "rectangle"):
                        fcolor = self.instruction[id]["fcolor"]
                    if type in ("line", "rectangle"):
                        width = self.instruction[id]["width"]
                    if type in ("line"):
                        point1, point2 = instr["where"][0], instr["where"][1]
                        point1 = self.CanvasPaperCoordinates(
                            rect=Rect2DPS(point1, (0, 0)), canvasToPaper=False
                        )[:2]
                        point2 = self.CanvasPaperCoordinates(
                            rect=Rect2DPS(point2, (0, 0)), canvasToPaper=False
                        )[:2]
                        coords = (point1, point2)

                    self.DrawGraphics(
                        drawid=id,
                        shape=type,
                        bb=oRect,
                        lineCoords=coords,
                        color=color,
                        fcolor=fcolor,
                        width=width,
                    )

                else:
                    self.Draw(
                        pen=self.pen[type],
                        brush=self.brush[type],
                        pdc=self.pdcObj,
                        drawid=id,
                        pdctype="rectText",
                        bb=oRect,
                    )
            # redraw tmp objects
            if self.dragId != -1:
                self.RedrawSelectBox(self.dragId)

        # redraw preview
        else:  # preview mode
            imageRect = self.pdcImage.GetIdBounds(self.imageId)
            if globalvar.wxPythonPhoenix:
                imageRect.Offset(-view[0], -view[1])
            else:
                imageRect.OffsetXY(-view[0], -view[1])
            imageRect = self.ScaleRect(rect=imageRect, scale=zoomFactor)
            self.DrawImage(imageRect)

    def ZoomAll(self):
        """Zoom to full extent"""
        if not self.preview:
            bounds = self.pdcPaper.GetIdBounds(self.pageId)
        else:
            bounds = self.pdcImage.GetIdBounds(self.imageId)
        zoomP = bounds.Inflate(round(bounds.width / 20), round(bounds.height / 20))
        zoomFactor, view = self.ComputeZoom(zoomP)
        self.Zoom(zoomFactor, view)

    def Draw(
        self,
        pen,
        brush,
        pdc,
        drawid=None,
        pdctype="rect",
        bb=Rect(0, 0, 0, 0),
        lineCoords=None,
    ):
        """Draw object with given pen and brush.

        :param pdc: PseudoDC
        :param pdctype: 'bitmap'/'rectText'/'rect'/'point'/'line'
        :param bb: bounding box
        :param lineCoords: coordinates of line start, end points (wx.Point, wx.Point)
        """
        if drawid is None:
            drawid = NewId()
        bb = bb.Get()
        pdc.BeginDrawing()
        pdc.RemoveId(drawid)
        pdc.SetId(drawid)
        pdc.SetPen(pen)
        pdc.SetBrush(brush)

        if pdctype == "bitmap":
            if havePILImage:
                file = self.instruction[drawid]["epsfile"]
                rotation = self.instruction[drawid]["rotate"]
                self.DrawBitmap(pdc=pdc, filePath=file, rotation=rotation, bbox=bb)
            else:  # draw only rectangle with label
                pdctype = "rectText"

        if pdctype in ("rect", "rectText"):
            pdc.DrawRectangle(*bb)

        if pdctype == "rectText":
            # dc created because of method GetTextExtent, which pseudoDC lacks
            dc = ClientDC(self)
            font = dc.GetFont()
            size = 10
            font.SetPointSize(size)
            font.SetStyle(wx.ITALIC)
            dc.SetFont(font)
            pdc.SetFont(font)
            text = "\n".join(self.itemLabels[drawid])
            w, h, lh = dc.GetFullMultiLineTextExtent(text)
            textExtent = (w, h)
            textRect = Rect(0, 0, *textExtent).CenterIn(Rect(*bb))
            r = map(int, bb)
            while not Rect(*r).ContainsRect(textRect) and size >= 8:
                size -= 2
                font.SetPointSize(size)
                dc.SetFont(font)
                pdc.SetFont(font)
                textExtent = dc.GetTextExtent(text)
                textRect = Rect(0, 0, *textExtent).CenterIn(Rect(*bb))
            pdc.SetTextForeground(wx.Colour(100, 100, 100, 200))
            pdc.SetBackgroundMode(wx.TRANSPARENT)
            pdc.DrawLabel(text=text, rect=textRect)

        elif pdctype == "point":
            pdc.DrawCircle(x=bb[0] + bb[2] / 2, y=bb[1] + bb[3] / 2, radius=bb[2] / 2)

        elif pdctype == "line":
            pdc.DrawLinePoint(*lineCoords[0], *lineCoords[1])

        pdc.SetIdBounds(drawid, Rect(*bb))
        pdc.EndDrawing()
        self.Refresh()

        return drawid

    def DrawGraphics(
        self, drawid, shape, color, bb, width=None, fcolor=None, lineCoords=None
    ):
        """Draw point/line/rectangle with given color and width

        :param drawid: id of drawn object
        :param shape: drawn shape 'point'/'line'/'rectangle'
        :param color: pen outline color ('RRR:GGG:BBB')
        :param fcolor: brush fill color, if meaningful ('RRR:GGG:BBB')
        :param width: pen width
        :param bb: bounding box
        :param lineCoords: line coordinates (for line only)
        """
        pdctype = {"point": "point", "line": "line", "rectangle": "rect"}

        if color == "none":
            pen = wx.TRANSPARENT_PEN
        else:
            if width is not None:
                units = UnitConversion(self)
                width = int(
                    units.convert(value=width, fromUnit="point", toUnit="pixel")
                    * self.currScale
                )
            else:
                width = 2
            pen = wx.Pen(colour=convertRGB(color), width=width)
            pen.SetCap(wx.CAP_BUTT)  # this is how ps.map draws

        brush = wx.TRANSPARENT_BRUSH
        if fcolor and fcolor != "none":
            brush = wx.Brush(colour=convertRGB(fcolor))

        self.Draw(
            pen=pen,
            brush=brush,
            pdc=self.pdcObj,
            pdctype=pdctype[shape],
            drawid=drawid,
            bb=bb,
            lineCoords=lineCoords,
        )

    def DrawBitmap(self, pdc, filePath, rotation, bbox):
        """Draw bitmap using PIL"""
        pImg = PILImage.open(filePath)
        if rotation:
            # get rid of black background
            pImg = pImg.convert("RGBA")
            rot = pImg.rotate(rotation, expand=1)
            new = PILImage.new("RGBA", rot.size, (255,) * 4)
            pImg = PILImage.composite(rot, new, rot)
        pImg = pImg.resize((int(bbox[2]), int(bbox[3])), resample=PILImage.BICUBIC)
        img = PilImageToWxImage(pImg)
        bitmap = img.ConvertToBitmap()
        mask = wx.Mask(bitmap, wx.WHITE)
        bitmap.SetMask(mask)
        pdc.DrawBitmap(bitmap, bbox[0], bbox[1], useMask=True)

    def DrawRotText(self, pdc, drawId, textDict, coords, bounds):
        if textDict["rotate"]:
            rot = float(textDict["rotate"])
        else:
            rot = 0

        if textDict["background"] != "none":
            background = textDict["background"]
        else:
            background = None

        pdc.RemoveId(drawId)
        pdc.SetId(drawId)
        pdc.BeginDrawing()

        # border is not redrawn when zoom changes, why?
        # if textDict['border'] != 'none' and not rot:
        ##            units = UnitConversion(self)
        # borderWidth = units.convert(value = textDict['width'],
        # fromUnit = 'point', toUnit = 'pixel' ) * self.currScale
        ##            pdc.SetPen(wx.Pen(colour = convertRGB(textDict['border']), width = borderWidth))
        # pdc.DrawRectangle(*bounds)

        if background:
            pdc.SetTextBackground(convertRGB(background))
            pdc.SetBackgroundMode(wx.SOLID)
        else:
            pdc.SetBackgroundMode(wx.TRANSPARENT)

        fn = self.parent.makePSFont(textDict)

        pdc.SetFont(fn)
        pdc.SetTextForeground(convertRGB(textDict["color"]))
        if rot == 0:
            pdc.DrawLabel(text=textDict["text"], rect=bounds)
        else:
            pdc.DrawRotatedText(textDict["text"], int(coords[0]), int(coords[1]), rot)

        pdc.SetIdBounds(drawId, Rect(*bounds))
        self.Refresh()
        pdc.EndDrawing()

    def DrawImage(self, rect):
        """Draw preview image to pseudoDC"""
        self.pdcImage.ClearId(self.imageId)
        self.pdcImage.SetId(self.imageId)
        img = self.image

        if img.GetWidth() != rect.width or img.GetHeight() != rect.height:
            img = img.Scale(rect.width, rect.height)
        bitmap = img.ConvertToBitmap()

        self.pdcImage.BeginDrawing()
        self.pdcImage.DrawBitmap(bitmap, rect.x, rect.y)
        self.pdcImage.SetIdBounds(self.imageId, rect)
        self.pdcImage.EndDrawing()
        self.Refresh()

    def DrawPaper(self, rect):
        """Draw paper and margins"""
        page = self.instruction[self.pageId]
        scale = page["Width"] / rect.GetWidth()
        w = (page["Width"] - page["Right"] - page["Left"]) / scale
        h = (page["Height"] - page["Top"] - page["Bottom"]) / scale
        x = page["Left"] / scale + rect.GetX()
        y = page["Top"] / scale + rect.GetY()

        self.pdcPaper.BeginDrawing()
        self.pdcPaper.RemoveId(self.pageId)
        self.pdcPaper.SetId(self.pageId)
        self.pdcPaper.SetPen(self.pen["paper"])
        self.pdcPaper.SetBrush(self.brush["paper"])
        self.pdcPaper.DrawRectangleRect(rect)

        self.pdcPaper.SetPen(self.pen["margins"])
        self.pdcPaper.SetBrush(self.brush["margins"])
        self.pdcPaper.DrawRectangle(int(x), int(y), int(w), int(h))

        self.pdcPaper.SetIdBounds(self.pageId, rect)
        self.pdcPaper.EndDrawing()
        self.Refresh()

    def ImageRect(self):
        """Returns image centered in canvas, computes scale"""
        img = wx.Image(self.imgName, wx.BITMAP_TYPE_PNG)
        cW, cH = self.GetClientSize()
        iW, iH = img.GetWidth(), img.GetHeight()

        self.currScale = min(float(cW) / iW, float(cH) / iH)
        iW = iW * self.currScale
        iH = iH * self.currScale
        x = cW / 2 - iW / 2
        y = cH / 2 - iH / 2
        imageRect = Rect(int(x), int(y), int(iW), int(iH))

        return imageRect

    def RedrawSelectBox(self, id):
        """Redraws select box when selected object changes its size"""
        if self.dragId == id:
            rect = self.pdcObj.GetIdBounds(id)
            if self.instruction[id].type != "line":
                rect = rect.Inflate(3, 3)
            # draw select box around object
            self.Draw(
                pen=self.pen["select"],
                brush=self.brush["select"],
                pdc=self.pdcTmp,
                drawid=self.idBoxTmp,
                pdctype="rect",
                bb=rect,
            )

            # draw small marks signalizing resizing
            if self.instruction[id].type in ("map", "rectangle"):
                controlP = self.pdcObj.GetIdBounds(id).GetBottomRight()
                rect = Rect(
                    controlP[0],
                    controlP[1],
                    self.resizeBoxSize[0],
                    self.resizeBoxSize[1],
                )
                self.Draw(
                    pen=self.pen["resize"],
                    brush=self.brush["resize"],
                    pdc=self.pdcTmp,
                    drawid=self.idResizeBoxTmp,
                    pdctype="rect",
                    bb=rect,
                )

            elif self.instruction[id].type == "line":
                p1Paper = self.instruction[id]["where"][0]
                p2Paper = self.instruction[id]["where"][1]
                p1Canvas = self.CanvasPaperCoordinates(
                    rect=Rect2DPS(p1Paper, (0, 0)), canvasToPaper=False
                )[:2]
                p2Canvas = self.CanvasPaperCoordinates(
                    rect=Rect2DPS(p2Paper, (0, 0)), canvasToPaper=False
                )[:2]
                rect = []
                box = Rect(0, 0, self.resizeBoxSize[0], self.resizeBoxSize[1])
                rect.append(box.CenterIn(Rect(p1Canvas[0], p1Canvas[1], 0, 0)))
                rect.append(box.CenterIn(Rect(p2Canvas[0], p2Canvas[1], 0, 0)))
                for i, point in enumerate((p1Canvas, p2Canvas)):
                    self.Draw(
                        pen=self.pen["resize"],
                        brush=self.brush["resize"],
                        pdc=self.pdcTmp,
                        drawid=self.idLinePointsTmp[i],
                        pdctype="rect",
                        bb=rect[i],
                    )

    def UpdateMapLabel(self):
        """Updates map frame label"""

        vector = self.instruction.FindInstructionByType("vector")
        if vector:
            vectorId = vector.id
        else:
            vectorId = None

        raster = self.instruction.FindInstructionByType("raster")
        if raster:
            rasterId = raster.id
        else:
            rasterId = None

        rasterName = "None"
        if rasterId:
            rasterName = self.instruction[rasterId]["raster"].split("@")[0]

        mapId = self.instruction.FindInstructionByType("map").id
        self.itemLabels[mapId] = []
        self.itemLabels[mapId].append(self.itemLabelsDict["map"])
        self.itemLabels[mapId].append("raster: " + rasterName)
        if vectorId:
            for map in self.instruction[vectorId]["list"]:
                self.itemLabels[mapId].append("vector: " + map[0].split("@")[0])

        labels = self.instruction.FindInstructionByType("labels")
        if labels:
            labelFiles = self.instruction[labels.id]["labels"]
            if not labelFiles:
                return
            labelFiles = [lFile.split("@")[0] for lFile in labelFiles]
            self.itemLabels[mapId].append(_("labels: ") + ", ".join(labelFiles))

    def UpdateLabel(self, itype, id):
        self.itemLabels[id] = []
        self.itemLabels[id].append(self.itemLabelsDict[itype])
        if itype == "image":
            file = os.path.basename(self.instruction[id]["epsfile"])
            self.itemLabels[id].append(file)

    def OnSize(self, event):
        """Init image size to match window size"""
        # not zoom all when notebook page is changed
        if (
            self.preview
            and self.parent.currentPage == 1
            or not self.preview
            and self.parent.currentPage == 0
        ):
            self.ZoomAll()
        self.OnIdle(None)
        event.Skip()

    def OnIdle(self, event):
        """Only re-render a image during idle time instead of
        multiple times during resizing.
        """

        width, height = self.GetClientSize()
        # Make new off screen bitmap: this bitmap will always have the
        # current drawing in it, so it can be used to save the image
        # to a file, or whatever.
        width = max(width, 20)
        height = max(height, 20)
        self._buffer = EmptyBitmap(width, height)
        # re-render image on idle
        self.resize = True

    def ScaleRect(self, rect, scale):
        """Scale rectangle"""
        return Rect(
            int(rect.GetLeft() * scale),
            int(rect.GetTop() * scale),
            int(rect.GetSize()[0] * scale),
            int(rect.GetSize()[1] * scale),
        )
