# -*- coding: utf-8 -*-
"""
/***************************************************************************
 WiscSIMSTool
                                 A QGIS plugin
 useful tools for WiscSIMS sessions
 Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
                              -------------------
        begin                : 2020-08-01
        git sha              : $Format:%H$
        copyright            : (C) 2019 by WiscSIMS
        email                : kitajima@wisc.edu
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/
"""

from PyQt5.QtCore import (
    QSettings,
    QTranslator,
    qVersion,
    QCoreApplication,
    Qt,
    QSize,
    QModelIndex,
    QVariant,
    QRect,
)
from PyQt5.QtGui import (
    QIcon,
    QPixmap,
    QColor,
    QCursor,
)
from PyQt5.QtWidgets import (
    QAction,
    QInputDialog,
    QFileDialog,
    QAbstractItemView,
    QMessageBox,
    QLineEdit,
    QToolTip,
    QHeaderView,
    QDialog,
    QLineEdit,
    QDialogButtonBox,
    QVBoxLayout,
    QLabel,
)

from qgis.core import (
    QgsProject,
    QgsFeature,
    QgsVectorLayer,
    QgsField,
    QgsVectorFileWriter,
    QgsGeometry,
    QgsMarkerSymbol,
    QgsPalLayerSettings,
    QgsPointXY,
    QgsVectorLayerSimpleLabeling,
    QgsPropertyCollection,
    QgsFeatureRequest,
    QgsMapLayer,
    QgsWkbTypes,
    QgsEffectStack,
    QgsDropShadowEffect,
    QgsDrawSourceEffect,
    Qgis,
)

from qgis.gui import (
    QgsRubberBand,
    QgsMapCanvasAnnotationItem,
)

from qgis.core.additions.edit import edit


# Initialize Qt resources from file resources.py
from .resources import *  # noqa: F401, F403

# Import custom tools
from .tools.alignmentTool import AlignmentModel, AlignmentModelNew, AlignmentMarker
from .tools.canvasMapTool import CanvasMapTool

# from .tools.sumTableTool import SumTableTool
from .tools.sumTableToolNew import SumTableTool
from .tools.coordinateTool import CoordinateTool, CoordinateToolNew

# Import the code for the DockWidget
from .wiscsims_tool_dockwidget import WiscSIMSToolDockWidget
from .pixel_size_tool import PixelSizeTool
from .import_layer_option import ImportLayerOption

import os.path
import os
import re
import json
import math

from pandas import DataFrame


class AlignmentInfo(QDialog):
    def __init__(self, parent=None, info=None):
        QDialog.__init__(self)

        self.setWindowTitle("Aligment Information")

        QBtn = QDialogButtonBox.Ok

        self.buttonBox = QDialogButtonBox(QBtn)
        self.buttonBox.accepted.connect(self.accept)

        self.layout = QVBoxLayout()
        message = ""
        if info is not None:
            beampos = info["beam"]["position"]
            beamsize = info["beam"]["size"]
            stage = info["stage"]
            canvas = info["canvas"]
            refname = info["name"]

            message = QLabel(
                f'"{refname}"\n\nStage: ({stage.x():0.0f}, {stage.y():0.0f})\nCanvas: ({canvas.x():0.2f}, {canvas.y():0.2f})\nBeam Position: ({beampos[0]}, {beampos[1]})\nBeam Size: {beamsize} um'
            )

        self.layout.addWidget(message)
        self.layout.addWidget(self.buttonBox)
        self.setLayout(self.layout)


