# -*- coding: utf-8 -*-
"""
/***************************************************************************
 ConvertHeightsDialog
                                 A QGIS plugin
 This plugin allows to calculate EGM96 and EGM2008 geoid height in Poland based on KRON86 or EVRF2007 normal heights.
 Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
                             -------------------
        begin                : 2021-11-25
        git sha              : $Format:%H$
        copyright            : (C) 2021 by Warsaw University of Technology
        email                : dorota.marjanska@pw.edu.pl
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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.                                   *
 *                                                                         *
 ***************************************************************************/
"""

import os
import numpy as np
import scipy.interpolate
import io
import re
import traceback
# import sys
# import time

from qgis.PyQt import uic
from qgis.PyQt import QtWidgets
from PyQt5.QtWidgets import QMessageBox
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QVariant, QObject, QThread
from PyQt5 import QtCore


def dms2decimal(st, mi = 0, se = 0):
    """
    Method to convert degree, minutes, seconds (angle) to decimal degree

    Parameters
    ----------
    st : str/int
        degrees.
    mi : str/int
        minutes.
    se : str/float
        seconds.

    Returns
    -------
    decimal_degree : float
        angular value in decimal degrees.

    """
    if st.isdigit() and mi.isdigit() and se.replace('.','').isdigit():
        decimal_degree = int(st) + int(mi)/60 + float(se)/3600
    else: 
        decimal_degree = -9999
    

    return decimal_degree

def decimal2dms(dd):
    """
    Method to convert decimal degrees to degrees, minutes, seconds (angle)

    Parameters
    ----------
    dd : str/float

    Returns
    -------
    degrees : int
    minutes : int
    seconds : float

    """
    if dd == -9999:
        return [-9999, 99, 99.99999]
    is_positive = float(dd) >= 0
    dd = abs(dd)
    minutes,seconds = divmod(dd*3600,60)
    degrees,minutes = divmod(minutes,60)
    degrees = degrees if is_positive else -degrees

    
    return [int(degrees), int(minutes), round(seconds,5)]


def read_model_data(model_filename):
    """
    Read model data and assign to three separate ndarrays

    Parameters
    ----------
    model_filename : TYPE
        DESCRIPTION.

    Returns
    -------
    lat_mod : np.ndarray
        latitudes from a grid, match with lon_mod
    lon_mod : np.ndarray
        longitudes from a grid, match with lat_mod
    dh_mod : np.ndarray
        dH between model in filename, match with nodes (lat_mod, lon_mod)

    """

    lat_mod, lon_mod, dh_mod = np.genfromtxt(model_filename,
                                             delimiter=',',
                                             skip_header=6,
                                             unpack=True)
    return lat_mod, lon_mod, dh_mod

