#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Created on Wed Nov 20 14:44:32 2013

@author: anna
"""

import os
import wx
import wx.lib.filebrowsebutton as filebrowse
from shutil import copyfile
from subprocess import PIPE
import signal

from grass.script.utils import set_path, get_lib_path
set_path(modulename='g.gui.tangible')
from grass.script.setup import set_gui_path
set_gui_path()

from gui_core.gselect import Select
from core.settings import UserSettings
import grass.script as gscript
from grass.pydispatch.signal import Signal
from grass.exceptions import CalledModuleError

from wxwrap import TextCtrl, Button, BitmapButton, SpinCtrl, CheckBox
from tangible_utils import get_environment, run_analyses, updateGUIEvt, EVT_UPDATE_GUI
from tangible_utils import EVT_ADD_LAYERS, EVT_REMOVE_LAYERS, EVT_CHECK_LAYERS, EVT_SELECT_LAYERS, EVT_CHANGE_LAYER
from drawing import DrawingPanel
from export import OutputPanel
from activities import ActivitiesPanel
from tangible_utils import get_show_layer_icon


class AnalysesPanel(wx.Panel):
    def __init__(self, parent, giface, settings, scaniface):
        wx.Panel.__init__(self, parent)
        self.giface = giface
        self.settings = settings
        self.scaniface = scaniface
        self.settingsChanged = Signal('AnalysesPanel.settingsChanged')

        mainSizer = wx.BoxSizer(wx.VERTICAL)
        if self.settings['analyses']['file']:
            path = self.settings['analyses']['file']
            initDir = os.path.dirname(path)
        else:
            path = initDir = ""

        topoBox = wx.StaticBox(self, label='  Topographic analyses ')
        topoSizer = wx.StaticBoxSizer(topoBox, wx.VERTICAL)
        self.contoursSelect = Select(self, size=(-1, -1), type='vector')
        self.contoursStepTextCtrl = TextCtrl(self, size=(40, -1))
        self.contoursStepTextCtrl.SetToolTip("Contour step")

        if 'contours' in self.settings['analyses'] and self.settings['analyses']['contours']:
            self.contoursStepTextCtrl.SetValue(str(self.settings['analyses']['contours_step']))
            self.contoursSelect.SetValue(self.settings['analyses']['contours'])

        bmp = get_show_layer_icon()
        self.addContours = wx.BitmapButton(self, bitmap=bmp, size=(bmp.GetWidth() + 12, bmp.GetHeight() + 8))
        self.addContours.Bind(wx.EVT_BUTTON, self._addContourLayer)

        self.contoursSelect.Bind(wx.EVT_TEXT, self.OnAnalysesChange)
        self.contoursStepTextCtrl.Bind(wx.EVT_TEXT, self.OnAnalysesChange)

        fileBox = wx.StaticBox(self, label='  Python file with analyses to run ')
        fileSizer = wx.StaticBoxSizer(fileBox, wx.VERTICAL)
        self.selectAnalyses = filebrowse.FileBrowseButton(self, labelText="File path:", fileMask="Python file (*.py)|*.py",
                                                          startDirectory=initDir, initialValue=path,
                                                          changeCallback=lambda evt: self.SetAnalysesFile(evt.GetString()))
        if self.settings['analyses']['file']:
            self.selectAnalyses.SetValue(self.settings['analyses']['file'])

        newAnalyses = wx.Button(self, label="Create new file with predefined analyses")
        newAnalyses.Bind(wx.EVT_BUTTON, lambda evt: self.CreateNewFile())
        self.selectAnalyses.Bind(wx.EVT_TEXT, self.OnAnalysesChange)

        if 'color_training' not in self.settings['analyses']:
            self.settings['analyses']['color_training'] = ''

        colorBox = wx.StaticBox(self, label='  Color calibration for classification  ')
        colorSizer = wx.StaticBoxSizer(colorBox, wx.VERTICAL)
        self.trainingAreas = Select(self, size=(-1, -1), type='raster')
        self.trainingAreas.SetValue(self.settings['analyses']['color_training'])
        self.trainingAreas.Bind(wx.EVT_TEXT, self.OnAnalysesChange)
        calibrateBtn = wx.Button(self, label="Calibrate")
        calibrateBtn.Bind(wx.EVT_BUTTON, self.OnColorCalibration)

        bmp = get_show_layer_icon()
        addLayerBtn = BitmapButton(self, bitmap=bmp, size=(bmp.GetWidth()+12, bmp.GetHeight()+8))
        addLayerBtn.SetToolTip("Add layer to display")
        addLayerBtn.Bind(wx.EVT_BUTTON, self._addCalibLayer)

        sizer = wx.BoxSizer(wx.HORIZONTAL)
        sizer.Add(wx.StaticText(self, label="Contour map name:"), proportion=0, flag=wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, border=5)
        sizer.Add(self.contoursSelect, proportion=4, flag=wx.ALIGN_CENTER_VERTICAL, border=5)
        sizer.Add(self.addContours, proportion=0, flag=wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, border=5)
        sizer.Add(wx.StaticText(self, label="Interval:"), proportion=0, flag=wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, border=5)
        sizer.Add(self.contoursStepTextCtrl, proportion=1, flag=wx.ALIGN_CENTER_VERTICAL, border=5)
        topoSizer.Add(sizer, flag=wx.EXPAND | wx.ALL, border=5)
        mainSizer.Add(topoSizer, flag=wx.EXPAND | wx.ALL, border=5)

        sizer = wx.BoxSizer(wx.HORIZONTAL)
        sizer.Add(self.selectAnalyses, proportion=1, flag=wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, border=5)
        fileSizer.Add(sizer, flag=wx.EXPAND | wx.ALL, border=5)

        sizer = wx.BoxSizer(wx.HORIZONTAL)
        sizer.AddStretchSpacer()
        sizer.Add(newAnalyses, proportion=1, flag=wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, border=5)
        fileSizer.Add(sizer, flag=wx.EXPAND | wx.ALL, border=5)
        mainSizer.Add(fileSizer, flag=wx.EXPAND | wx.ALL, border=5)

        # color training
        sizer = wx.BoxSizer(wx.HORIZONTAL)
        sizer.Add(wx.StaticText(self, label="Raster with training areas:"), proportion=0, flag=wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, border=5)
        sizer.Add(self.trainingAreas, proportion=1, flag=wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, border=1)
        sizer.Add(addLayerBtn, proportion=0, flag=wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, border=5)
        sizer.Add(calibrateBtn, proportion=0, flag=wx.ALIGN_CENTER_VERTICAL)
        colorSizer.Add(sizer, flag=wx.EXPAND | wx.ALL, border=5)
        mainSizer.Add(colorSizer, flag=wx.EXPAND | wx.ALL, border=5)

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

    def SetAnalysesFile(self, path):
        self.settings['analyses']['file'] = path

    def OnAnalysesChange(self, event):
        self.settings['analyses']['contours'] = self.contoursSelect.GetValue()
        self.settings['analyses']['contours_step'] = self.contoursStepTextCtrl.GetValue()
        self.settings['analyses']['file'] = self.selectAnalyses.GetValue()
        self.settings['analyses']['color_training'] = self.trainingAreas.GetValue()
        self.settingsChanged.emit()

    def CreateNewFile(self):
        get_lib_path('g.gui.tangible')
        dlg = wx.FileDialog(self, message="Create a new file with analyses",
                            wildcard="Python source (*.py)|*.py",
                            style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT)
        if dlg.ShowModal() == wx.ID_OK:
            path = dlg.GetPath()
            orig = os.path.join(get_lib_path('g.gui.tangible'), 'current_analyses.py')
            if not os.path.exists(orig):
                self.giface.WriteError("File with analyses not found: {}".format(orig))
            else:
                copyfile(orig, path)
                self.selectAnalyses.SetValue(path)
                self.settings['analyses']['file'] = path
        dlg.Destroy()

    def OnColorCalibration(self, event):
        if self.scaniface.IsScanning():
            dlg = wx.MessageDialog(self, 'In order to calibrate, please stop scanning process first.',
                                   'Stop scanning',
                                   wx.OK | wx.ICON_WARNING)
            dlg.ShowModal()
            dlg.Destroy()
            return

        training = self.trainingAreas.GetValue()
        if not training:
            return
        if self.settings['output']['color'] and self.settings['output']['color_name']:
            self.group = self.settings['output']['color_name']
        else:
            self.group = None
            dlg = wx.MessageDialog(self, "In order to calibrate colors, please specify name of output color raster in 'Output' tab.",
                                   'Need color output',
                                   wx.OK | wx.ICON_WARNING)
            dlg.ShowModal()
            dlg.Destroy()
            return

        self.CalibrateColor()

    def CalibrateColor(self):
        ll = self.giface.GetLayerList()
        checked = []
        for l in ll:
            if ll.IsLayerChecked(l):
                checked.append(l.cmd)
                ll.CheckLayer(l, False)
        wx.Yield()

        self.scaniface.Scan(continuous=False)
        self.scaniface.process.wait()
        self.scaniface.process = None
        self.scaniface.status.SetLabel("Done.")

        self._defineEnvironment()

        self._calibrateColor()
        # check the layers back to previous state
        ll = self.giface.GetLayerList()
        for l in ll:
            if l.cmd in checked:
                ll.CheckLayer(l, True)

    def _calibrateColor(self):
        gscript.run_command('i.gensigset', trainingmap=self.settings['analyses']['color_training'], group=self.group,
                            subgroup=self.group, signaturefile='signature', env=self.env, overwrite=True)  # we need here overwrite=True

    def _defineEnvironment(self):
        self.env = None
        maps = gscript.read_command('i.group', flags='g', group=self.group, subgroup=self.group, quiet=True).strip()
        if maps:
            self.env = get_environment(raster=maps.splitlines()[0])

    def _addCalibLayer(self, event):
        ll = self.giface.GetLayerList()
        raster = self.trainingAreas.GetValue()
        if not raster:
            return
        cmd = ['d.rast', 'map=' + raster]
        ll.AddLayer('raster', name=raster, checked=True, cmd=cmd)

    def _addContourLayer(self, event):
        ll = self.giface.GetLayerList()
        vector = self.contoursSelect.GetValue()
        if not vector:
            return
        cmd = ['d.vect', 'map=' + vector]
        ll.AddLayer('vector', name=vector, checked=True, cmd=cmd)


class ScanningPanel(wx.Panel):
    def __init__(self, parent, giface, settings, scaniface):
        wx.Panel.__init__(self, parent)
        self.giface = giface
        self.settings = settings
        self.scaniface = scaniface
        if 'scan' not in self.settings:
            self.settings['scan'] = {}
            self.settings['scan']['elevation'] = ''
            self.settings['scan']['region'] = ''
            self.settings['scan']['zexag'] = 1
            self.settings['scan']['smooth'] = 8
            self.settings['scan']['numscans'] = 1
            self.settings['scan']['rotation_angle'] = 180
            self.settings['scan']['resolution'] = 2
            self.settings['scan']['trim_nsewtb'] = '30,30,30,30,50,150'
            self.settings['scan']['interpolate'] = False
            self.settings['scan']['trim_tolerance'] = ''
            self.settings['scan']['resolution'] = 2

        self.scan = self.settings['scan']

        self.settingsChanged = Signal('ScanningPanel.settingsChanged')

        mainSizer = wx.BoxSizer(wx.VERTICAL)
        # define static boxes before all widgets are defined
        georefBox = wx.StaticBox(self, label='  Georeferencing  ')
        georefSizer = wx.StaticBoxSizer(georefBox, wx.VERTICAL)
        geomBox = wx.StaticBox(self, label='  Scan geometry  ')
        geomSizer = wx.StaticBoxSizer(geomBox, wx.VERTICAL)
        demBox = wx.StaticBox(self, label=' DEM quality ')
        demSizer = wx.StaticBoxSizer(demBox, wx.VERTICAL)

        # create widgets
        self.btnCalibrateTilt = Button(self, label="Calibration 1")
        self.btnCalibrateTilt.SetToolTip('Calibrate to remove tilt of the scanner and to set suitable distance from the scanner')
        self.btnCalibrateExtent = Button(self, label="Calibration 2")
        self.btnCalibrateExtent.SetToolTip('Calibrate to identify the extent and position of the scanned object')

        # widgets for model
        self.elevInput = Select(self, size=(-1, -1), type='raster')
        self.elevInput.SetToolTipString('Raster from which we take the georeferencing information')
        self.regionInput = Select(self, size=(-1, -1), type='region')
        self.regionInput.SetToolTipString('Saved region from which we take the georeferencing information')
        self.zexag = TextCtrl(self)
        self.zexag.SetToolTip('Set vertical exaggeration of the physical model')
        self.numscans = SpinCtrl(self, min=1, max=5, initial=1)
        self.numscans.SetToolTip('Set number of scans to integrate')
        self.rotate = SpinCtrl(self, min=0, max=360, initial=180)
        self.rotate.SetToolTip('Set angle of rotation of the sensor around Z axis (typically 180 degrees)')
        self.smooth = TextCtrl(self)
        self.smooth.SetToolTip('Set smoothing of the DEM (typically between 7 to 12, higher value means more smoothing)')
        self.resolution = TextCtrl(self)
        self.resolution.SetToolTip('Raster resolution in mm of the ungeoreferenced scan')
        self.trim = {}
        for each in 'tbnsew':
            self.trim[each] = TextCtrl(self, size=(40, -1))
            if each in 'tb':
                self.trim[each].SetToolTip('Distance from the scanner')
            else:
                self.trim[each].SetToolTip('Distance from the center of scanning to the scanning boundary')
        self.trim_tolerance = TextCtrl(self)
        self.trim_tolerance.SetToolTip('Automatic trimming of the edges for rectangular models')
        self.interpolate = CheckBox(self, label="Use interpolation instead of binning")
        self.interpolate.SetToolTip('Interpolation avoids gaps in the scan, but takes longer')

        self.elevInput.SetValue(self.scan['elevation'])
        self.regionInput.SetValue(self.scan['region'])
        self.zexag.SetValue(str(self.scan['zexag']))
        self.rotate.SetValue(self.scan['rotation_angle'])
        self.numscans.SetValue(self.scan['numscans'])
        self.interpolate.SetValue(self.scan['interpolate'])
        for i, each in enumerate('nsewtb'):
            self.trim[each].SetValue(self.scan['trim_nsewtb'].split(',')[i])
        self.smooth.SetValue(str(self.scan['smooth']))
        self.resolution.SetValue(str(self.scan['resolution']))
        self.trim_tolerance.SetValue(str(self.scan['trim_tolerance']))

        # layout
        #
        # Geometry box
        #
        # rotation
        hSizer = wx.BoxSizer(wx.HORIZONTAL)
        hSizer.Add(wx.StaticText(self, label="Rotation angle:"), proportion=1, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=5)
        hSizer.Add(self.rotate, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=5)
        geomSizer.Add(hSizer, flag=wx.EXPAND)
        # trimming
        hSizer = wx.BoxSizer(wx.HORIZONTAL)
        hSizer.Add(wx.StaticText(self, label="Trim vertically [cm]:"), proportion=1, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=5)
        for each in 'tb':
            hSizer.Add(wx.StaticText(self, label=each.upper() + ':'), flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=2)
            hSizer.Add(self.trim[each], flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=2)
        hSizer.Add(self.btnCalibrateTilt, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=2)
        geomSizer.Add(hSizer, flag=wx.EXPAND)
        hSizer = wx.BoxSizer(wx.HORIZONTAL)
        hSizer.Add(wx.StaticText(self, label="Trim horizontally [cm]:"), proportion=1, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=5)
        for each in 'nsew':
            hSizer.Add(wx.StaticText(self, label=each.upper() + ':'), flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=3)
            hSizer.Add(self.trim[each], flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=2)
        hSizer.Add(self.btnCalibrateExtent, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=2)
        geomSizer.Add(hSizer, flag=wx.EXPAND)
        # automatic trim
        hSizer = wx.BoxSizer(wx.HORIZONTAL)
        hSizer.Add(wx.StaticText(self, label="Trim tolerance [0-1]:"), proportion=1, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=5)
        hSizer.Add(self.trim_tolerance, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=5)
        geomSizer.Add(hSizer, flag=wx.EXPAND)
        mainSizer.Add(geomSizer, flag=wx.EXPAND|wx.ALL, border=10)

        hSizer2 = wx.BoxSizer(wx.HORIZONTAL)
        #
        # Georeferencing box
        #
        # model parameters
        hSizer = wx.BoxSizer(wx.HORIZONTAL)
        hSizer.Add(wx.StaticText(self, label="Reference DEM:"), flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=5)
        hSizer.Add(self.elevInput, proportion=1, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=5)
        georefSizer.Add(hSizer, flag=wx.EXPAND)
        # region
        hSizer = wx.BoxSizer(wx.HORIZONTAL)
        hSizer.Add(wx.StaticText(self, label="Reference region:"), flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=5)
        hSizer.Add(self.regionInput, proportion=1, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=5)
        georefSizer.Add(hSizer, flag=wx.EXPAND)
        hSizer = wx.BoxSizer(wx.HORIZONTAL)
        hSizer.Add(wx.StaticText(self, label="Z-exaggeration:"), proportion=1, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=5)
        hSizer.Add(self.zexag, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=5)
        georefSizer.Add(hSizer, flag=wx.EXPAND)
        hSizer2.Add(georefSizer, proportion=1, flag=wx.EXPAND|wx.RIGHT, border=10)

        #
        # DEM properties box
        #
        # number of scans
        hSizer = wx.BoxSizer(wx.HORIZONTAL)
        hSizer.Add(wx.StaticText(self, label="Number of scans:"), proportion=1, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=5)
        hSizer.Add(self.numscans, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=5)
        demSizer.Add(hSizer, flag=wx.EXPAND)

        # smooth
        hSizer = wx.BoxSizer(wx.HORIZONTAL)
        hSizer.Add(wx.StaticText(self, label="Smooth value:"), proportion=1, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=5)
        hSizer.Add(self.smooth, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=5)
        demSizer.Add(hSizer, flag=wx.EXPAND)
        # resolution
        hSizer = wx.BoxSizer(wx.HORIZONTAL)
        hSizer.Add(wx.StaticText(self, label="Resolution [mm]:"), proportion=1, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=5)
        hSizer.Add(self.resolution, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=5)
        demSizer.Add(hSizer, flag=wx.EXPAND)

        hSizer = wx.BoxSizer(wx.HORIZONTAL)
        hSizer.Add(self.interpolate, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=5)
        demSizer.Add(hSizer, flag=wx.EXPAND)

        hSizer2.Add(demSizer, proportion=1, flag=wx.EXPAND)
        mainSizer.Add(hSizer2, flag=wx.EXPAND|wx.LEFT|wx.RIGHT|wx.BOTTOM, border=10)

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

        self.BindModelProperties()

    def BindModelProperties(self):
        self.btnCalibrateTilt.Bind(wx.EVT_BUTTON, self.scaniface.Calibrate)
        self.btnCalibrateExtent.Bind(wx.EVT_BUTTON, self.scaniface.CalibrateModelBBox)

        # model parameters
        self.elevInput.Bind(wx.EVT_TEXT, self.OnModelProperties)
        self.regionInput.Bind(wx.EVT_TEXT, self.OnModelProperties)
        self.zexag.Bind(wx.EVT_TEXT, self.OnModelProperties)
        self.rotate.Bind(wx.EVT_SPINCTRL, self.OnModelProperties)
        self.rotate.Bind(wx.EVT_TEXT, self.OnModelProperties)
        self.numscans.Bind(wx.EVT_SPINCTRL, self.OnModelProperties)
        self.numscans.Bind(wx.EVT_TEXT, self.OnModelProperties)
        self.interpolate.Bind(wx.EVT_CHECKBOX, self.OnModelProperties)
        self.smooth.Bind(wx.EVT_TEXT, self.OnModelProperties)
        self.resolution.Bind(wx.EVT_TEXT, self.OnModelProperties)
        self.trim_tolerance.Bind(wx.EVT_TEXT, self.OnModelProperties)
        for each in 'nsewtb':
            self.trim[each].Bind(wx.EVT_TEXT, self.OnModelProperties)

    def OnModelProperties(self, event):
        self.scan['elevation'] = self.elevInput.GetValue()
        self.scan['region'] = self.regionInput.GetValue()
        self.scan['rotation_angle'] = self.rotate.GetValue()
        self.scan['numscans'] = self.numscans.GetValue()
        self.scan['interpolate'] = self.interpolate.IsChecked()
        self.scan['smooth'] = self.smooth.GetValue()
        self.scan['resolution'] = self.resolution.GetValue()
        trim_tol = self.trim_tolerance.GetValue()
        self.scan['trim_tolerance'] = float(trim_tol) if trim_tol else trim_tol

        try:
            self.scan['zexag'] = float(self.zexag.GetValue())
            nsewtb_list = []
            for each in 'nsewtb':
                nsewtb_list.append(self.trim[each].GetValue())
            self.scan['trim_nsewtb'] = ','.join(nsewtb_list)
        except ValueError:
            pass
        self.settingsChanged.emit()


class TangibleLandscapePlugin(wx.Dialog):
    def __init__(self, giface, parent):
        wx.Dialog.__init__(self, parent, title="Tangible Landscape", style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
        self.giface = giface
        self.parent = parent

        if not gscript.find_program('r.in.kinect'):
            self.giface.WriteError("ERROR: Module r.in.kinect not found.")

        self.settings = {}
        UserSettings.ReadSettingsFile(settings=self.settings)
        # for the first time
        if not 'tangible' in self.settings:
            self.settings['tangible'] = {'calibration': {'matrix': None},
                                         'analyses': {'file': None,
                                                      'contours': None,
                                                      'contours_step': 1}
                                         }
        self.calib_matrix = self.settings['tangible']['calibration']['matrix']

        self.delay = 0.3
        self.process = None
        self.observer = None
        self.timer = wx.Timer(self)
        self.changedInput = False
        self.filter = {'filter': False,
                       'counter': 0,
                       'threshold': 0.1,
                       'debug': False}
        # to be able to add params to runAnalyses from outside
        self.additionalParams4Analyses = {}

        self.notebook = wx.Notebook(self)
        self.scanning_panel = ScanningPanel(self.notebook, self.giface, self.settings['tangible'], scaniface=self)
        self.notebook.AddPage(self.scanning_panel, "Scanning")
        self.scan = self.settings['tangible']['scan']

        self.outputPanel = OutputPanel(self.notebook, self.giface, self.settings['tangible'])
        self.notebook.AddPage(self.outputPanel, "Output")
        self.scanning_panel.settingsChanged.connect(lambda: setattr(self, 'changedInput', True))
        analyses_panel = AnalysesPanel(self.notebook, self.giface, self.settings['tangible'], scaniface=self)
        self.notebook.AddPage(analyses_panel, "Analyses")
        analyses_panel.settingsChanged.connect(lambda: setattr(self, 'changedInput', True))
        self.outputPanel.settingsChanged.connect(lambda: setattr(self, 'changedInput', True))
        self.drawing_panel = DrawingPanel(self.notebook, self.giface, self.settings['tangible'])
        self.notebook.AddPage(self.drawing_panel, "Drawing")
        self.drawing_panel.Bind(EVT_UPDATE_GUI, self.OnUpdate)
        self.drawing_panel.settingsChanged.connect(lambda: setattr(self, 'changedInput', True))
        self.activities_panel = ActivitiesPanel(self.notebook, self.giface, self.settings['tangible'], scaniface=self)
        self.notebook.AddPage(self.activities_panel, "Activities")

        btnStart = wx.Button(self, label="Start")
        btnStop = wx.Button(self, label="Stop")
        btnPause = wx.Button(self, label="Pause")
        self.btnPause = btnPause
        btnScanOnce = wx.Button(self, label="Scan once")
        btnHelp = wx.Button(self, label="Help")
        btnClose = wx.Button(self, label="Close")
        self.status = wx.StaticText(self)

        # bind events
        btnStart.Bind(wx.EVT_BUTTON, lambda evt: self.Start())
        btnStop.Bind(wx.EVT_BUTTON, lambda evt: self.Stop())
        btnPause.Bind(wx.EVT_BUTTON, lambda evt: self.Pause())
        btnScanOnce.Bind(wx.EVT_BUTTON, self.ScanOnce)
        btnHelp.Bind(wx.EVT_BUTTON, self.OnHelp)
        btnClose.Bind(wx.EVT_BUTTON, self.OnClose)
        self.Layout()

        sizer = wx.BoxSizer(wx.VERTICAL)
        hSizer = wx.BoxSizer(wx.HORIZONTAL)
        hSizer.Add(btnStart, flag=wx.EXPAND | wx.ALL, border=5)
        hSizer.Add(btnStop, flag=wx.EXPAND | wx.ALL, border=5)
        hSizer.Add(btnPause, flag=wx.EXPAND | wx.ALL, border=5)
        hSizer.Add(btnScanOnce, flag=wx.EXPAND | wx.ALL, border=5)
        sizer.Add(hSizer, 0, wx.ALL | wx.EXPAND, 5)
        hSizer = wx.BoxSizer(wx.HORIZONTAL)
        hSizer.Add(self.status, flag=wx.EXPAND | wx.LEFT, border=5)
        sizer.Add(hSizer)
        sizer.Add(self.notebook, 1, wx.ALL | wx.EXPAND, 5)
        hSizer = wx.BoxSizer(wx.HORIZONTAL)
        hSizer.AddStretchSpacer()
        hSizer.Add(btnHelp, flag=wx.EXPAND | wx.ALL, border=5)
        hSizer.Add(btnClose, flag=wx.EXPAND | wx.ALL, border=5)
        sizer.Add(hSizer, flag=wx.EXPAND)

        self.SetSizer(sizer)
        sizer.Fit(self)
        self.SetMinSize(self.GetBestSize())
        self.Layout()

        self.Bind(wx.EVT_TIMER, self.RestartIfNotRunning, self.timer)
        self.Bind(wx.EVT_CLOSE, self.OnClose)
        self.Bind(EVT_UPDATE_GUI, self.OnUpdate)
        self.Bind(EVT_ADD_LAYERS, self.OnAddLayers)
        self.Bind(EVT_REMOVE_LAYERS, self.OnRemoveLayers)
        self.Bind(EVT_CHECK_LAYERS, self.OnCheckLayers)
        self.Bind(EVT_SELECT_LAYERS, self.OnSelectLayers)
        self.Bind(EVT_CHANGE_LAYER, self.OnChangeLayer)

        self.pause = None
        self.resume_once = None

    def OnHelp(self, event):
        """Show help"""
        self.giface.Help(entry='g.gui.tangible', online=False)

    def OnClose(self, event):
        self.Stop()
        UserSettings.SaveToFile(self.settings)
        self.Destroy()

    def OnUpdate(self, event=None):
        for each in self.giface.GetAllMapDisplays():
            each.GetMapWindow().UpdateMap(delay=self.delay)

    def CalibrateModelBBox(self, event):
        if self.IsScanning():
            dlg = wx.MessageDialog(self, 'In order to calibrate, please stop scanning process first.',
                                   'Stop scanning',
                                   wx.OK | wx.ICON_WARNING)
            dlg.ShowModal()
            dlg.Destroy()
            return
        params = {}
        if self.calib_matrix:
            params['calib_matrix'] = self.calib_matrix
        params['rotate'] = self.scan['rotation_angle']
        zrange = ','.join(self.scan['trim_nsewtb'].split(',')[4:])
        params['zrange'] = zrange
        res = gscript.parse_command('r.in.kinect', flags='m', overwrite=True, **params)
        if not res['bbox']:
            gscript.message(_("Failed to find model extent"))
        offsetcm = 2
        n, s, e, w = [int(round(float(each))) for each in res['bbox'].split(',')]
        self.scanning_panel.trim['n'].SetValue(str(n + offsetcm))
        self.scanning_panel.trim['s'].SetValue(str(abs(s) + offsetcm))
        self.scanning_panel.trim['e'].SetValue(str(e + offsetcm))
        self.scanning_panel.trim['w'].SetValue(str(abs(w) + offsetcm))

    def Calibrate(self, event):
        if self.IsScanning():
            dlg = wx.MessageDialog(self, 'In order to calibrate, please stop scanning process first.',
                                   'Stop scanning',
                                   wx.OK | wx.ICON_WARNING)
            dlg.ShowModal()
            dlg.Destroy()
            return
        dlg = wx.MessageDialog(self, 'In order to calibrate, please remove objects from the table.',
                                   'Calibration',
                                   wx.OK | wx.CANCEL | wx.ICON_INFORMATION)
        if dlg.ShowModal() != wx.ID_OK:
            dlg.Destroy()
            return
        dlg.Destroy()

        res = gscript.parse_command('r.in.kinect', flags='c', overwrite=True)
        if not (res['calib_matrix'] and len(res['calib_matrix'].split(',')) == 9):
            gscript.message(_("Failed to calibrate"))
            return
        else:
            self.giface.WriteCmdLog("Measured and corrected tilting of sensor: {angle} degrees".format(angle=res['angle_deviation']))
            if float(res['angle_deviation']) > 3:
                self.giface.WriteWarning("Angle deviation is too high, please level the sensor.")

        offsetcm = 1
        height = str(round(float(res['height']) * 100 - offsetcm, 1))
        self.scanning_panel.trim['b'].SetValue(height)
        nswetb = self.settings['tangible']['scan']['trim_nsewtb'].split(',')
        nswetb[-1] = height
        self.settings['tangible']['scan']['trim_nsewtb'] = ','.join(nswetb)
        self.settings['tangible']['calibration']['matrix'] = res['calib_matrix']
        UserSettings.SaveToFile(self.settings)

        # update
        self.calib_matrix = res['calib_matrix']

    def GatherParameters(self, editMode, continuous):
        """Create dict of input parameteres for r.in.kinect.
        Parameter editMode=True is needed when this dict is passed as stdin
        into r.in.kinect during scanning. Parameter continuous is needed when
        the scanning is supposed to run in loop and not just once"""
        params = {}
        if self.settings['tangible']['output']['scan']:
            params['output'] = self.settings['tangible']['output']['scan'] + 'tmp'
        # drawing
        if self.settings['tangible']['drawing']['active'] and self.settings['tangible']['drawing']['name']:
            params['draw_output'] = self.settings['tangible']['drawing']['name']
            params['draw'] = self.settings['tangible']['drawing']['type']
            params['draw_threshold'] = self.settings['tangible']['drawing']['threshold']
            # we don't want to scan when drawing
            if editMode:
                params['output'] = ""
            else:
                del params['output']
        elif editMode:
            params['draw_output'] = ""

        if self.calib_matrix:
            params['calib_matrix'] = self.calib_matrix
        if self.scan['elevation']:
            params['raster'] = self.scan['elevation']
        elif self.scan['region']:
            params['region'] = self.scan['region']
        if self.scan['trim_tolerance']:
            params['trim_tolerance'] = self.scan['trim_tolerance']

        # flags
        params['flags'] = ''
        if continuous:
            params['flags'] += 'l'
        if not editMode and not params['flags']:
            del params['flags']

        if self.settings['tangible']['analyses']['contours'] and 'output' in params:
            params['contours'] = self.settings['tangible']['analyses']['contours']
            params['contours_step'] = self.settings['tangible']['analyses']['contours_step']
        elif editMode:
            params['contours'] = ""
        # export PLY
        if 'output' in self.settings['tangible'] and self.settings['tangible']['output']['PLY'] and \
           self.settings['tangible']['output']['PLY_file'] and not self.settings['tangible']['drawing']['active']:
            params['ply'] = self.settings['tangible']['output']['PLY_file']
        elif editMode:
            params['ply'] = ""
        # export color
        if 'output' in self.settings['tangible'] and self.settings['tangible']['output']['color'] and \
           self.settings['tangible']['output']['color_name']:
            params['color_output'] = self.settings['tangible']['output']['color_name']
        elif editMode:
            params['color_output'] = ""

        trim_nsew = ','.join(self.scan['trim_nsewtb'].split(',')[:4])
        params['trim'] = trim_nsew
        params['smooth_radius'] = float(self.scan['smooth'])/1000
        if self.scan['interpolate']:
            method = 'interpolation'
        else:
            method = 'mean'
        params['method'] = method
        zrange = ','.join(self.scan['trim_nsewtb'].split(',')[4:])
        params['zrange'] = zrange
        params['rotate'] = self.scan['rotation_angle']
        params['resolution'] = float(self.scan['resolution'])/1000
        params['zexag'] = self.scan['zexag']
        params['numscan'] = self.scan['numscans']
        if self.process and self.process.poll() is None:  # still running
            if self.resume_once is True:
                params['resume_once'] = ''
                self.resume_once = None

            if self.pause is True:
                params['pause'] = ''
            elif self.pause is False:
                params['resume'] = ''

        return params

    def IsScanning(self):
        if self.process and self.process.poll() is None:
            return True
        return False

    def Scan(self, continuous):
        if self.process and self.process.poll() is None:
            return
        self.status.SetLabel("Scanning...")
        wx.SafeYield()
        params = self.GatherParameters(editMode=False, continuous=continuous)
        self.process = gscript.start_command('r.in.kinect', overwrite=True, quiet=True,
                                             stdin=PIPE, **params)
        return self.process

    def ScanOnce(self, event):
        # if already running, resume scanning one time
        if self.process and self.process.poll() is None:  # still running
            self.resume_once = True
            self.changedInput = True
        else:
            self.Scan(continuous=False)
            self.status.SetLabel("Importing scan...")
            self.process.wait()
            self.process = None
            run_analyses(settings=self.settings, analysesFile=self.settings['tangible']['analyses']['file'],
                         giface=self.giface, update=self.OnUpdate, eventHandler=self, scanFilter=self.filter)
            self.status.SetLabel("Done.")
            self.OnUpdate(None)

    def RestartIfNotRunning(self, event):
        """Mechanism to restart scanning if process ends or
        to update scanning properties during running r.in.kinect
        if scanning input changed"""
        if self.process and self.process.poll() is not None:
            if self.observer:
                try:
                    self.observer.stop()
                except TypeError:  # throws error on mac
                    pass
                self.observer.join()
                self.observer = None
            self.Start()
        if self.changedInput:
            self.changedInput = False
            if self.process and self.process.poll() is None:
                params = self.GatherParameters(editMode=True, continuous=True)
                new_input = ["{}={}".format(key, params[key]) for key in params]
                self.process.stdin.write(gscript.encode('\n'.join(new_input) + '\n\n'))
                # flush needs to be there for Py3, alternative is to use Popen bufsize
                self.process.stdin.flush()
                # SIGUSR1 is the signal r.in.kinect looks for
                self.process.send_signal(signal.SIGUSR1)

    def Start(self):
        self.Scan(continuous=True)
        self.status.SetLabel("Real-time scanning is running now.")

        if self.observer:
            return
        gisenv = gscript.gisenv()
        mapsetPath = os.path.join(gisenv['GISDBASE'], gisenv['LOCATION_NAME'], gisenv['MAPSET'])
        path1 = os.path.join(mapsetPath, 'fcell')
        if not os.path.exists(path1):
            os.mkdir(os.path.join(mapsetPath, 'fcell'))
        path2 = os.path.join(mapsetPath, 'vector')
        if not os.path.exists(path2):
            os.mkdir(os.path.join(mapsetPath, 'vector'))
        paths = [path1, path2]
        handlers = [RasterChangeHandler(self.runImport, self.settings['tangible']['output']),
                    DrawingChangeHandler(self.runImportDrawing, self.settings['tangible']['drawing']['name'])]

        self.observer = Observer()
        for path, handler in zip(paths, handlers):
            self.observer.schedule(handler, path)

        self.observer.start()
        self.timer.Start(1000)

    def Stop(self):
        if self.process and self.process.poll() is None:  # still running
            self.process.terminate()
            self.process.wait()
            self.process = None
            if self.observer:
                try:
                    self.observer.stop()
                except TypeError:  # throws error on mac
                    pass
                self.observer.join()
                self.observer = None
        self.timer.Stop()
        self.status.SetLabel("Real-time scanning stopped.")
        self.pause = False
        self.btnPause.SetLabel("Pause")

    def Pause(self):
        if self.process and self.process.poll() is None:  # still running
            if not self.pause:
                self.pause = True
                self.btnPause.SetLabel("Resume")
            else:
                self.pause = False
                self.btnPause.SetLabel("Pause")
            self.changedInput = True

    def runImport(self):
        run_analyses(settings=self.settings, analysesFile=self.settings['tangible']['analyses']['file'],
                     giface=self.giface, update=self.OnUpdate, eventHandler=self, scanFilter=self.filter,
                     **self.additionalParams4Analyses)
        evt = updateGUIEvt(self.GetId())
        wx.PostEvent(self, evt)

    def runImportDrawing(self):
        self.drawing_panel.appendVector()
        run_analyses(settings=self.settings, analysesFile=self.settings['tangible']['analyses']['file'],
                     giface=self.giface, update=self.OnUpdate, eventHandler=self, scanFilter=self.filter,
                     **self.additionalParams4Analyses)
        evt = updateGUIEvt(self.GetId())
        wx.PostEvent(self, evt)

    def postEvent(self, receiver, event):
        wx.PostEvent(receiver, event)

    def OnAddLayers(self, event):
        ll = self.giface.GetLayerList()
        for each in event.layerSpecs:
            ll.AddLayer(**each)

    def OnRemoveLayers(self, event):
        ll = self.giface.GetLayerList()
        if not hasattr(ll, 'DeleteLayer'):
            print("Removing layers from layer Manager requires GRASS GIS version > 7.2")
            return
        for each in event.layers:
            ll.DeleteLayer(each)

    def OnCheckLayers(self, event):
        ll = self.giface.GetLayerList()
        if not hasattr(ll, 'CheckLayer'):
            print("Checking and unchecking layers in layer Manager requires GRASS GIS version > 7.2")
            return
        for each in event.layers:
            ll.CheckLayer(each, checked=event.checked)

    def OnSelectLayers(self, event):
        ll = self.giface.GetLayerList()
        if not hasattr(ll, 'SelectLayer'):
            print("Selecting layers in Layer Manager requires GRASS GIS version >= 7.6")
            return
        for each in event.layers:
            ll.SelectLayer(each, select=event.select)

    def OnChangeLayer(self, event):
        ll = self.giface.GetLayerList()
        if not hasattr(ll, 'ChangeLayer'):
            print("Changing layer in Layer Manager requires GRASS GIS version > 7.8")
            return
        params = {}
        if hasattr(event, 'ltype'):
            params['ltype'] = event.ltype
        if hasattr(event, 'cmd'):
            params['cmd'] = event.cmd
        if hasattr(event, 'opacity'):
            params['opacity'] = event.opacity
        ll.ChangeLayer(event.layer, **params)


def main(giface=None):
    global Observer, RasterChangeHandler, DrawingChangeHandler
    from watchdog.observers import Observer
    from change_handler import RasterChangeHandler, DrawingChangeHandler
    dlg = TangibleLandscapePlugin(giface, parent=None)
    dlg.Show()


if __name__ == '__main__':
    gscript.parser()
    from watchdog.observers import Observer
    from change_handler import RasterChangeHandler, DrawingChangeHandler
    main()