class WiscSIMSTool:
    """QGIS Plugin Implementation."""

    def __init__(self, iface):
        # self.debug = False
        """Constructor.

        :param iface: An interface instance that will be passed to this class
            which provides the hook by which you can manipulate the QGIS
            application at run time.
        :type iface: QgsInterface
        """

        self.debug = False

        # Save reference to the QGIS interface
        self.iface = iface

        self.window = self.iface.mainWindow()
        self.canvas = self.iface.mapCanvas()

        # Tool instances
        self.model = AlignmentModelNew()
        self.ref_marker = AlignmentMarker(self.canvas, self.model)
        self.cot = CoordinateToolNew()

        # initialize plugin directory
        self.plugin_dir = os.path.dirname(__file__)

        # initialize locale
        locale = QSettings().value("locale/userLocale")[0:2]
        locale_path = os.path.join(self.plugin_dir, "i18n", "WiscSIMSTool_{}.qm".format(locale))

        if os.path.exists(locale_path):
            self.translator = QTranslator()
            self.translator.load(locale_path)

            if qVersion() > "4.3.3":
                QCoreApplication.installTranslator(self.translator)

        # Declare instance attributes
        self.actions = []
        self.menu = self.tr("&WiscSIMS")
        # TODO: We are going to let the user set this up in a future iteration
        self.toolbar = self.iface.addToolBar("WiscSIMS")
        self.toolbar.setObjectName("WiscSIMS Tool")

        self.pluginIsActive = False
        # self.dockwidget = None

        self.rb_line = QgsRubberBand(self.canvas, QgsWkbTypes.LineGeometry)
        self.pxsize_line = QgsRubberBand(self.canvas, QgsWkbTypes.LineGeometry)

        self.start_point = []
        self.end_point = []
        self.line_in_progress = False
        self.prev_tool = None

        self.preset_points = []
        self.undo_preset = []

        self.scale = 1

        self.feature_id = None

        # self.scratchLayer = QgsVectorLayer("Point", "tmp",  "memory")
        # self.sc_dp = self.scratchLayer.dataProvider()
        # self.feature_id = None  # feature id for relocating preset point

        # maximum # of undo
        self.undo_max = 100

        self.flag_cancel_moving_spot = False

        self.state_shift_key = False
        self.state_ctrl_key = False
        self.state_alt_key = False
        self.mouse_move_counter = 0
        self.move_throttling_threshold = 3

        # defaut value for aligment parameters
        self.default_val = -999999999

        self.new_aliginment = True

        self.flag_pixel_size_tool = False

    # noinspection PyMethodMayBeStatic

    def tr(self, message):
        """Get the translation for a string using Qt translation API.

        We implement this ourselves since we do not inherit QObject.

        :param message: String for translation.
        :type message: str, QString

        :returns: Translated version of message.
        :rtype: QString
        """
        # noinspection PyTypeChecker,PyArgumentList,PyCallByClass
        return QCoreApplication.translate("WiscSIMS Tool", message)

    def add_action(
        self,
        icon_path,
        text,
        callback,
        enabled_flag=True,
        add_to_menu=True,
        add_to_toolbar=True,
        status_tip=None,
        whats_this=None,
        parent=None,
    ):
        """Add a toolbar icon to the toolbar.

        :param icon_path: Path to the icon for this action. Can be a resource
            path (e.g. ':/plugins/foo/bar.png') or a normal file system path.
        :type icon_path: str

        :param text: Text that should be shown in menu items for this action.
        :type text: str

        :param callback: Function to be called when the action is triggered.
        :type callback: function

        :param enabled_flag: A flag indicating if the action should be enabled
            by default. Defaults to True.
        :type enabled_flag: bool

        :param add_to_menu: Flag indicating whether the action should also
            be added to the menu. Defaults to True.
        :type add_to_menu: bool

        :param add_to_toolbar: Flag indicating whether the action should also
            be added to the toolbar. Defaults to True.
        :type add_to_toolbar: bool

        :param status_tip: Optional text to show in a popup when mouse pointer
            hovers over the action.
        :type status_tip: str

        :param parent: Parent widget for the new action. Defaults None.
        :type parent: QWidget

        :param whats_this: Optional text to show in the status bar when the
            mouse pointer hovers over the action.

        :returns: The action that was created. Note that the action is also
            added to self.actions list.
        :rtype: QAction
        """

        icon = QIcon(icon_path)
        action = QAction(icon, text, parent)
        action.triggered.connect(callback)
        action.setEnabled(enabled_flag)

        if status_tip is not None:
            action.setStatusTip(status_tip)

        if whats_this is not None:
            action.setWhatsThis(whats_this)

        if add_to_toolbar:
            self.toolbar.addAction(action)

        if add_to_menu:
            self.iface.addPluginToMenu(self.menu, action)

        self.actions.append(action)

        return action

    def initGui(self):
        """Create the menu entries and toolbar icons inside the QGIS GUI."""

        icon_path = ":/plugins/wiscsims_tool/img/icon.png"

        self.wiscsims_tool_action = self.add_action(
            icon_path, text=self.tr("WiscSIMS Tool [action]"), callback=self.run, parent=self.iface.mainWindow()
        )

        self.wiscsims_tool_action.setCheckable(True)

    # --------------------------------------------------------------------------

    def onClosePlugin(self):
        """Cleanup necessary items here when plugin dockwidget is closed"""

        # disconnects
        # self.dockwidget.closingPlugin.disconnect(self.onClosePlugin)
        self.wiscsims_tool_action.setChecked(False)
        # remove this statement if ockwidget is to remain
        # for reuse if plugin is reopened
        # Commented next statement since it causes QGIS crashe
        # when closing the docked window:

        self.remove_legend_connections()
        self.remove_ui_connections()

        # self.dockwidget = None
        delattr(self, "dockwidget")

        self.unsetMapTool()

        self.clear_preview_spots()

        self.pluginIsActive = False

    def unload(self):
        """Removes the plugin menu item and icon from QGIS GUI."""

        self.clear_preview_points()
        self.ref_marker.init_ref_point_markers()

        for action in self.actions:
            self.iface.removePluginMenu(self.tr("&WiscSIMS"), action)
            self.iface.removeToolBarIcon(action)
        # remove the toolbar
        del self.toolbar

    # --------------------------------------------------------------------------
    def run(self):
        """Run method that loads and starts the plugin"""
        print("run wiscsims tool")
        if self.pluginIsActive:
            print("already activated")
            if not self.wiscsims_tool_action.isChecked():
                print("set to active!!!!")
                self.wiscsims_tool_action.setChecked(True)
            self.init_map_tool()
            return

        self.pluginIsActive = True
        # dockwidget may not exist if:
        #    first run of plugin
        #    removed on close (see self.onClosePlugin method)
        if not hasattr("self", "dockwidget"):
            # Create the dockwidget (after translation) and keep reference
            plugin_path = ":plugins/wiscsims_tool"
            self.dockwidget = WiscSIMSToolDockWidget()
            icon_open_folder = QPixmap(os.path.join(plugin_path, "img", "icon_open_folder.png"))
            # icon_open_folder = os.path.join(plugin_path, 'img', 'icon_open_folder.png')
            self.dockwidget.Btn_Select_Workbook.setIcon(QIcon(icon_open_folder))
            self.dockwidget.Btn_Select_Workbook.setIconSize(QSize(16, 16))

            self.init_alignmentTableNew()

            self.deactivate_Workbook_Section()
            self.deactivate_Import_Section()

            self.activate_button(self.dockwidget.Btn_Import_Alignments)

            self.prev_workbook_path = ""
            self.workbook_path = ""

            self.init_preset_layer_combobox()

        # connect to provide cleanup on closing of dockwidget
        self.dockwidget.closingPlugin.connect(self.onClosePlugin)

        self.create_ui_connections()
        self.create_legend_connections()

        # show the dockwidget
        # TODO: fix to allow choice of dock location
        self.iface.addDockWidget(Qt.RightDockWidgetArea, self.dockwidget)
        self.dockwidget.show()

        self.init_map_tool()

        # in memory layer while moving the spot
        self.create_scratch_layer()

    def handle_layers_changed(self, layers):
        # if self.dockwidget.Cmb_Target_Layer.isEnabled():
        #     self.update_import_layers()
        self.init_preset_layer_combobox()

    def create_legend_connections(self):
        ltr = QgsProject.instance().layerTreeRoot()
        ltr.visibilityChanged.connect(self.handle_layers_changed)
        ltr.addedChildren.connect(self.handle_layers_changed)
        ltr.removedChildren.connect(self.handle_layers_changed)
        ltr.nameChanged.connect(self.handle_layers_changed)

    def remove_legend_connections(self):
        ltr = QgsProject.instance().layerTreeRoot()
        ltr.visibilityChanged.disconnect(self.handle_layers_changed)
        ltr.addedChildren.disconnect(self.handle_layers_changed)
        ltr.removedChildren.disconnect(self.handle_layers_changed)
        ltr.nameChanged.disconnect(self.handle_layers_changed)

    def create_ui_connections(self):
        dock = self.dockwidget

        # dock.Grp_Alignment.toggled.connect(self.toggle_use_alignment)
        dock.Tbv_Alignment.clicked.connect(self.change_tableview_selection)
        dock.Tbv_Alignment.doubleClicked.connect(self.handle_table_doubleClicked)
        dock.Gbx_Alignment_Ref_Point_Markers.toggled.connect(self.update_ref_point_markers)
        dock.Cbx_Alignment_Ref_Points.toggled.connect(self.update_ref_point_markers)
        dock.Cbx_Alignment_Ref_Names.toggled.connect(self.update_ref_point_markers)
        dock.Btn_Import_Alignments.clicked.connect(self.import_alignments)
        dock.Btn_Select_Workbook.clicked.connect(self.select_workbook)
        dock.Tbx_Workbook.textChanged.connect(self.workbook_updated)
        # dock.Btn_Create_New_Layer.clicked.connect(self.create_new_layer)
        # dock.Btn_Create_New_Layer.clicked.connect(self.test_import_dialog)
        # dock.Btn_Refresh_Import_Layers.clicked.connect(self.update_import_layers)
        dock.Btn_Import_From_Excel.clicked.connect(self.handle_import_dialog)

        dock.Cmb_Excel_From.currentIndexChanged.connect(self.update_n_importing_data)
        dock.Cmb_Excel_To.currentIndexChanged.connect(self.update_n_importing_data)
        dock.Tbx_Comment_Match.textChanged.connect(self.update_n_importing_data)
        dock.Opt_Range.toggled.connect(self.update_n_importing_data)

        dock.Tab_Preset_Mode.currentChanged.connect(self.preset_tool_changed)

        dock.Spn_Grid_Step_Size_X.valueChanged.connect(self.update_grid)
        dock.Spn_Grid_Step_Size_Y.valueChanged.connect(self.update_grid)
        dock.Spn_Grid_N_Point_X.valueChanged.connect(self.update_grid)
        dock.Spn_Grid_N_Point_Y.valueChanged.connect(self.update_grid)
        dock.Cmb_Grid_Move_Order.currentIndexChanged.connect(self.update_grid)

        dock.Cmb_Preset_Layer.currentIndexChanged.connect(self.handle_change_preset_layer)

        dock.Spn_Line_Step_Size.valueChanged.connect(self.update_line)
        dock.Spn_Line_N_Spot.valueChanged.connect(self.update_line)

        dock.Btn_Create_Preset_Layer.clicked.connect(self.create_preset_layer)

        dock.Btn_Grid_Add_Points.clicked.connect(self.add_preset_points)
        dock.Btn_Line_Add_Points.clicked.connect(self.add_preset_points)

        dock.Btn_Reset_Current_Number.clicked.connect(self.reset_current_number)
        dock.Btn_Undo_Add_Preset_Point.clicked.connect(self.handle_undo)
        dock.Btn_Refresh_Preset_Layers.clicked.connect(self.init_preset_layer_combobox)

        dock.Cmb_Preset_Layer.currentIndexChanged.connect(self.handle_change_preset_layer)
        dock.Spn_Preset_Pixel_Size.valueChanged.connect(self.handle_change_pixel_size)
        dock.Spn_Preset_Spot_Size.valueChanged.connect(self.handle_change_spot_size)
        dock.Btn_Preset_PxSizeCalc.clicked.connect(self.handle_pixel_size_tool)

        dock.Tbx_Comment.textChanged.connect(self.reset_current_number)
        dock.Tbx_Comment.textChanged.connect(self.handle_comment_change_preview)

    def remove_ui_connections(self):
        dock = self.dockwidget

        # dock.Grp_Alignment.toggled.disconnect(self.toggle_use_alignment)
        dock.Tbv_Alignment.clicked.disconnect(self.change_tableview_selection)
        dock.Gbx_Alignment_Ref_Point_Markers.toggled.disconnect(self.update_ref_point_markers)
        dock.Cbx_Alignment_Ref_Points.toggled.disconnect(self.update_ref_point_markers)
        dock.Cbx_Alignment_Ref_Names.toggled.disconnect(self.update_ref_point_markers)
        dock.Btn_Import_Alignments.clicked.disconnect(self.import_alignments)
        dock.Btn_Select_Workbook.clicked.disconnect(self.select_workbook)
        dock.Tbx_Workbook.textChanged.disconnect(self.workbook_updated)
        dock.Btn_Create_New_Layer.clicked.disconnect(self.create_new_layer)
        dock.Btn_Import_From_Excel.clicked.disconnect(self.import_from_excel)
        # dock.Btn_Refresh_Import_Layers.clicked.disconnect(self.update_import_layers)

        dock.Tab_Preset_Mode.currentChanged.disconnect(self.preset_tool_changed)

        dock.Spn_Grid_Step_Size_X.valueChanged.disconnect(self.update_grid)
        dock.Spn_Grid_Step_Size_Y.valueChanged.disconnect(self.update_grid)
        dock.Spn_Grid_N_Point_X.valueChanged.disconnect(self.update_grid)
        dock.Spn_Grid_N_Point_Y.valueChanged.disconnect(self.update_grid)
        dock.Cmb_Grid_Move_Order.currentIndexChanged.disconnect(self.update_grid)

        dock.Spn_Line_Step_Size.valueChanged.disconnect(self.update_line)
        dock.Spn_Line_N_Spot.valueChanged.disconnect(self.update_line)

        dock.Btn_Create_Preset_Layer.clicked.disconnect(self.create_preset_layer)

        dock.Btn_Grid_Add_Points.clicked.disconnect(self.add_preset_points)
        dock.Btn_Line_Add_Points.clicked.disconnect(self.add_preset_points)

        dock.Btn_Reset_Current_Number.clicked.disconnect(self.reset_current_number)
        dock.Btn_Undo_Add_Preset_Point.clicked.disconnect(self.undo_add_preset_point)
        dock.Btn_Refresh_Preset_Layers.clicked.disconnect(self.init_preset_layer_combobox)

        dock.Spn_Preset_Pixel_Size.valueChanged.disconnect(self.handle_change_pixel_size)
        dock.Spn_Preset_Spot_Size.valueChanged.disconnect(self.handle_change_spot_size)

        dock.Tbx_Comment.textChanged.disconnect(self.reset_current_number)
        dock.Tbx_Comment.textChanged.disconnect(self.handle_comment_change_preview)

    def init_map_tool(self):
        # check the plugin activation state
        if self.wiscsims_tool_action.isChecked():
            # when the user activate the WiscSIMS Tool
            self.prev_tool = self.canvas.mapTool()
            self.wiscsims_tool_action.setChecked(True)
            self.canvasMapTool = CanvasMapTool(self.canvas, self.wiscsims_tool_action)

            self.canvas.mapToolSet.connect(self.mapToolChanged)
            self.canvasMapTool.canvasClicked.connect(self.canvasClicked)  # released

            # start moving spot
            self.canvasMapTool.canvasClickedWShift.connect(self.canvasClickedWShift)
            # finish moving spot
            self.canvasMapTool.canvasReleaseWShift.connect(self.canvasReleaseWShift)
            # edit comment
            self.canvasMapTool.canvasReleaseWAlt.connect(self.canvasReleaseWAlt)
            # delete spot
            self.canvasMapTool.canvasReleaseWAltShift.connect(self.canvasReleaseWAltShift)

            self.canvasMapTool.canvasClickedRight.connect(self.canvasClickedRight)
            self.canvasMapTool.canvasMoved.connect(self.canvasMoved)

            self.canvasMapTool.canvasShiftKeyState.connect(self.canvasShiftKeyState)
            self.canvasMapTool.canvasCtrlKeyState.connect(self.canvasCtrlkeyState)
            self.canvasMapTool.canvasAltKeyState.connect(self.canvasAltKeyState)
            self.canvasMapTool.canvasEscapeKeyState.connect(self.canvasEscapeKeyState)
            self.canvasMapTool.canvasUndoKey.connect(self.handle_undo)

            self.canvas.setMapTool(self.canvasMapTool)
        else:
            # when the user deactivate the WiscSIMS Tool
            try:
                self.wiscsims_tool_action.setChecked(False)
                self.canvas.mapToolSet.disconnect(self.mapToolChanged)
                self.canvas.unsetMapTool(self.canvasMapTool)
                if not re.search("wiscsims_tool", self.prev_tool):
                    self.canvas.setMapTool(self.prev_tool)
                self.dockwidget.setEnabled(False)

            except Exception:
                pass

    def toggle_use_alignment(self, status):
        wb_status = True
        if status and not self.model.isAvailable():
            wb_status = False
        self.dockwidget.Grp_Workbook.setEnabled(wb_status)

    def init_alignmentTableNew(self):
        tbl = self.dockwidget.Tbv_Alignment
        tbl.setModel(self.model)
        hiddenColumns = ["stage", "canvas", "beam"]
        [tbl.setColumnHidden(self.model.getColumnIndex(c), True) for c in hiddenColumns]
        w = {"0": 20, "2": 0, "3": 0, "4": 0}
        [tbl.setColumnWidth(c, w[str(c)]) for c in [0, 2, 3, 4]]
        [tbl.horizontalHeader().setSectionResizeMode(c, QHeaderView.Fixed) for c in [0, 2, 3, 4]]
        tbl.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch)

        tbl.verticalHeader().setDefaultSectionSize(22)
        tbl.verticalHeader().setSectionResizeMode(QHeaderView.Fixed)
        tbl.setSelectionBehavior(QAbstractItemView.SelectRows)
        tbl.setSelectionMode(QAbstractItemView.SingleSelection)
        tbl.clicked.connect(self.handle_tbl_clicked)
        self.model.itemChanged.connect(self.handle_item_changed)
        self.model.refPtUpdated.connect(self.handle_ref_point_updated)

    def handle_tbl_clicked(self):
        pass

    def handle_table_doubleClicked(self, clickedRow):
        r = clickedRow.row()
        alinfo = {
            "beam": self.model.getBeam(r),
            "stage": self.model.getStagePosition(r),
            "canvas": self.model.getCanvasPosition(r),
            "name": self.model.getRefName(r),
        }

        ex = AlignmentInfo(self, info=alinfo)
        if ex.exec():
            print("success")
        else:
            print("Canel!")

        print("double click")

    def handle_item_changed(self):
        pass

    def handle_ref_point_updated(self):
        pass

    def init_alignmentTable(self):
        self.dockwidget.Tbv_Alignment.setModel(self.model)
        hiddenColumns = ["point_1", "point_2", "scale", "offset", "rotation", "ref1", "ref2"]
        [self.dockwidget.Tbv_Alignment.setColumnHidden(self.model.getColumnIndex(c), True) for c in hiddenColumns]
        self.dockwidget.Tbv_Alignment.setColumnWidth(0, 20)
        self.dockwidget.Tbv_Alignment.horizontalHeader().setStretchLastSection(True)
        self.dockwidget.Tbv_Alignment.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.dockwidget.Tbv_Alignment.setSelectionMode(QAbstractItemView.SingleSelection)

    def change_tableview_selection(self, index):
        if type(index) == QModelIndex:
            if index.column() == 0:
                return
            row = index.row()
        else:
            row = index
        # stage1, canvas1 = self.model.getRefPoint(row, 1)
        # stage2, canvas2 = self.model.getRefPoint(row, 2)
        # scale, offset, rotation = self.model.getAlignmentParams(row)
        self.update_ref_point_markers()

    def import_alignments(self, alnFile=None):
        project_path = os.path.dirname(QgsProject.instance().fileName())
        if not alnFile:
            alnFile, _ = QFileDialog.getOpenFileName(
                self.window,
                "Open Stage Navigator alignment file",
                project_path,
                "Stage Navigator alignment file (*.json)",
            )

        self.dockwidget.Grp_Workbook.setEnabled(False)
        if alnFile == "":
            return

        self.deactivate_button(self.dockwidget.Btn_Select_Workbook)
        self.deactivate_button(self.dockwidget.Btn_Import_From_Excel)

        with open(alnFile) as data_file:
            obj = json.load(data_file)

            self.new_aliginment = self.model.isNewFormat(obj)

            if not self.new_aliginment:
                # AlignmentModelNew is default
                self.model = AlignmentModel()

            if self.model.isAvailable():
                self.model.clear()

            if self.new_aliginment:
                self.model = AlignmentModelNew()
                self.cot = CoordinateToolNew()
                self.init_alignmentTableNew()
            else:
                self.model = AlignmentModel()
                self.cot = CoordinateTool()
                self.init_alignmentTable()

            self.model.ImportFromsJson(obj)
            # params = self.getParams()
            # self.canvas.setRotation(params['rotation'])
            self.dockwidget.Grp_Workbook.setEnabled(True)
            # calculate average scale for preset
            scales = self.model.getScales()
            average_scale = sum(scales) / len(scales)

            # set spot size
            arows = self.model.getAvailableRows()
            spot_sizes = [self.model.getBeam(r)["size"] for r in arows]
            mean_spot_size = self.model.getAverage(spot_sizes)
            self.dockwidget.Spn_Spot_Size.setValue(round(mean_spot_size))

            #
            # set average scale to preset scales
            self.dockwidget.Spn_Preset_Pixel_Size.setValue(average_scale)
            self.update_ref_point_markers()
            self.update_canvas_rotation()

        self.deactivate_button(self.dockwidget.Btn_Import_Alignments)
        self.activate_button(self.dockwidget.Btn_Select_Workbook)

    def is_default_values(self, pt):
        return pt[0] == self.default_val and pt[1] == self.default_val

    def update_ref_point_markers(self):
        self.ref_marker.init_ref_point_markers()
        # shwo ref points?
        if not self.dockwidget.Gbx_Alignment_Ref_Point_Markers.isChecked():
            return

        alignments = self.model.ExportAsObject()
        selected_alignment = self.dockwidget.Tbv_Alignment.currentIndex().row()

        print(f"New Alignment: {self.new_aliginment}")

        if self.new_aliginment:
            for aln in alignments:
                if aln["used"] < 2 and aln["r"] != selected_alignment:
                    continue
                current_flag = selected_alignment == aln["r"]
                if not self.is_default_values(aln["stage"]):
                    self.handle_ref_marker(aln["canvas"], aln["refname"], current_flag)
        else:
            for aln in alignments:
                current_flag = selected_alignment == aln["r"]
                self.handle_ref_marker(aln["point_1"][1], aln["refname"] + " (1)", current_flag)
                self.handle_ref_marker(aln["point_2"][1], aln["refname"] + " (2)", current_flag)
            self.update_canvas_rotation()

    def update_canvas_rotation(self):
        if self.model.isAvailable():
            params = self.model.getParams()
            self.canvas.setRotation(params["rotation"])

    def handle_ref_marker(self, pt, name, current=False):
        # print(pt, name)
        if self.dockwidget.Cbx_Alignment_Ref_Points.isChecked():
            self.ref_marker.add_ref_marker(pt, name, current)
        if self.dockwidget.Cbx_Alignment_Ref_Names.isChecked():
            self.ref_marker.add_ref_marker_annotation(pt, name, current)

    def select_workbook(self):
        title = "Select WiscSIMS sesssion Workbook"
        mypath = os.path.dirname(QgsProject.instance().fileName())
        self.workbook_path, _ = QFileDialog.getOpenFileName(self.window, title, mypath, "Excel (*.xls *.xlsx)")

        if self.workbook_path == "":
            return
        if self.prev_workbook_path == self.workbook_path:
            return

        self.xl = SumTableTool(self.workbook_path)
        if self.xl.ws is None:
            QMessageBox.warning(
                self.window,
                "Excel File Selection",
                "Open appropriate workbook in Excel, then select the workbook.\n\n"
                'The workbook must have a sheet named "Sum_table" or "Data".',
            )
            self.workbook_path = None
            # self.dockwidget.Grp_Layer.setEnabled(False)
            self.deactivate_Import_Section()
        else:
            self.prev_workbook_path = self.workbook_path
            self.dockwidget.Tbx_Workbook.setText(os.path.basename(self.workbook_path))
            self.deactivate_button(self.dockwidget.Btn_Select_Workbook)

    def is_ok_to_import(self):
        """check importing condition"""
        if not self.model.isAvailable():
            """ use alignment but no alignment file imported """
            return False

        if self.xl.ws is None:
            """ no excel file """
            return False

        # if self.dockwidget.Cmb_Target_Layer.currentIndex() == -1:
        #     """ no layer is selected """
        #     return False
        return True

    def update_conv_params(self):
        if self.model.isAvailable():
            # Get available rows (indexes)
            available_refs = self.model.getAvailableRows()
            self.conv_params = {}
            for l in available_refs:
                for r in available_refs:
                    if l >= r:
                        continue
                    # Get combinations of indexes
                    key = f"{l}-{r}"
                    # Calculate ConvertParams
                    scale, offset, rotation = self.cot.getConvertParams(
                        self.model.getStagePosition(l),
                        self.model.getCanvasPosition(l),
                        self.model.getStagePosition(r),
                        self.model.getCanvasPosition(r),
                    )
                    self.conv_params[key] = {
                        "scale": scale,
                        "offset": offset,
                        "rotation": rotation,
                    }

            self.update_canvas_rotation()
        self.update_ref_point_markers()

    def sort_by_distance(self, pt, ref_canvas_pt):
        out = {}
        if not isinstance(pt, QgsPointXY):
            pt = QgsPointXY(pt[0], pt[1])
        for k, v in ref_canvas_pt.items():
            out[k] = pt.distance(v[0], v[1])
        # sorted by distance
        retval = [(k, v) for k, v in sorted(out.items(), key=lambda item: item[1])]
        return retval

    def get_conversion_models(self, pt, ref_pos, conv_params, limit=2):
        sorted_idxes = self.sort_by_distance(pt, ref_pos)
        # if limit less than 2, all conv_params are going to be used
        sorted_idxes = sorted_idxes[:limit]

        new_conv_params = []
        out = {}
        for j in sorted_idxes:
            for k in sorted_idxes:
                if j[0] >= k[0]:
                    continue
                key = f"{j[0]}-{k[0]}"
                new_conv_params.append(conv_params[key])
                weight = 1 / (j[1] + k[1]) ** 2
                out[key] = weight
                new_conv_params[-1]["weight"] = weight
        wt_sum = sum(out.values())
        if self.debug:
            print([round(100 * a / wt_sum, 1) for a in out.values() if a > 0.0])
        return new_conv_params

    def update_n_importing_data(self):
        d = self.get_importing_data()
        if not isinstance(d, DataFrame):
            self.dockwidget.Txt_N_Importing_Data.setText("")
            return

        _r, _c = d.shape
        self.dockwidget.Txt_N_Importing_Data.setText(f"{_r} spot{'s' if _r > 1 else ''} will be imported")

    def get_importing_data(self):
        importing_data = DataFrame()
        # import date from Excel file
        if self.dockwidget.Opt_Comment.isChecked():
            importing_data = self.xl.filter_by_comment(self.dockwidget.Tbx_Comment_Match.text())
        else:
            start_idx = self.dockwidget.Cmb_Excel_From.currentIndex()
            end_idx = self.dockwidget.Cmb_Excel_To.currentIndex()
            if start_idx < 0 or end_idx < 0:
                return importing_data
            if start_idx > end_idx:
                start_idx, end_idx = end_idx, start_idx
                self.dockwidget.Cmb_Excel_From.setCurrentIndex(start_idx)
                self.dockwidget.Cmb_Excel_To.setCurrentIndex(end_idx)
            start_asc = self.dockwidget.Cmb_Excel_From.itemData(start_idx)
            end_asc = self.dockwidget.Cmb_Excel_To.itemData(end_idx)
            importing_data = self.xl.filter_by_asc(start=start_asc, end=end_asc)

        if importing_data is None:
            return DataFrame()

        return importing_data

    def handle_import_dialog(self):
        # get available import layers
        self.excel_layers = self.get_excel_layers(self.xl)

        # open dialogn with params (layers)
        layers = [layer.name() for layer in self.excel_layers]
        layers.insert(0, "New Layer")

        self.import_layer_opt = ImportLayerOption(self.window, layers)
        self.import_layer_opt.accepted.connect(self.handle_import_dialog_accepted)
        self.import_layer_opt.rejected.connect(self.handle_import_dialog_canceled)

        if len(self.excel_layers) == 0:
            self.handle_import_dialog_accepted()
            return

        self.import_layer_opt.show()

    def handle_import_dialog_accepted(self):
        selected_index = self.import_layer_opt.Cmb_Layer_Option.currentIndex()
        self.import_layer_opt.close()

        self.target_layer = QgsVectorLayer()

        if selected_index == 0:
            # create new layer

            # set new layer as a target layer
            self.target_layer = self.create_new_layer()
        else:
            # set selected layer as a target layer
            self.target_layer = self.excel_layers[selected_index - 1]

        if self.target_layer.geometryType() != Qgis.GeometryType.Point:
            print("invalid layer")
            return

        # import Excel data to the target layer
        self.import_from_excel()

    def handle_import_dialog_canceled(self):
        self.import_layer_opt.close()

    def import_from_excel(self):
        if not self.is_ok_to_import():
            return

        self.deactivate_button(self.dockwidget.Btn_Select_Workbook)
        self.deactivate_button(self.dockwidget.Btn_Import_Alignments)

        # import date from Excel file
        importing_data = self.get_importing_data()

        features = []

        # get conversion model
        self.update_conv_params()

        i = 0
        # is_direct_import = not self.dockwidget.Grp_Alignment.isChecked()
        #
        _r, _c = importing_data.shape

        for r in range(_r):
            d = dict(importing_data.iloc[r])

            if d["X"] == "" or d["Y"] == "":
                continue
            i += 1
            # if is_direct_import:
            #     canvasX, canvasY = [d["X"], d["Y"]]
            # else:
            conv_model = self.get_conversion_models(
                [d["X"], d["Y"]], self.model.getStagePositions(), self.conv_params, 2
            )
            canvasX, canvasY = self.cot.getWtAveragedStageToCanvas([d["X"], d["Y"]], conv_model)
            feature = QgsFeature()
            feature.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(canvasX, canvasY)))
            vals = list(d.values())
            feature.setAttributes(vals)
            features.append(feature)

        dpr = self.target_layer.dataProvider()
        dpr.addFeatures(features)
        self.target_layer.setDisplayExpression("File")
        self.target_layer.triggerRepaint()
        QMessageBox.information(self.window, "Import Excel Data", f"{i} data were imported!")

    # self.canvas.refresh()

    def workbook_updated(self):
        # addItems asc files
        if self.xl.ws is None:
            self.xl = SumTableTool(self.workbook_path)
        asc_list = self.xl.get_asc_list()
        # asc_list_s = map(lambda x: x[8:], asc_list)

        # initialize from/to @xxx combobox
        self.dockwidget.Cmb_Excel_From.clear()
        self.dockwidget.Cmb_Excel_To.clear()

        # add @xxx to from/to combobox
        [self.dockwidget.Cmb_Excel_From.addItem(asc[8:], asc) for asc in asc_list]
        [self.dockwidget.Cmb_Excel_To.addItem(asc[8:], asc) for asc in asc_list]

        # set selected item in the "to" combobox
        self.dockwidget.Cmb_Excel_To.setCurrentIndex(len(asc_list) - 1)

        # select Opt_Range radiobox
        self.dockwidget.Opt_Range.setChecked(True)

        # update number of importing data
        self.update_n_importing_data()

        # activate "#3 group box (import data button)"
        self.activate_Import_Section()

    def activate_button(self, btn):
        styles = "background-color: rgb(255, 49, 49); color: #ffffff;"
        btn.setStyleSheet(styles)
        btn.setEnabled(True)

    def deactivate_button(self, btn):
        styles = "background-color: none; color: none;"
        btn.setStyleSheet(styles)

    def activate_Workbook_Section(self):
        self.dockwidget.Grp_Workbook.setEnabled(True)
        self.deactivate_button(self.dockwidget.Btn_Import_Alignments)
        self.deactivate_button(self.dockwidget.Btn_Import_From_Excel)
        self.activate_button(self.dockwidget.Btn_Select_Workbook)

    def deactivate_Workbook_Section(self):
        self.dockwidget.Grp_Workbook.setEnabled(False)
        self.deactivate_button(self.dockwidget.Btn_Select_Workbook)

    def activate_Import_Section(self):
        self.dockwidget.Grp_Import_Excel_Data.setEnabled(True)
        self.deactivate_button(self.dockwidget.Btn_Import_Alignments)
        self.deactivate_button(self.dockwidget.Btn_Select_Workbook)
        self.activate_button(self.dockwidget.Btn_Import_From_Excel)

    def deactivate_Import_Section(self):
        self.dockwidget.Grp_Import_Excel_Data.setEnabled(False)
        self.deactivate_button(self.dockwidget.Btn_Import_From_Excel)

    def create_new_layer(self, fpath=None):
        project_path = os.path.dirname(QgsProject.instance().fileName())
        saveFile, _ = QFileDialog.getSaveFileName(
            self.window, "Save a Shape file", project_path, "ESRI Shapefile (*.shp)"
        )

        if saveFile == "":
            return None

        vl = QgsVectorLayer("Point?crs=epsg:4326", "temporary_points", "memory")
        vl.setProviderEncoding("UTF-8")
        pr = vl.dataProvider()
        labels = self.xl.get_headers()
        attrs = [QgsField(label, self.get_field_type(label)) for label in labels]

        pr.addAttributes(attrs)
        vl.setDisplayExpression("File")
        vl.updateFields()

        reg = QgsProject.instance()
        # Remove layers which have same souce file as shape file to be overwritten
        layers = [l.source() for l in self.get_layer_list(layertype=0)]
        [reg.removeMapLayer(layer) for layer in layers if layer == saveFile]

        # write shape + etc files
        error, _ = QgsVectorFileWriter.writeAsVectorFormat(vl, saveFile, "UTF-8", vl.crs(), "ESRI Shapefile")
        if not fpath:
            fpath = self.xl.get_workbook_name()
        parsedPath = os.path.basename(fpath).split(".")[:-1]
        layerName = ".".join(parsedPath)
        text, okPressed = QInputDialog.getText(
            self.window, "Input Layer Name", "Layer name: ", QLineEdit.Normal, layerName
        )
        if okPressed:
            layerName = text

        if error == QgsVectorFileWriter.NoError:
            newVlayer = QgsVectorLayer(saveFile, layerName, "ogr")
            newVlayer.setCustomProperty("workbookPath", fpath)
            props = newVlayer.renderer().symbol().symbolLayer(0).properties()
            props["name"] = "circle"
            props["size_unit"] = "MapUnit"
            props["offset_unit"] = "MapUnit"
            props["outline_width_unit"] = "MapUnit"
            spotSize = 1.0 * self.dockwidget.Spn_Spot_Size.value()
            if self.model.isAvailable():
                scale = self.get_average(self.model.getScales())
            else:
                scale = 1.0
            # scale = self.ct[self.getNavAlign()].scale / 1.0
            props["size"] = str(spotSize / scale)
            mySimpleSymbol = QgsMarkerSymbol.createSimple(props)
            newVlayer.renderer().setSymbol(mySimpleSymbol)
            newVlayer.updateExtents()
            insta = QgsProject.instance()
            insta.addMapLayer(newVlayer)

            # show feature count
            root = insta.layerTreeRoot()
            myLayerNode = root.findLayer(newVlayer.id())
            myLayerNode.setCustomProperty("showFeatureCount", True)

            self.iface.setActiveLayer(newVlayer)

            return newVlayer

        return None

    def get_fields(self, layer):
        return [f.name() for f in layer.fields()]

    def get_field_type(self, fieldName):
        if re.match(r"^(file|comment|sample|date|time)", fieldName, re.I):
            return QVariant.String
        return QVariant.Double

    def intersect(self, list1, list2):
        return [l for l in list1 if l in list2]

    def get_layer_list(self, layertype=-1):
        # Layer Types
        #  Vector: 0
        #  Raster: 1
        #  Plugin: 2
        #  Mesh:   3
        layers = QgsProject.instance().mapLayers().values()
        if layertype == -1:
            return layers
        # Filter by layer type.
        return [layer for layer in layers if layer.type() == layertype]

    def get_vector_point_layers(self):
        # Filter by layer type 0: vector
        layers = self.get_layer_list(layertype=0)
        # [print(l.dataProvider().name()) for l in layers]
        # filter only point geometry layer 0: point and not memory layers
        return [l for l in layers if l.geometryType() == 0 and l.dataProvider().name() != "memory"]

    def get_true_shapefile_headers(self, headers):
        # create tmporal shapefile with provided headers
        tmp_file_path = "KK_tmp_file.shp"
        vl = QgsVectorLayer("Point?crs=epsg:4326", "temporary_points", "memory")
        vl.setProviderEncoding("UTF-8")
        pr = vl.dataProvider()
        attrs = [QgsField(header, self.get_field_type(header)) for header in headers]

        pr.addAttributes(attrs)
        vl.setDisplayExpression("File")
        vl.updateFields()
        error, _error_str = QgsVectorFileWriter.writeAsVectorFormat(
            vl, tmp_file_path, "UTF-8", vl.crs(), "ESRI Shapefile"
        )
        if error != QgsVectorFileWriter.NoError:
            print(_error_str)
            return
        # open shapefile, then get and return headers
        tmp_layer = QgsVectorLayer(tmp_file_path, "kk", "ogr")
        fields = self.get_fields(tmp_layer)

        return fields

    def is_identical_field_names(self, layer, xl_fields, field_max_len=5):
        my_fields = self.get_fields(layer)
        my_fileds_names = [f[:field_max_len] for f in my_fields]
        xl_fields_len = len(self.intersect(my_fileds_names, xl_fields))
        return xl_fields_len == len(my_fields)

    def get_excel_layers(self, xl):
        field_max_len = 5
        # field names in vector layers have been chopped
        if xl.ws is None:
            return []
        xl_fields = [h[:field_max_len] for h in xl.get_headers()]
        legend = QgsProject.instance().layerTreeRoot()
        try:
            vec_layers = self.get_vector_point_layers()
            layers = [layer for layer in vec_layers if legend.findLayer(layer.id()).isVisible()]
        except Exception:
            return []

        return [l for l in layers if self.is_identical_field_names(l, xl_fields, field_max_len)]

    """
    Functions for Aliginment and Coordinate Calculations
    """

    def get_average(self, arr):
        return sum(arr) / len(arr) * 1.0

    def getParams(self):
        if not self.model.isAvailable():
            return None
        scales = self.model.getScales()
        rotations = self.model.getRotations()
        return {"scale": self.get_average(scales), "rotation": self.get_average(rotations)}

    def my_round_int(self, val):
        return int((val * 2 + 1) // 2)

    def getWtAverageStageToCanvas(self, pt):
        model = self.model.ExportAsObject()
        weights, wpt_x, wpt_y, np = [], [], [], [0, 0]
        tmp_out = []
        for m in model:
            if m["used"] == 0:
                continue
            params = [m["scale"], m["offset"], m["rotation"]]
            for i in [1, 2]:
                pt_name = "point_" + str(i)
                d = self.cot.getDistance(pt, m[pt_name][0])
                if d == 0:
                    d = 1e-10
                w = 1.0 / (d**2)
                if i == 1:
                    np = self.cot.toCanvasCoordinates2(pt, params)
                tmp_out.append(w)
                weights.append(w)
                wpt_x.append(w * np[0])
                wpt_y.append(w * np[1])
        sum_weights = sum(weights)
        return [p / sum_weights for p in [sum(wpt_x), sum(wpt_y)]]

    """
    Functions for Preset
    """

    def init_preset_layer_combobox(self):
        self.is_preset_initializing = True
        # get current index (selected item) for refresh contents

        current_layer_index = self.dockwidget.Cmb_Preset_Layer.currentIndex()
        # layer = None
        # if current_layer_index > -1:
        #     layer = self.dockwidget.Cmb_Preset_Layer.itemText(current_layer_index)

        # clear current items
        self.dockwidget.Cmb_Preset_Layer.clear()
        # Add layers which have Vector, Point geometry and 'Comment' field to combobox for preset
        layers = self.get_vector_point_layers()

        # filter scratch layer
        layers = [l for l in layers if l.name() != "tmp" and l.storageType() != "Memory storage"]

        if len(layers) == 0:
            return
        # filter layers which has 'Comment' field
        layers = [layer for layer in layers if "Comment" in self.get_fields(layer)]
        # add to Cmb_Preset_Layer
        [self.dockwidget.Cmb_Preset_Layer.addItem(l.name(), l) for l in layers]
        if current_layer_index > -1:
            self.dockwidget.Cmb_Preset_Layer.setCurrentIndex(current_layer_index)

        self.is_preset_initializing = False

        self.handle_change_preset_layer(current_layer_index)

    def handle_change_preset_layer(self, idx=-1):
        if self.is_preset_initializing:
            return

        if idx < 0:
            return

        ssps = self.get_ss_ps()

        self.dockwidget.Spn_Preset_Pixel_Size.setValue(ssps["ps"])
        self.dockwidget.Spn_Preset_Spot_Size.setValue(ssps["ss"])

        self.handle_change_pixel_size(ssps["ps"])

    def create_preset_layer(self):
        # ask layer name
        project_path = os.path.dirname(QgsProject.instance().fileName())
        saveFile, _filter = QFileDialog.getSaveFileName(
            self.window, "Save a Shape file", os.path.join(project_path, "preset.shp"), "ESRI Shapefile (*.shp)"
        )

        if saveFile == "":
            return None

        # create layer with given name
        #  field: [Comment]
        spot_size = self.dockwidget.Spn_Preset_Spot_Size.value()
        pixel_size = self.dockwidget.Spn_Preset_Pixel_Size.value()
        vl = QgsVectorLayer("Point?crs=epsg:4326", "temporary_points", "memory")
        vl.setProviderEncoding("UTF-8")
        pr = vl.dataProvider()
        attrs = [
            QgsField("Comment", QVariant.String),
            QgsField(f"ss_{spot_size}", QVariant.Int),
            QgsField(f"ps_{pixel_size}", QVariant.Double),
        ]

        pr.addAttributes(attrs)
        vl.setDisplayExpression("Comment")
        vl.updateFields()

        error, _error_str = QgsVectorFileWriter.writeAsVectorFormat(vl, saveFile, "UTF-8", vl.crs(), "ESRI Shapefile")

        # Set layer name without duplication
        layer_names = [l.name() for l in self.get_vector_point_layers()]
        layer_name = "Preset"

        # check layer names to avoid name duplication
        i = 1
        while layer_name in layer_names:
            i += 1
            layer_name = "Preset_{}".format(i)

        spot_size = self.dockwidget.Spn_Preset_Spot_Size.value()
        if error != QgsVectorFileWriter.NoError:
            print(_error_str)
            return

        newVlayer = QgsVectorLayer(saveFile, layer_name, "ogr")

        props = newVlayer.renderer().symbol().symbolLayer(0).properties()
        props["name"] = "circle"
        props["size_unit"] = "MapUnit"
        props["offset_unit"] = "MapUnit"
        props["outline_width_unit"] = "MapUnit"
        props["size"] = f"{1.0 * self.scale * spot_size}"  # spot size
        mySimpleSymbol = QgsMarkerSymbol.createSimple(props)
        newVlayer.renderer().setSymbol(mySimpleSymbol)
        newVlayer.updateExtents()

        # load and register created layer to mapCanvas
        QgsProject.instance().addMapLayer(newVlayer)
        self.iface.setActiveLayer(newVlayer)
        # self.iface.activeLayer().renderer().symbol().symbolLayer(0).setSizeUnit(1)

        settings = QgsPalLayerSettings()
        settings.enabled = True
        settings.fieldName = "Comment"
        settings.displayAll = True
        coll = QgsPropertyCollection("preset")
        # coll.setProperty(settings.ShadowDraw, True)
        # coll.setProperty(settings.BufferDraw, True)
        settings.setDataDefinedProperties(coll)

        newVlayer.setLabelsEnabled(True)
        newVlayer.setLabeling(QgsVectorLayerSimpleLabeling(settings))
        newVlayer.triggerRepaint()

        # show feature count
        root = QgsProject.instance().layerTreeRoot()
        myLayerNode = root.findLayer(newVlayer.id())
        myLayerNode.setCustomProperty("showFeatureCount", True)

        # # add created layer to Cmb_Preset_Layer
        # self.init_preset_layer_combobox()

        # select/highlight created layer
        self.dockwidget.Cmb_Preset_Layer.setCurrentIndex(self.dockwidget.Cmb_Preset_Layer.findText(layer_name))

        self.create_scratch_layer()

    def store_ss_ps(self, data):
        layer = self.get_preset_layer()
        layer.startEditing()
        # Rename field
        flag_rename = False
        for field in layer.fields():
            if field.name()[:3] == "ss_":
                idx = layer.fields().indexFromName(field.name())
                layer.renameAttribute(idx, f"ss_{data['ss']}")
                flag_rename = True
            elif field.name()[:3] == "ps_":
                idx = layer.fields().indexFromName(field.name())
                layer.renameAttribute(idx, f"ps_{data['ps']}")
                flag_rename = True

        if not flag_rename:  # no ss_ or ps_
            pr = layer.dataProvider()
            attrs = [QgsField(f"ss_{data['ss']}", QVariant.Int), QgsField(f"ps_{data['ps']}", QVariant.Double)]
            pr.addAttributes(attrs)

        # Close editing session and save changes
        layer.commitChanges()

    def get_ss_ps(self):
        layer = self.get_preset_layer()
        fields = self.get_fields(layer)
        out = {"ss": 12, "ps": 1.0}
        for f in fields:
            c = f[:3]
            if c == "ss_":
                out["ss"] = int(f.split("_")[1])
                continue
            elif c == "ps_":
                out["ps"] = float(f.split("_")[1])
        return out

    def handle_change_pixel_size(self, size):
        self.scale = 1 / size
        current_mode = self.get_preset_mode()
        if not self.flag_pixel_size_tool:
            if current_mode == "grid":
                self.update_grid()
            elif current_mode == "line":
                self.update_line()

        ss = self.dockwidget.Spn_Preset_Spot_Size.value()
        self.handle_change_spot_size(ss)

    def handle_set_pixel_size(self):
        px_size = self.pixel_size_tool_win.SPN_PixelSize.value()
        # - close PST window
        self.pixel_size_tool_win.close()
        # - set flag to False
        self.flag_pixel_size_tool = False
        # - change button state
        self.dockwidget.Btn_Preset_PxSizeCalc.setChecked(False)
        # - set pixel size
        # this will automatically change spot size in preset layer
        self.dockwidget.Spn_Preset_Pixel_Size.setValue(px_size)
        # - delete rubberbands
        self.init_px_size_tool()

    def handle_cancel_pixel_size(self):
        print("cancel/close PST")
        # - close PST window
        self.pixel_size_tool_win.close()
        # - set flag to False
        self.flag_pixel_size_tool = False
        # - change button state
        self.dockwidget.Btn_Preset_PxSizeCalc.setChecked(False)
        # - delete rubberbands
        self.init_px_size_tool()

    def handle_pixel_size_tool(self):
        params = {}
        self.init_px_size_tool()
        if self.flag_pixel_size_tool:
            self.flag_pixel_size_tool = False
            self.pixel_size_tool_win.close()
            print("after closed PST window: ", self.pixel_size_tool_win)
        else:
            self.pixel_size_tool_win = PixelSizeTool(self.window, params)

            # self.pixel_size_tool_win.setPxSize.connect(self.handle_set_pixel_size)
            # self.pixel_size_tool_win.canceled.connect(self.handle_cancel_pixel_size)

            self.pixel_size_tool_win.accepted.connect(self.handle_set_pixel_size)
            self.pixel_size_tool_win.rejected.connect(self.handle_cancel_pixel_size)

            self.flag_pixel_size_tool = True
            retval = self.pixel_size_tool_win.show()
            print(f"retval: {retval}")

            return

    def init_px_size_tool(self):
        if self.pxsize_line.size() > 0:
            self.pxsize_line.reset()
        self.pxsize_line = QgsRubberBand(self.canvas, QgsWkbTypes.LineGeometry)
        self.pxsize_line.setWidth(4)
        self.pxsize_line.setColor(QColor(25, 105, 200, 60))

        self.pxsize_start_point = QgsPointXY()
        self.pxsize_end_point = QgsPointXY()

    def handle_change_spot_size(self, size):
        preset_layer = self.dockwidget.Cmb_Preset_Layer
        if preset_layer.currentIndex() < 0:
            return

        layer = preset_layer.itemData(preset_layer.currentIndex())
        layer.renderer().symbol().symbolLayer(0).setSize(1.0 * self.scale * size)
        layer.triggerRepaint()

        ss = self.dockwidget.Spn_Preset_Spot_Size.value()
        ps = self.dockwidget.Spn_Preset_Pixel_Size.value()

        # save spot and pixel size to the attribute table (shapefile) as field names
        self.store_ss_ps({"ss": ss, "ps": ps})

        self.update_preview_spots()
        self.update_scratch_layer_symbol_size()

    def update_preview_spots(self):
        if self.flag_pixel_size_tool:
            return
        current_mode = self.get_preset_mode()
        if current_mode == "grid":
            self.update_grid()
        elif current_mode == "line":
            self.update_line()

    def reset_current_number(self, btn):
        # hit reset button => btn = bool
        # comment changed => btn = unicode
        if isinstance(btn, str) and not self.dockwidget.Cbx_Reset_Increment_Number.isChecked():
            return
        self.dockwidget.Spn_Current_Number.setValue(1)

    def get_current_tool(self):
        tools = ["import", "preset", "alignment"]
        return tools[self.dockwidget.Tab_Tool.currentIndex()]

    def get_preset_layer(self):
        layer = self.dockwidget.Cmb_Preset_Layer.itemData(self.dockwidget.Cmb_Preset_Layer.currentIndex())
        return layer

    def get_preset_mode(self):
        modes = ["point", "line", "grid"]
        return modes[self.dockwidget.Tab_Preset_Mode.currentIndex()]

    def add_preset_points(self):
        self.remove_objects_from_scratch_layer()

        if len(self.preset_points) == 0:
            return

        if self.dockwidget.Cmb_Preset_Layer.currentIndex() == -1:
            return

        layer = self.get_preset_layer()
        layer.startEditing()
        fields = layer.fields()
        features = []
        for p in self.preset_points:
            feature = QgsFeature()
            feature.setFields(fields)
            geom = QgsGeometry.fromPointXY(QgsPointXY(p[1], p[2]))
            comment = p[0]
            feature.setGeometry(geom)
            feature["Comment"] = comment
            features.append(feature)
        dpr = layer.dataProvider()
        results, newFeatures = dpr.addFeatures(features)
        layer.commitChanges()
        points_for_undo = [f.id() for f in newFeatures]
        self.undo_preset.append({"type": "addition", "data": points_for_undo})

        # check number of undos
        if len(self.undo_preset) > self.undo_max:
            del self.undo_preset[0]

        self.update_undo_btn_state()

        if self.dockwidget.Cbx_Number_Increment.isChecked():
            self.dockwidget.Spn_Current_Number.setValue(
                self.dockwidget.Spn_Current_Number.value() + len(self.preset_points)
            )

        self.init_rb_line()
        self.clear_preview_spots()

    def add_preset_point(self, pt):
        comment = self.get_comment(0)
        if self.dockwidget.Opt_Comment_Popup.isChecked():
            comment, okPressed = QInputDialog.getText(
                self.window, "Input Comment", "Comment: ", QLineEdit.Normal, comment
            )
            if not okPressed:
                return
        self.preset_points = [[comment, pt[0], pt[1]]]
        self.add_preset_points()

    def handle_undo(self):
        if len(self.undo_preset) == 0:
            return

        current_layer_index = self.dockwidget.Cmb_Preset_Layer.currentIndex()
        if current_layer_index == -1:
            return

        # get and remove last item from self.undo_preset
        undo_item = self.undo_preset.pop()

        preset_layer = self.dockwidget.Cmb_Preset_Layer.itemData(current_layer_index)
        preset_layer.startEditing()

        undo_message = ""
        if undo_item["type"] == "addition":
            self.undo_add_preset_point(preset_layer, undo_item["data"])
            undo_message = "Addition of "
            if len(undo_item["data"]) == 1:
                undo_message += "a spot"
            else:
                undo_message += "spots"
        elif undo_item["type"] == "movement":
            undo_message = "Relocating a spot"
            self.undo_moving_point(preset_layer, undo_item["data"])
        elif undo_item["type"] == "comment":
            undo_message = "Editing a comment"
            self.undo_editing_comment(preset_layer, undo_item["data"])
        elif undo_item["type"] == "delete":
            undo_message = "Deleting a spot"
            self.undo_delete_preset_point(preset_layer, undo_item["data"])

        preset_layer.commitChanges()

        self.iface.messageBar().pushMessage("Undo", undo_message, level=Qgis.Info, duration=1)
        self.update_undo_btn_state()

    def undo_editing_comment(self, preset_layer, data):
        fid = data["id"]
        comment = data["comment"]
        field_idx = preset_layer.fields().indexOf("Comment")
        preset_layer.changeAttributeValue(fid, field_idx, comment)

    def undo_moving_point(self, preset_layer, data):
        fid = data["id"]
        geom = data["geom"]
        preset_layer.dataProvider().changeGeometryValues({fid: geom})

    def undo_delete_preset_point(self, preset_layer, data):
        preset_layer.startEditing()
        fields = preset_layer.fields()
        feature = QgsFeature()
        feature.setFields(fields)
        geom = data["geometry"]
        comment = data["comment"]
        feature.setGeometry(geom)
        feature["Comment"] = comment
        dpr = preset_layer.dataProvider()
        dpr.addFeatures([feature])
        preset_layer.commitChanges()

    def undo_add_preset_point(self, preset_layer, id_list):
        preset_layer.deleteFeatures(id_list)

        # update increment number
        if self.dockwidget.Cbx_Number_Increment.isChecked():
            spn = self.dockwidget.Spn_Current_Number
            spn.setValue(spn.value() - len(id_list))

    def update_undo_btn_state(self):
        if len(self.undo_preset):
            self.dockwidget.Btn_Undo_Add_Preset_Point.setEnabled(True)
        else:
            self.iface.messageBar().pushMessage("Already at oldest change", level=Qgis.Info, duration=3)
            self.dockwidget.Btn_Undo_Add_Preset_Point.setEnabled(False)

    def clear_preview_points(self):
        if self.rb_line.size() > 0:
            self.rb_line.reset()
            # self.rb2.reset()
            # self.rb_start.reset()
            self.init_rb_line()
            # self.init_annotation()

    def clear_preview_spots(self, init=True):
        # clear/init all preview related items
        self.start_point = []
        self.end_point = []
        self.feature_id = None
        self.line_in_progress = False
        if init:
            self.init_scratch_layer()
        else:
            self.remove_objects_from_scratch_layer()
        self.clear_preview_points()

    def init_rb_line(self):
        if self.rb_line.size() > 0:
            self.rb_line.reset()
        self.rb_line = QgsRubberBand(self.canvas, QgsWkbTypes.LineGeometry)
        # self.rb = QgsRubberBand(self.canvas, True)  # False = not a polygon
        self.rb_line.setWidth(2)
        self.rb_line.setColor(QColor(255, 20, 20, 60))
        self.dockwidget.Txt_Line_Length.setText("0")

    # def init_rb2(self):
    #     # Points
    #     if self.rb2.size() > 0:
    #         self.rb2.reset()
    #     self.rb2 = QgsRubberBand(self.canvas, QgsWkbTypes.PointGeometry)
    #     self.rb2.setIcon(QgsRubberBand.ICON_CIRCLE)
    #     self.rb2.setIconSize(10)
    #     self.rb2.setColor(QColor(255, 20, 20, 90))
    #     self.preset_points = []
    #     self.init_annotation()

    # def init_rb_s(self):
    #     # Starting point of line preset
    #     if self.rb_start.size() > 0:
    #         self.rb_start.reset()
    #     self.rb_start = QgsRubberBand(self.canvas, QgsWkbTypes.PointGeometry)
    #     # self.rb_s = QgsRubberBand(self.canvas, False)  # False = not a polygon
    #     self.rb_start.setIcon(QgsRubberBand.ICON_CIRCLE)
    #     self.rb_start.setIconSize(10)
    #     self.rb_start.setColor(QColor(255, 0, 255, 250))

    # def is_annotation_item(self, item):
    #     return issubclass(type(item), QgsMapCanvasAnnotationItem)
    #
    # def remove_item(self, item):
    #     self.canvas.scene().removeItem(item)

    # def init_annotation(self):
    #     [self.remove_item(i) for i in self.canvas.scene().items()
    #      if self.is_annotation_item(i)]

    # def init_rb(self):
    #     self.init_rb()
    #     # self.init_rb2()
    #     # self.init_rb_s()

    def init_scratch_layer(self, moving=False):
        self.remove_objects_from_scratch_layer()
        if hasattr(self, "scaratcLayer"):
            symbol = self.scratchLayer.renderer().symbol()
            symbol.symbolLayer(0).setStrokeColor(QColor(255, 255, 255))  # stroke: black
            self.scratchLayer.renderer().symbol().setColor(QColor(64, 143, 176, 102))  # lightblue, alpha: 0.4

    def cleanup_old_scratch_layers(self):
        instance = QgsProject.instance()
        vals = instance.mapLayers().values()
        [
            instance.removeMapLayer(layer)
            for layer in vals
            if layer.name() == "tmp" and layer.storageType() == "Memory storage"
        ]

    def create_scratch_layer(self, moving=False):
        self.cleanup_old_scratch_layers()
        self.scratchLayer = QgsVectorLayer("Point", "tmp", "memory")
        self.scratchLayer.setFlags(QgsMapLayer.Private)
        self.scratchLayer.startEditing()
        # Copy and paste symbol sytle from preset layer
        layer = self.get_preset_layer()
        if layer is None:
            return
        symbol = layer.renderer().symbol()
        props = symbol.symbolLayer(0).properties()

        renderer = self.scratchLayer.renderer()
        renderer.setSymbol(QgsMarkerSymbol.createSimple(props))

        # black
        renderer.symbol().symbolLayer(0).setStrokeColor(QColor(255, 255, 255))
        # light blue
        renderer.symbol().setColor(QColor(64, 143, 176, 102))

        self.sc_dp = self.scratchLayer.dataProvider()
        # add "Comment" field to the attribute table
        self.sc_dp.addAttributes([QgsField("Comment", QVariant.String)])

        # Label formatting: "Comment" field, diplay all comments, drop shodow, buffer (white)
        settings = QgsPalLayerSettings()
        settings.enabled = True
        settings.fieldName = "Comment"
        settings.displayAll = True
        coll = QgsPropertyCollection("preset")
        # coll.setProperty(settings.ShadowDraw, True)
        # coll.setProperty(settings.BufferDraw, True)
        # coll.setProperty(settings.BufferColor, QColor(0, 0, 0))
        coll.setProperty(settings.Color, QColor(20, 20, 20))

        # rendering: overlap
        plmt = settings.placementSettings()
        plmt.setOverlapHandling(Qgis.LabelOverlapHandling.AllowOverlapAtNoCost)
        settings.setPlacementSettings(plmt)

        # placement: obstacle
        obs = settings.obstacleSettings()
        obs.setIsObstacle(False)
        settings.setObstacleSettings(obs)

        settings.setDataDefinedProperties(coll)
        self.scratchLayer.setLabelsEnabled(True)
        self.scratchLayer.setLabeling(QgsVectorLayerSimpleLabeling(settings))
        self.scratchLayer.commitChanges()

        # add drop shadow and color to the spot preview
        # pe_stack = QgsEffectStack()
        # pe_stack.appendEffect(QgsDropShadowEffect())
        # pe_stack.appendEffect(QgsDrawSourceEffect())
        #
        # self.scratchLayer.renderer().setPaintEffect(pe_stack)

        self.fields = self.scratchLayer.fields()

        self.feature_id = None

        self.scratchLayer.triggerRepaint()
        QgsProject.instance().addMapLayer(self.scratchLayer)

    def remove_objects_from_scratch_layer(self):
        layer = self.get_preset_layer()

        if layer is None:
            return

        layer.removeSelection()
        if hasattr(self, "sc_dp"):
            feature_ids = [f.id() for f in self.sc_dp.getFeatures()]
            self.sc_dp.deleteFeatures(feature_ids)

            self.scratchLayer.commitChanges()
            self.scratchLayer.triggerRepaint()

        self.feature_id = None

        self.update_add_points_btn_status(False)

    def add_features_to_scratch_layer(self, features):
        self.sc_dp.addFeatures(features)
        self.scratchLayer.commitChanges()
        self.scratchLayer.triggerRepaint()

    def show_tooltip(self, situ):
        self.hide_tooltip()

        pos = QCursor.pos()

        if situ == "line-start":
            txt = "🌱 Click end-point to see preview spots!"
        elif situ == "line-end":
            txt = "🌱 If the preview spots look good,\n" '🌱 click the "Add Points" button➡️'
        else:
            txt = ""
        QToolTip.showText(pos, txt, self.canvas, QRect(pos.x(), pos.y(), 200, 200), 30000)
        # QToolTip.showText(pos, txt, self.window)

    def hide_tooltip(self):
        QToolTip.hideText()

    def update_add_points_btn_status(self, status):
        mode = self.get_preset_mode()
        btns = []

        if mode == "line":
            btns.append(self.dockwidget.Btn_Line_Add_Points)
        elif mode == "grid":
            btns.append(self.dockwidget.Btn_Grid_Add_Points)
        else:
            btns.append(self.dockwidget.Btn_Line_Add_Points)
            btns.append(self.dockwidget.Btn_Grid_Add_Points)

        if status:  # Activate
            fg_color = "rgb(255, 255, 255)"
            bg_color = "rgb(255, 49, 49)"
        else:  # Deactivate
            fg_color = "rgb(128, 128, 128)"
            bg_color = "rgb(192, 192, 192)"

        styles = f"background-color: {bg_color};" f"color: {fg_color};" "font-weight: bold;"

        for btn in btns:
            btn.setStyleSheet(styles)
            btn.setEnabled(status)

    def update_scratch_layer_symbol_size(self):
        layer = self.get_preset_layer()
        symbol = layer.renderer().symbol()
        props = symbol.symbolLayer(0).properties()
        size = props["size"]
        if hasattr(self, "scratchLayer"):
            sc_props = self.scratchLayer.renderer().symbol().symbolLayer(0).properties()
            sc_props["size"] = size
            self.scratchLayer.renderer().setSymbol(QgsMarkerSymbol.createSimple(sc_props))

    def preset_line(self, pt, line_end=False, live_preview=False):
        if line_end:
            self.end_point = pt
            self.draw_line_points(live_preview)  # preview spots
            if not live_preview:
                self.line_in_progress = False
        else:  # start line drawing
            self.init_rb_line()

            # self.init_scratch_layer()
            self.start_point = pt

            geom = QgsGeometry.fromPointXY(pt)
            feature = QgsFeature()
            feature.setFields(self.fields)
            feature.setGeometry(geom)
            feature["Comment"] = ""
            self.add_features_to_scratch_layer([feature])
            self.line_in_progress = True

            self.rb_line.addPoint(self.start_point, False)
            self.rb_line.addPoint(self.start_point, True)
            # self.rb_start.addPoint(self.start_point, True)

    def update_line(self):
        if len(self.start_point) and len(self.end_point):
            self.init_scratch_layer()
            self.update_scratch_layer_symbol_size()
            self.preset_points = []
            self.draw_line_points()

    def draw_line_points(self, live_preview=False):
        if len(self.start_point) == 0:
            return
        # update line length in the widget
        length = self.update_line_length(self.end_point)

        if self.dockwidget.Opt_Line_Step_Size.isChecked():
            # use step size
            step = self.dockwidget.Spn_Line_Step_Size.value()
            n_spots = int(length / step) + 1
            self.set_value_without_signal(self.dockwidget.Spn_Line_N_Spot, n_spots)
        else:
            # use number of spots
            n_spots = self.dockwidget.Spn_Line_N_Spot.value()
            step = length / (n_spots - 1)
            self.set_value_without_signal(self.dockwidget.Spn_Line_Step_Size, step)

        angle = self.get_angle(self.start_point, self.end_point)
        cos = step * math.cos(angle)
        sin = step * math.sin(angle)
        i, x, y = 0, 0, 0
        pt = QgsPointXY(x, y)

        features = []

        for n in range(n_spots):
            x = self.start_point[0] + cos * n * self.scale
            y = self.start_point[1] + sin * n * self.scale
            comment = self.get_comment(i)
            pt = QgsPointXY(x, y)
            geom = QgsGeometry.fromPointXY(pt)

            feature = QgsFeature()

            feature.setFields(self.fields)
            feature.setGeometry(geom)
            feature["Comment"] = comment
            features.append(feature)

            self.preset_points.append([comment, x, y])
            i += 1

        self.add_features_to_scratch_layer(features)

        if live_preview:
            self.preset_points = []

        self.update_add_points_btn_status(True)
        self.canvas.refresh()

    def preset_grid(self, pt):
        self.start_point = pt
        self.init_rb_line()
        self.init_scratch_layer()
        self.draw_grid_points()

    def update_grid(self):
        if self.start_point is None:
            return
        self.preset_points = []
        self.init_scratch_layer()
        self.update_scratch_layer_symbol_size()
        self.init_rb_line()
        self.draw_grid_points()

    def draw_grid_points(self):
        order = self.dockwidget.Cmb_Grid_Move_Order.currentIndex()
        step_x = self.dockwidget.Spn_Grid_Step_Size_X.value()
        step_y = self.dockwidget.Spn_Grid_Step_Size_Y.value()
        n_step_x = self.dockwidget.Spn_Grid_N_Point_X.value()
        n_step_y = self.dockwidget.Spn_Grid_N_Point_Y.value()
        rotation = self.canvas.rotation()
        if len(self.start_point) == 0:
            return
        points = self.calculate_grid_positions(
            self.start_point, [step_x, step_y], [n_step_x, n_step_y], order, self.scale, rotation
        )
        i = 0
        features = []
        for point in points:
            pt = QgsPointXY(point[0], point[1])
            comment = self.get_comment(i)
            self.rb_line.addPoint(pt, True)
            self.preset_points.append([comment, point[0], point[1]])

            geom = QgsGeometry.fromPointXY(pt)
            feature = QgsFeature()

            feature.setFields(self.fields)
            feature.setGeometry(geom)
            feature["Comment"] = comment
            features.append(feature)

            i += 1

        self.add_features_to_scratch_layer(features)
        self.update_add_points_btn_status(True)
        self.canvas.refresh()

    def calculate_grid_positions(self, start_xy, step_xy, n_xy, order, scale=1, rotation=0):
        # start, step, n are lists
        # order: 0 = Horizontal, then vertical
        #        1 = Vertical, then horizontal
        start_xy = list(start_xy)
        if order == 0:
            for i in [start_xy, step_xy, n_xy]:
                i.reverse()
        p_n, s_n = n_xy
        p_step, s_step = step_xy
        p_pt, s_pt = start_xy
        out = []
        for p in range(p_n):  #
            p_val = p_pt + p * p_step * scale
            for s in range(s_n):
                s_val = s_pt + s * s_step * scale
                xy = [s_val, p_val]
                if order:
                    xy.reverse()
                out.append(xy)
        if rotation:
            if order == 0:
                start_xy.reverse()
            out = self.rotate_coordinates(out, rotation, start_xy)
        return out

    def rotate_coordinates(self, points, rotation, origin=[0, 0]):
        # poinsts: list of point
        # rotation: rotation in degree (0 to +180, 0 to -180), CW = +, CCW = -
        # origin: coordiates of origin of rotation. Default is [0, 0]
        r = math.radians(rotation)
        cos, sin = math.cos(r), math.sin(r)
        out = map(
            lambda p: [
                (p[0] - origin[0]) * cos - (p[1] - origin[1]) * sin + origin[0],
                (p[0] - origin[0]) * sin + (p[1] - origin[1]) * cos + origin[1],
            ],
            points,
        )
        return out

    def get_distance(self, pt1, pt2):
        return ((pt2[0] - pt1[0]) ** 2 + (pt2[1] - pt1[1]) ** 2) ** 0.5

    def get_angle(self, pt1, pt2):
        return math.atan2((pt2[1] - pt1[1]), (pt2[0] - pt1[0]))

    def preset_tool_changed(self, tool_index):
        self.clear_preview_spots()  # scratch layer

    def set_value_without_signal(self, target, value):
        target.blockSignals(True)
        target.setValue(value)
        target.blockSignals(False)

    def handle_comment_change_preview(self):
        current_mode = self.get_preset_mode()
        if current_mode == "point" or self.rb_line.size() == 0:
            return
        if current_mode == "line":
            self.update_line()
        elif current_mode == "grid":
            self.update_grid()

    def get_comment(self, n):
        comment_base = self.dockwidget.Tbx_Comment.text()
        flag_comment_format = self.dockwidget.Cbx_Number_Increment.isChecked()
        current_num = self.dockwidget.Spn_Current_Number.value()
        if not flag_comment_format:
            return comment_base
        return self.format_comment(comment_base, current_num + n)

    def format_comment(self, comment, num):
        try:
            out = comment.format(num)
        except (IndexError, KeyError, ValueError):
            out = comment
        return out

    def update_line_length(self, pt=None):
        if pt is None:
            length = 0
        else:
            length = self.get_distance(self.start_point, pt) / self.scale
        self.dockwidget.Txt_Line_Length.setText(f"{length:.1f}")
        return length

    """
    Canvas related
    """

    def mapToolChanged(self, tool, prev_tool):
        #     r'wiscsims_tool', str(tool)))
        if re.search(r"wiscsims_tool", str(tool)) or re.search(r"WiscSIMSTool", str(tool)):
            self.dockwidget.setEnabled(True)
            return

        try:
            self.unsetMapTool()
            # self.clear_preview_points()
            self.remove_objects_from_scratch_layer()
            self.wiscsims_tool_action.setChecked(False)
            self.dockwidget.setEnabled(False)
        except Exception:
            pass

    def unsetMapTool(self):
        self.canvas.mapToolSet.disconnect(self.mapToolChanged)
        self.canvas.unsetMapTool(self.canvasMapTool)

    def get_near_features(self, layer, geom):
        # radius = spot_size / 2 / pixel_size (10 / 2 / 1 = 5)
        spot_size = self.dockwidget.Spn_Preset_Spot_Size.value()
        pixel_size = self.dockwidget.Spn_Preset_Pixel_Size.value()
        radius = spot_size / 2 / pixel_size
        features = layer.dataProvider().getFeatures()

        out = [f for f in features if 0 < geom.distance(f.geometry()) < radius]
        return out

    def canvasClickedWShift(self, e):
        # Start moving preset point
        # self.f_id = None
        self.init_scratch_layer(moving=True)
        self.init_rb_line()

        layer = self.get_preset_layer()

        if layer is None:
            # there is no preset layer
            QMessageBox.warning(self.window, "No Preset Layer", "Preset layer is not selected.")
            return

        cursor_geom = QgsGeometry.fromPointXY(self.canvasMapTool.getMapCoordinates(e))
        self.movement_offst = [0, 0]
        features = self.get_near_features(layer, cursor_geom)

        if len(features) == 0:
            # do nothing
            return

        self.canvas.setCursor(Qt.ClosedHandCursor)
        feature = features[-1]  # select uppermost feature

        geom = feature.geometry()
        geom_p = geom.asPoint()
        cursor_geom_p = cursor_geom.asPoint()
        self.movement_offset = [geom_p.x() - cursor_geom_p.x(), geom_p.y() - cursor_geom_p.y()]
        self.feature_id = feature.id()
        layer.selectByIds([self.feature_id], QgsVectorLayer.SetSelection)

        comment = [f["Comment"] for f in layer.getFeatures(QgsFeatureRequest(self.feature_id))][0]
        tmp_feature = QgsFeature()
        tmp_feature.setGeometry(geom)
        tmp_feature.setFields(self.fields)
        tmp_feature["Comment"] = comment

        self.add_features_to_scratch_layer([tmp_feature])

        return

    def canvasReleaseWShift(self, e):
        # End move preset point
        if self.feature_id is None:
            return

        layer = self.get_preset_layer()
        original_geo = [f.geometry() for f in layer.getFeatures() if f.id() == self.feature_id][0]
        layer.startEditing()
        pt = self.canvasMapTool.getMapCoordinates(e)
        pt.set(pt.x() + self.movement_offset[0], pt.y() + self.movement_offset[1])
        geom = QgsGeometry.fromPointXY(pt)
        layer.dataProvider().changeGeometryValues({self.feature_id: geom})
        layer.commitChanges()

        layer.removeSelection()
        undo_data = {"type": "movement", "data": {"id": self.feature_id, "geom": original_geo}}
        self.undo_preset.append(undo_data)

        self.init_scratch_layer()

        self.canvas.setCursor(Qt.CrossCursor)

        self.update_undo_btn_state()
        self.flag_cancel_moving_spot = False
        return

    def delete_spot(self, e):
        # delete spot
        layer = self.get_preset_layer()
        if layer is None:
            return

        cursor_geom = QgsGeometry.fromPointXY(self.canvasMapTool.getMapCoordinates(e))
        features = self.get_near_features(layer, cursor_geom)

        if len(features) == 0:
            return

        f_id = features[-1].id()  # select uppermost feature

        layer.selectByIds([f_id], QgsVectorLayer.SetSelection)
        f = [f for f in layer.getFeatures(QgsFeatureRequest(f_id))][0]
        comment = f["Comment"]
        geom = f.geometry()
        title = "Delete Spot"
        label = f'Are you sure you want to delete "{comment}"?'

        self.state_shift_key = False
        self.state_alt_key = False

        self.canvas.setCursor(Qt.CrossCursor)

        # show confirmation dailog
        rep_message = QMessageBox.warning(self.window, title, label, QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
        if rep_message == QMessageBox.No:
            layer.removeSelection()
            return

        layer.startEditing()
        layer.deleteFeature(f_id)
        layer.commitChanges()

        self.undo_preset.append(
            {
                "type": "delete",
                "data": {
                    "id": f_id,
                    "geometry": geom,
                    "comment": comment,
                },
            }
        )

        self.state_alt_key = False
        self.state_shift_key = False

        # reset cursor
        self.canvas.setCursor(Qt.CrossCursor)

        self.update_undo_btn_state()

    def canvasReleaseWAltShift(self, e):
        self.delete_spot(e)

    def canvasReleaseWAlt(self, e):
        layer = self.get_preset_layer()
        if layer is None:
            return

        cursor_geom = QgsGeometry.fromPointXY(self.canvasMapTool.getMapCoordinates(e))
        features = self.get_near_features(layer, cursor_geom)

        if len(features) == 0:
            return

        f_id = features[-1].id()  # select uppermost feature

        # f_id = features[0].mFeature.id()
        layer.selectByIds([f_id], QgsVectorLayer.SetSelection)
        comment = [f["Comment"] for f in layer.getFeatures(QgsFeatureRequest(f_id))][0]
        title = "Modifying Comment"
        label = "Enter New Comment"
        mode = QLineEdit.Normal
        default = comment

        # show dailog for new comment
        new_comment, ok = QInputDialog.getText(self.window, title, label, mode, default)

        if ok:
            field_idx = layer.fields().indexOf("Comment")
            layer.startEditing()
            for feat_id in layer.selectedFeatureIds():
                layer.changeAttributeValue(feat_id, field_idx, new_comment)
            layer.commitChanges()

        layer.removeSelection()

        self.undo_preset.append(
            {
                "type": "comment",
                "data": {
                    "id": f_id,
                    "comment": default,
                },
            }
        )
        # reset curosr and mod_key status
        self.state_alt_key = False
        self.canvas.setCursor(Qt.CrossCursor)

        self.update_undo_btn_state()

    def canvasClicked(self, pt):
        self.init_scratch_layer()

        if self.get_current_tool() != "preset":
            self.remove_objects_from_scratch_layer()
            return

        if self.flag_pixel_size_tool:
            # selecting locations for the Pixel Size Tool (PST)

            if not self.pxsize_end_point.isEmpty():
                # Activate input
                self.init_px_size_tool()

            if self.pxsize_start_point.isEmpty():
                # start point selected
                # self.init_px_size_tool()
                self.pxsize_start_point = pt
                print("PST win hide")
                self.pixel_size_tool_win.hide()

                # start showing rubberband
                self.pxsize_line.addPoint(self.pxsize_start_point, False)  # add start point
                self.pxsize_line.addPoint(self.pxsize_start_point, True)  # add new temp end-point
            else:
                # end point selected
                self.pxsize_end_point = pt

                # set value to the PST window
                self.pixel_size_tool_win.set_scale_start_end(self.pxsize_start_point, self.pxsize_end_point)

                # Show/activate PST window

                self.pixel_size_tool_win.show()

            # self.pixel_size_tool()

            return

        if self.get_preset_layer() is None:
            # preset layer has not been selected yet
            return

        if self.flag_cancel_moving_spot:
            self.flag_cancel_moving_spot = False
            return

        mode = self.get_preset_mode()
        if self.state_ctrl_key:
            self.state_ctrl_key = False

        elif mode == "point":
            self.add_preset_point(pt)

        elif mode == "line":
            if self.line_in_progress:
                # select end-point
                self.show_tooltip("line-end")
                self.preset_line(pt, True)
            else:
                # select start-point
                self.show_tooltip("line-start")
                self.start_point = []
                self.end_point = []
                # elif self.start_point and self.end_point:
                self.preset_line(pt)
                # else:
                pass
                #
        elif mode == "grid":
            self.preset_points = []
            self.init_rb_line()
            self.preset_grid(pt)

    def canvasClickedRight(self, pt):
        self.canvasClicked(pt)

    def canvasMoved(self, pt):
        if self.flag_pixel_size_tool:
            if not self.pxsize_start_point.isEmpty() and self.pxsize_end_point.isEmpty():
                self.pxsize_line.removeLastPoint()  # remove temp end-point (cursor)
                self.pxsize_line.addPoint(pt, True)  # add new temp end-point
            return

        # Throttling of preview repainting
        # no throttling for rubberband line of the Line mode

        if self.line_in_progress:
            # drawing a tie line between start-point and cursor
            self.rb_line.removeLastPoint()  # remove temp end-point (cursor)
            self.rb_line.addPoint(pt, True)  # add new temp end-point
            self.update_line_length(pt)

        self.mouse_move_counter += 1
        if self.mouse_move_counter < self.move_throttling_threshold:
            return
        self.mouse_move_counter = 0

        self.cursor_pos = pt

        if self.line_in_progress:
            # update Line length
            self.init_scratch_layer()
            self.preset_line(pt, True, True)
            return

        # moving preset point
        if self.feature_id is not None or self.state_ctrl_key:
            if self.feature_id:
                pt.set(pt.x() + self.movement_offset[0], pt.y() + self.movement_offset[1])
            geom = QgsGeometry.fromPointXY(pt)
            feature_id = [f.id() for f in self.sc_dp.getFeatures()]
            self.sc_dp.changeGeometryValues({feature_id[0]: geom})
            self.scratchLayer.triggerRepaint()
            return

    def set_deleting_spot_cursor(self):
        cursor = QCursor(
            QPixmap(":/plugins/wiscsims_tool/img/icon_delete.png"),
            hotX=0,
            hotY=0,
        )
        self.canvas.setCursor(cursor)

        return True

    def canvasShiftKeyState(self, state):
        self.state_shift_key = state
        if state:
            if not self.state_alt_key:
                # shift key pressed
                self.canvas.setCursor(Qt.OpenHandCursor)
                self.flag_cancel_moving_spot = False
            else:
                # shift + alt => delete spot
                self.set_deleting_spot_cursor()
        else:
            # shift key released
            if self.feature_id:
                # cancel moving
                self.remove_objects_from_scratch_layer()
                self.flag_cancel_moving_spot = True
            else:
                # after spot movement
                self.flag_cancel_moving_spot = False
            self.canvas.setCursor(Qt.CrossCursor)

    def canvasAltKeyState(self, state):
        self.state_alt_key = state
        if state:
            if self.state_shift_key:
                # shift + alt
                self.set_deleting_spot_cursor()
            else:
                # alt
                self.canvas.setCursor(Qt.PointingHandCursor)
        else:
            self.canvas.setCursor(Qt.CrossCursor)  #

    def canvasCtrlkeyState(self, state):
        if not state and not self.state_ctrl_key:
            # do nothing if state_ctrl_key is already False when CTRL key was released
            # Because CTRL + click trigger clear preview spots (remove a rubberband spot of start-point)
            self.canvas.setCursor(Qt.CrossCursor)
            return

        self.clear_preview_spots()
        self.state_ctrl_key = state
        if state:
            feature = QgsFeature()
            feature.setGeometry(QgsGeometry.fromPointXY(self.cursor_pos))
            self.add_features_to_scratch_layer([feature])
            # hide cursor while showing spot size preview
            self.canvas.setCursor(Qt.BlankCursor)
        else:
            self.canvas.setCursor(Qt.CrossCursor)

    def canvasEscapeKeyState(self):
        self.clear_preview_spots(init=False)

    def update_cursor(self):
        pass