def read_file(user_input_file, delimiter=';'):
    """
    Function for reading user data with point coordinates;
    Expected input:
        dd.dddd;dd.dddd;hh.hhhh
        dd mm ss.sss;dd mm ss.sss;hh.hhh
        dd,dddd;dd,ddd;hh,hhhh # for comma as decimal separator
        dd mm ss,sss;dd mm ss,sss;hh,hhh

    No other columns, such as comments are allowed in the file.

    All lines of the file should have the same format. Missing values are allowed,
    but the delimiter (;) must be present. The missing values are replaced by NaN
    and enable further computation. Such errors are signalised.

    If a bytes file is given as input, ValueError is raised.

    Parameters
    ----------
    user_input_file : str
        path to a file with the user data; it should be valid;
    delimiter : str, optional
        delimiter of the columns in a file. The default is ';'.

    Returns
    -------
    user_data : ndarray
        3-column array with latitude, longitude and heights of points.
        The array is two-dimensional.

    """

    with open(user_input_file, "r") as file:
        lines = file.read().replace(",", ".")

    # process first line to see if we have three columns with spaces; pattern is simplified
    end_of_first_line = lines.index("\n") if lines.count("\n") else len(lines)
    re_result_space = re.search(r"[\d]{1,2} +[\d]{1,2}.*;.*[\d]{1,2} +[\d]{1,2}",
                          lines[:end_of_first_line])
    re_result_colon = re.search(r"[\d]{1,2}:+[\d]{1,2}.*;.*[\d]{1,2}:+[\d]{1,2}",
                          lines[:end_of_first_line])

    if re_result_space:
        # probably dd mm ss in file, split into lines, then into columns
        lines_c = lines.split("\n")
        lines_dd = [[str(dms2decimal(*(col.strip().split()))) if col.strip().count(" ") != 0
                     else col for col in line.split(";")] for line in lines_c]
        lines_dd = [";".join(line) for line in lines_dd]
        lines_dd = "\n".join(lines_dd)

        input_dms_flag = 1
        user_data = np.genfromtxt(io.StringIO(lines_dd),
                              delimiter=";",
                              encoding="utf-8",
                              dtype='float64')
    elif re_result_colon:
        # probably dd mm ss in file, split into lines, then into columns
        lines_c = lines.split("\n")
        lines_dd = [[str(dms2decimal(*(col.strip().split(':')))) if col.strip().count(":") != 0
                     else col for col in line.split(";")] for line in lines_c]
        lines_dd = [";".join(line) for line in lines_dd]
        lines_dd = "\n".join(lines_dd)

        input_dms_flag = 1
        user_data = np.genfromtxt(io.StringIO(lines_dd),
                              delimiter=";",
                              encoding="utf-8",
                              dtype='float64')
    else:
        input_dms_flag = 0
        user_data = np.genfromtxt(io.StringIO(lines),
                              delimiter=";",
                              encoding="utf-8",
                              dtype='float64')
    # if there is only one line in a file, make the result 2-dimensional
    if user_data.ndim == 1:
        user_data = user_data.reshape(-1, user_data.shape[0])
    r, c = user_data.shape

    # 7 cols that need to be modified into 3
    if c == 7:
        new_data = np.zeros((r, 3))
        for i, row in enumerate(user_data):
            new_data[i, :] = [dms2decimal(str(int(row[0])),str(int(row[1])),str(row[2])),
                              dms2decimal(str(int(row[3])),str(int(row[4])),str(row[5])),
                              row[6]]
            
            # new_data[i, :] = [dms2decimal(*row[0:3]),
            #                   dms2decimal(*row[3:6]),
            #                   row[6]]
        user_data = new_data

    if c != 3 and c != 2 and c != 7 and c != 6:
        raise ValueError(f'Some errors were detected! \r\n Line #1: Incorrect number of columns ({c})')
    
    user_data = np.nan_to_num(user_data, nan=-9999)
    
    return user_data, input_dms_flag

def create_point_interpolator(point_lat, point_lon, model_filename, method):
    """
    Create a function for interpolating value in a given point;
    the function finds the closest points (4x4 grid), if a given point is a node,
    then it's located in second row, second col of the grid, indexing from 1;

    Raises InterpolatorBoundsError, if there are not enough points nearby (not possible to access dH)

    Parameters
    ----------
    point_lat : float
        latitude of a given point (degrees)
    point_lon : float
        longitude of a given point
    lat_model : ndarray
        list of latitudes in a grid (paired with lon_model create unique keys)
    lon_model : ndarray
        list of longitudes in a grid (paired with lon_model create unique keys)
    dh_model : ndarray
        list of the values on grid nodes (lat_model(i), lon_model(j))
    method : str
        method of interpolation; available {'bilinear', 'cubic', 'nn'}

    Returns
    -------
    function
        interpolation function for a given method on the surrounding grid

    """
    # read model data
    lat_mod, lon_mod, dh_mod = read_model_data(model_filename)

    # create a dictionary for easier access to dh values
    dh_map = {(lon, lat): dh for lat, lon, dh in zip(lat_mod, lon_mod, dh_mod)}
    d_grid = 0.01

    # find nearest points in 4x4 grid ;
    near_lat = np.floor(point_lat*100)/100
    near_lon = np.floor(point_lon*100)/100
    lat_nearest = [np.round(near_lat + i * d_grid,2) for i in range(-1, 3)]
    lon_nearest = [np.round(near_lon + i * d_grid,2) for i in range(-1, 3)]
    dh_grid = [[dh_map.get((lon, lat)) for lon in lon_nearest]
                                               for lat in lat_nearest]
    # if not all the points were found error
    if None in np.array(dh_grid).ravel():
        raise InterpolatorBoundsError

    if method == 'bilinear':
        return scipy.interpolate.interp2d(lon_nearest, lat_nearest, dh_grid,
                                          kind='linear', bounds_error=False)
    elif method == 'cubic':
        return scipy.interpolate.interp2d(lon_nearest, lat_nearest, dh_grid,
                                          kind='cubic', bounds_error=False)
    else:
        coord_pairs = [(lon, lat) for lon in lon_nearest for lat in lat_nearest]
        return scipy.interpolate.NearestNDInterpolator(coord_pairs, np.array(dh_grid).ravel())


def create_file_interpolator(model_filename, method):
    '''
    Creates a spline function for interpolation over entire country


    Parameters
    ----------
    model_filename : str
        path to a file with the height model data;
    method : str
        method of interpolation; available {'bilinear', 'cubic', 'nn'}

    Returns
    -------
    function
        interpolation function for a given method for all points included in the model

    '''
    # read model data
    lat_mod, lon_mod, dh_mod = read_model_data(model_filename)
    cartcoord = list(zip(lat_mod, lon_mod))
    if method == 'bilinear':
        return scipy.interpolate.LinearNDInterpolator(cartcoord, dh_mod) # fill_value=0
    elif method == 'cubic':
        return scipy.interpolate.CloughTocher2DInterpolator(cartcoord, dh_mod)
    else:
        return scipy.interpolate.NearestNDInterpolator(cartcoord, dh_mod)

def check_lat_lon(lat,lon,lat_min = 49.0, lat_max = 54.88, lon_min = 14.05, lon_max = 24.2):
    '''
    This function checks if user provided latitude or longitude
    within the limits (lat_min, lat_max) and (lon_min, lon_max).
    If not, the function raises BoundsError.

    Parameters
    ----------
    lat : float
        user latitude of a given point (degrees)
    lon : float
        user longitude of a given point
    lat_min : float, optional
        lower limit setting the minimum value of latitudes. The default is 49.0.
    lat_max : float, optional
        upper limit setting the maximum value of latitudes. The default is 54.88.
    lon_min : float, optional
        lower limit setting the minimum value of longitudes. The default is 14.05.
    lon_max : float, optional
        upper limit setting the maximum value of longitudes. The default is 24.2.

    Raises
    ------
    BoundsError
        Provided point is outside the declared range (outside of Poland)

    Returns
    -------
    None.

    '''
    if lat_min <= lat <= lat_max and lon_min <= lon <= lon_max:
        pass
    else:
        raise BoundsError('Point coordinates are out of range')


def interp_point(ulat, ulon, uh, interpolator, flag, flag_source=0):
    '''


    Parameters
    ----------
    ulat : float
        latitude of a point provided by the user (decimal degrees).
    ulon : float
        longitude of a point provided by the user (decimal degrees).
    uh : float
        height of a point provided by the user (meters).
    interpolator : function
        function that allow to interpolate value over provided ulat, ulon
        (check create_point_interpolator or create_file_interpolator).
    flag : int
        flag revealing the type of conversion between two height systems.
    flag_source : int, optional
        flag revealing the type of interpolation function; available:
            {0 for point interpolator, 1 for file interpolator}
        The default is 0.

    Returns
    -------
    result_user_pt : float
        value that is a final result of height convertion.

    '''

    # check if any of the values is nan and do not continue computations if so
    if ulat == -9999 or ulon == -9999:
        return -9999

    if flag_source == 1:
        # file interpolation
        resid_user_pt = interpolator(ulat, ulon)
    else:
        resid_user_pt = interpolator(ulon, ulat)
        if isinstance(resid_user_pt, np.ndarray):
            resid_user_pt = resid_user_pt[0]

    result_user_pt = uh + flag * resid_user_pt
    if np.isnan(result_user_pt) or uh == -9999:
        result_user_pt = -9999
    return result_user_pt


def resolve_filepath(name, basepath=None):
    if not basepath:
        basepath = os.path.dirname(os.path.realpath(__file__))
    return os.path.join(basepath, name)

def save_to_textfile(user_file_data, output, user_filename, suffix, input_dms_flag=0):
    output_file = os.path.splitext(user_filename)
    if input_dms_flag == 1:
        lats_dms = [decimal2dms(user_file_data[i,0]) for i in range(0,len(user_file_data))]
        lons_dms = [decimal2dms(user_file_data[i,1]) for i in range(0,len(user_file_data))]
        lats_dms = ['{:d}:{:02d}:{:08.5f}'.format(elem[0],elem[1],elem[2]) for elem in lats_dms]
        lons_dms = ['{:d}:{:02d}:{:08.5f}'.format(elem[0],elem[1],elem[2]) for elem in lons_dms]
        user_h_converted = user_file_data[:,-1].tolist()
        user_h_converted = ['{:.3f}'.format(elem) for elem in user_h_converted]
        output_h_converted = output[:,-1].tolist()
        output_h_converted = ['{:.3f}'.format(elem) for elem in output_h_converted]

        with open(output_file[0] + suffix + output_file[1], "w") as final_file:
            final_file.write('#inp_lat;inp_lon;inp_hgt;out_lat;out_lon;out_hgt\n')
            for i in range(0,user_file_data.shape[0]):
                merged_line = ';'.join([lats_dms[i], lons_dms[i], user_h_converted[i], lats_dms[i], lons_dms[i], output_h_converted[i]])
                final_file.write(merged_line + '\n')
    else:
        merged_input_output = np.hstack((user_file_data[:,:3],output))
        fmt = '%.7f', '%.7f', '%.3f','%.7f', '%.7f', '%.3f'
        np.savetxt(output_file[0] + suffix + output_file[1], merged_input_output, fmt = fmt, delimiter = ';', header = 'inp_lat;inp_lon;inp_hgt;out_lat;out_lon;out_hgt')

class InterpolatorBoundsError(Exception):
    """Exception - when point is out of model boundaries"""
    pass

class BoundsError(Exception):
    """Exception - when point is out of model boundaries"""
    pass


class Worker(QObject):
    finished = pyqtSignal(object)
    error = pyqtSignal(Exception, str)
	# error = pyqtSignal(Exception, str, str)
    enabled = pyqtSignal(bool)
    update_line = pyqtSignal(str)

    def __init__(self, **data):
        QObject.__init__(self)
        self.user_input_file = data['user_input_file']
        self.method = data['method']
        self.flag = data['flag']
        self.model_file = data['model_file']
        self.suffix = data['suffix']
        self.killed = False

    def run_processing_file(self):
        try:
            # kill request received, exit loop early
            if self.killed is True:
                self.thread.exit()
            self.update_line.emit('Loading data...')
            user_file_data, input_dms_flag = read_file(self.user_input_file)
            self.update_line.emit('User data file has been loaded')
            r = user_file_data.shape[0]
            output = np.zeros((r, 3))
            self.update_line.emit('Loading interpolation model...')
            interpolator = create_file_interpolator(self.model_file, self.method)
            self.update_line.emit('Interpolation function has been created!')
            i = 0
            self.update_line.emit('Converting heights...')
            while i < r:
                if self.killed is True:
                    break
                output[i, 0] = user_file_data[i, 0]
                output[i, 1] = user_file_data[i, 1]
                if self.method == 'NN' and (user_file_data[i, 0] < 49.0 or user_file_data[i, 0] > 54.88) and (user_file_data[i, 1] < 14.05 or user_file_data[i, 1] > 24.2):
                    output[i, 2] = -9999
                else:
                    output[i, 2] = interp_point(user_file_data[i, 0], user_file_data[i, 1],
                                                user_file_data[i, 2], interpolator, self.flag, flag_source=1)
                i += 1
            if self.killed is False:
                self.update_line.emit('Heights have been converted!')
                self.update_line.emit('Saving output file... ')
                save_to_textfile(user_file_data, output, self.user_input_file, self.suffix, input_dms_flag)
                merged_input_output = np.hstack((user_file_data,output))
                self.update_line.emit('Process finished!')
                if (merged_input_output == -9999).any():
                    self.update_line.emit('Warning! Some points were not calculated properly \r\n ___________________')
                    where_no_data = np.where(merged_input_output[:,:2] == -9999)
                    where_no_height = np.where(merged_input_output[:,2] == -9999)
                    where_bad_coords = np.where(np.logical_and(merged_input_output[:,-1] == -9999,
                                                 np.logical_and(merged_input_output[:,2] != -9999,
                                                 np.logical_and(merged_input_output[:,0] != -9999, merged_input_output[:,1] != -9999))))
                    if where_no_data[0].size != 0:
                        where_no_data = where_no_data[0]+1
                        no_data = ', '.join([str(element) for element in np.unique(where_no_data)])
                        self.update_line.emit(f'In lines {no_data}: incorrect format or missing coordinates in the input file')
                    if where_no_height[0].size != 0:
                        where_no_height = where_no_height[0]+1
                        no_height = ', '.join([str(element) for element in np.unique(where_no_height)])
                        self.update_line.emit(f'In lines {no_height}: missing height value in the input file')
                    if where_bad_coords[0].size != 0:
                        where_bad_coords = where_bad_coords[0]+1
                        bad_coords = ', '.join([str(element) for element in np.unique(where_bad_coords)])
                        self.update_line.emit(f'In lines {bad_coords}: some coordinates in the input file are out of range')
            else:
                merged_input_output = None
        except Exception as e:
            merged_input_output = None
            # forward the exception upstream
            self.error.emit(e, self.user_input_file)
            # self.error.emit(e, traceback.format_exc(), self.user_input_file)
        self.finished.emit(merged_input_output)
        self.enabled.emit(True)

    def kill(self):
        self.killed = True

# This loads your .ui file so that PyQt can populate your plugin with the elements from Qt Designer
FORM_CLASS, _ = uic.loadUiType(os.path.join(
    os.path.dirname(__file__), 'convert_heights_dialog_base.ui'))

class ConvertHeightsDialog(QtWidgets.QDialog, FORM_CLASS):
    def __init__(self, parent=None):
        """Constructor."""
        super(ConvertHeightsDialog, self).__init__(parent)
        # Set up the user interface from Designer through FORM_CLASS.
        # After self.setupUi() you can access any designer object by doing
        # self.<objectname>, and you can use autoconnect slots - see
        # http://qt-project.org/doc/qt-4.8/designer-using-a-ui-file.html
        # #widgets-and-dialogs-with-auto-connect
        self.setupUi(self)
        self.tab_dms = True
        self.tab_dd = False
        self.tab_file = False

        lista_Kron = ['EVRF2007', 'EGM96', 'EGM2008']
        lista_EVRF = ['KRON86', 'EGM96', 'EGM2008']
        lista_EGM96 = ['KRON86', 'EVRF2007']
        lista_EGM2008 = ['KRON86', 'EVRF2007']

        self.output_system_box.addItems(lista_Kron)
        self.input_hmodel = self.input_system_box.currentText()
        self.output_hmodel = self.output_system_box.currentText()
        self.user_inputs_tab.setCurrentIndex(0)

        self.sub_lists = lista_Kron, lista_EVRF, lista_EGM96, lista_EGM2008
        self.input_system_box.currentIndexChanged.connect(self.updateCombo)
        self.input_system_box.activated.connect(self.pass_uklWej)
        self.output_system_box.currentIndexChanged.connect(self.pass_uklWyn)
        self.compute_values_button.clicked.connect(self.compute_vals)
        self.user_inputs_tab.currentChanged.connect(self.on_user_inputs_tab_currentChanged)
        self.dms_clear_button.clicked.connect(self.clear_dms)
        self.dd_clear_button.clicked.connect(self.clear_dd)
        self.clear_console_button.clicked.connect(self.clear_cons)
        self.help_button.clicked.connect(self.open_help_dialog)

    def pass_uklWej(self):
        self.input_hmodel = str(self.input_system_box.currentText())

    def pass_uklWyn(self):
        self.output_hmodel = str(self.output_system_box.currentText())

    def updateCombo(self, index):
        self.output_system_box.clear()
        self.output_system_box.addItems(self.sub_lists[index])

    def on_user_inputs_tab_currentChanged(self):
        if self.user_inputs_tab.currentIndex() == 0:
            self.tab_dms = True
            self.tab_dd = False
            self.tab_file = False
        elif self.user_inputs_tab.currentIndex() == 1:
            self.tab_dms = False
            self.tab_dd = True
            self.tab_file = False
        else:
            self.tab_dms = False
            self.tab_dd = False
            self.tab_file = True

    def get_radio_method(self):
        if self.RBintLinear.isChecked():
            return 'Bilinear'.lower()
        if self.RBintCubic.isChecked():
            return 'Cubic'.lower()
        if self.RBintNN.isChecked():
            return 'NN'

    def startWorker_control(self, user_input_file, method, flag, model_file, suffix):
        # create a new worker instance
        worker = Worker(user_input_file=user_input_file,
                        method=method,
                        flag=flag,
                        model_file=model_file,
                        suffix=suffix)
        # start the worker in a new thread
        thread = QThread(self)
        worker.moveToThread(thread)
        worker.finished.connect(self.workerFinished)
        worker.error.connect(self.workerError)
        worker.enabled.connect(self.compute_values_button.setEnabled)
        worker.update_line.connect(self.console_window.appendPlainText)
        self.compute_values_button.setEnabled(False)
        thread.started.connect(worker.run_processing_file)
        thread.start()
        self.thread = thread
        self.worker = worker

    def workerFinished(self, merged_input_output):
        # clean up the worker and thread
        self.worker.deleteLater()
        self.thread.quit()
        self.thread.wait()
        self.compute_values_button.setEnabled(True)
        if merged_input_output is not None:
            fname = os.path.splitext(self.user_filename)
            output_file = fname[0] + self.suffix + fname[1]
            msg = QMessageBox()
            msg.setIcon(QMessageBox.Information)
            msg.setText("Saved to file")

            msg.setInformativeText(f'Results saved to file {output_file}')
            msg.setWindowTitle("Success")
            msg.exec_()
            self.console_window.appendPlainText(f'Results of height (geoid) conversion from {self.input_hmodel} to {self.output_hmodel} available in file \r\n {output_file}')

    # def workerError(self, e, traceback, user_filename):
    def workerError(self, e, user_filename):
        msg = QMessageBox()
        msg.setIcon(QMessageBox.Critical)
        msg.setText("Incorrect input data")
        msg.setInformativeText("Check your input data in file " + user_filename)
        msg.setWindowTitle("Critical error")
        msg.exec_()
        self.console_window.insertPlainText(f'{e}')
        # self.console_window.insertPlainText(f'{e}, {traceback}')

    def compute_vals(self):
        model_dict = {"KRON86":"86","EVRF2007":"2007","EGM96":"96","EGM2008":"2008"}

        if self.input_hmodel == 'EGM96' or self.input_hmodel == 'EGM2008':
            model_filename = "height_models/dH" + model_dict[self.output_hmodel] + "_" + model_dict[self.input_hmodel] + '.txt'
            flag_ng = -1
            model_filename = resolve_filepath(model_filename)
        elif (self.input_hmodel == 'KRON86' and self.output_hmodel == 'EVRF2007'):
            model_filename = "height_models/dH" + model_dict[self.output_hmodel] + "_" + model_dict[self.input_hmodel] + '.txt'
            flag_ng = 1
            model_filename = resolve_filepath(model_filename)
        elif (self.input_hmodel == 'EVRF2007' and self.output_hmodel == 'KRON86'):
            model_filename = "height_models/dH" + model_dict[self.input_hmodel] + "_" + model_dict[self.output_hmodel] + '.txt'
            flag_ng = -1
            model_filename = resolve_filepath(model_filename)
        else:
            model_filename = "height_models/dH" + model_dict[self.input_hmodel] + "_" + model_dict[self.output_hmodel] + '.txt'
            flag_ng = 1
            model_filename = resolve_filepath(model_filename)
        method = self.get_radio_method()
        if self.tab_dd == True:
            #user point to interpolate
            try:
                lat_deg = float(self.dd_lat.text())
                lon_deg = float(self.dd_lon.text())
                h_deg = float(self.dd_h.text() or "0")
                check_lat_lon(lat_deg, lon_deg)
                interpolator = create_point_interpolator(lat_deg, lon_deg, model_filename, method)
                result = interp_point(lat_deg, lon_deg, h_deg, interpolator, flag_ng)
                self.console_window.appendPlainText(f'{lat_deg:.7f}; {lon_deg:.7f}; {result:.3f} m --- (geoid) height conversion from {self.input_hmodel} to {self.output_hmodel}, {method} interp.')
            except ValueError:
                msg = QMessageBox()
                msg.setIcon(QMessageBox.Critical)
                msg.setText("Incorrect input values")
                msg.setInformativeText("Accepted format of inputs is DD.DDDD")
                msg.setWindowTitle("Critical error")
                msg.exec_()
            except (BoundsError, InterpolatorBoundsError):
                msg = QMessageBox()
                msg.setIcon(QMessageBox.Critical)
                msg.setText("Boundaries error")
                msg.setInformativeText('Point coordinates are out of range')
                msg.setWindowTitle("Critical error")
                msg.exec_()
        elif self.tab_dms == True:
            try:
                if self.dms_lat_m.text() == '':
                    self.dms_lat_m.setText("0")
                if self.dms_lon_m.text() == '':
                    self.dms_lon_m.setText("0")
                if self.dms_lat_s.text() == '':
                    self.dms_lat_s.setText("0")
                if self.dms_lon_s.text() == '':
                    self.dms_lon_s.setText("0")
                    
                int(self.dms_lat_d.text()) + 1
                int(self.dms_lat_m.text()) + 1
                float(self.dms_lat_s.text()) + 1
                int(self.dms_lon_d.text()) + 1
                int(self.dms_lon_m.text()) + 1
                float(self.dms_lon_s.text()) + 1
                lat_deg = dms2decimal(self.dms_lat_d.text(), self.dms_lat_m.text() or "0", self.dms_lat_s.text() or "0")
                lon_deg = dms2decimal(self.dms_lon_d.text(), self.dms_lon_m.text() or "0", self.dms_lon_s.text() or "0")
                h_deg = float(self.dms_h.text() or "0")
                check_lat_lon(lat_deg, lon_deg)
                interpolator = create_point_interpolator(lat_deg, lon_deg, model_filename, method)
                result = interp_point(lat_deg, lon_deg, h_deg, interpolator, flag_ng)
                self.console_window.appendPlainText(f'{lat_deg:.7f}; {lon_deg:.7f}; {result:.3f} m --- (geoid) height conversion from {self.input_hmodel} to {self.output_hmodel}, {method} interp.')
            except ValueError:
                msg = QMessageBox()
                msg.setIcon(QMessageBox.Critical)
                msg.setText("Incorrect input values")
                msg.setInformativeText("Accepted format of inputs is DD MM SS.SSS")
                msg.setWindowTitle("Critical error")
                msg.exec_()
            except (BoundsError, InterpolatorBoundsError):
                msg = QMessageBox()
                msg.setIcon(QMessageBox.Critical)
                msg.setText("Boundaries error")
                msg.setInformativeText('Point coordinates are out of range')
                msg.setWindowTitle("Critical error")
                msg.exec_()
        else:
            user_filename = self.file_browser.filePath()
            self.user_filename = user_filename
            suffix = '_' + self.input_hmodel + self.output_hmodel + '_' + method
            self.suffix = suffix
            if os.path.exists(user_filename):
                self.startWorker_control(user_input_file=user_filename,
                                             method=method,
                                             flag=flag_ng,
                                             model_file=model_filename,
                                             suffix = suffix)
            else:
                msg = QMessageBox()
                msg.setIcon(QMessageBox.Critical)
                msg.setText("Incorrect filename")
                msg.setInformativeText("Filename " + user_filename + ' does not exist')
                msg.setWindowTitle("Critical error")
                msg.exec_()

    def clear_dms(self):
        self.dms_lat_d.clear()
        self.dms_lat_m.clear()
        self.dms_lat_s.clear()
        self.dms_lon_d.clear()
        self.dms_lon_m.clear()
        self.dms_lon_s.clear()
        self.dms_h.clear()

    def clear_dd(self):
        self.dd_lat.clear()
        self.dd_lon.clear()
        self.dd_h.clear()

    def clear_cons(self):
        self.console_window.clear()
        
    def open_help_dialog(self):
        self.nd = HelpDialog(self)
        self.nd.show()
 
FORM_CLASS_help, _ = uic.loadUiType(os.path.join(
    os.path.dirname(__file__), 'help_dialog_base.ui'))        
 
class HelpDialog(QtWidgets.QDialog, FORM_CLASS_help):
  def __init__(self, parent):
    super(HelpDialog, self).__init__(parent)
    self.setupUi(self)
    


