# -*- coding: utf-8 -*-
"""
/***************************************************************************
 TopoMapsDialog
                                 A QGIS plugin
 .....Description
 Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
                             -------------------
        begin                : 2024-11-11
        git sha              : $Format:%H$
        copyright            : (C) 2024 by ShingoNagano
        email                : shingosnaganon@.gmail.com
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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 concurrent.futures
from PIL import Image
from PIL import ImageEnhance
from PIL import ImageFilter
import webbrowser

from matplotlib import pyplot as plt
import numpy as np
from osgeo import gdal
from PyQt5.QtCore import pyqtSignal
from qgis.PyQt import QtWidgets
from qgis.PyQt.QtCore import QCoreApplication
from qgis.PyQt.QtCore import QSettings
from qgis.PyQt.QtWidgets import QMessageBox
from qgis.core import QgsMessageLog
from qgis.core import Qgis
from qgis.core import QgsTask
import scipy.signal

from .apps.config import Configs
from .apps.message import msg
from .apps.sampling import SamplingRaster
from .gdal_drawer.custom import CustomGdalDataset
from .gdal_drawer.custom import gdal_open
from .gdal_drawer.kernels import kernels
from .gdal_drawer.utils.colors import CustomCmap
from .gdal_drawer.utils.colors import LinearColorMap
from .apps.tabs import HillshadeTab
from .apps.tabs import InputTab
from .apps.tabs import OthersTab
from .apps.tabs import OutputTab
from .apps.tabs import SlopeTab
from .apps.tabs import TpiTab
from .apps.tabs import TriTab
config = Configs()
custom_cmap = CustomCmap()

MESSAGE_CATEGORY = 'TopoMaps Plugin'

################################################################################
#################### Main Dialog ###############################################
class TopoMapsDialog(
    QtWidgets.QDialog, 
    config.main_form, 
    InputTab,
    OutputTab,
    SlopeTab,
    TpiTab,
    TriTab,
    HillshadeTab,
    OthersTab
    ):
    def __init__(self, parent=None):
        """Constructor."""
        super(TopoMapsDialog, 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._dst = None
        self.unit = 1.0
        self.setupUi(self)
        # Make input tab
        self.make_input_tab()
        self.radioBtn_InputIsFile.toggled.connect(self.make_input_tab)
        self.radioBtn_InputIsLayer.toggled.connect(self.make_input_tab)
        self.checkBox_FirstResample.stateChanged.connect(self.make_input_tab)
        self.cmbBox_FirstResampleIs.currentIndexChanged.connect(self.make_input_tab)

        # Make output tab
        self.make_output_tab()
        self.mapSelectRadioBtn_Org.toggled.connect(self.make_output_tab)
        self.mapSelectRadioBtn_RGB.toggled.connect(self.make_output_tab)
        self.mapSelectRadioBtn_Vintage.toggled.connect(self.make_output_tab)
        self.mapSelectRadioBtn_YourStyle.toggled.connect(self.make_output_tab)
        self.checkBox_Sample.stateChanged.connect(self.make_output_tab)
        self.btn_ShowStyles.clicked.connect(self.show_map_styles)
        self.btn_CustomDlg.clicked.connect(self.show_custom_color_dlg)
        
        
        # Make slope tab
        self.make_slope_tab()
        self.cmbBox_SlopeDistanceSpec.currentIndexChanged.connect(self.make_slope_tab)
        self.btn_ShowSlopeGaussKernel.clicked.connect(
            # ボタンがクリックされたときに、メソッドを呼び出す
            lambda: self.show_slope_gaussian_kernel(self.cell_size_in_metre())
        )

        # Make tpi tab
        self.make_tpi_tab()
        self.radioBtn_OrgKernel.toggled.connect(self.make_tpi_tab)
        self.radioBtn_GaussKernel.toggled.connect(self.make_tpi_tab)
        self.radioBtn_InvGaussKernel.toggled.connect(self.make_tpi_tab)
        self.radioBtn_MeanKernel.toggled.connect(self.make_tpi_tab)
        self.radioBtn_DoughnutKernel.toggled.connect(self.make_tpi_tab)
        self.cmbBox_KernelDistanceSpec.currentIndexChanged.connect(self.make_tpi_tab)
        self.btn_ShowTpiKernel.clicked.connect(self.show_convolution_kernel)

        # Make tri tab
        self.btn_ShowTriGaussKernel.clicked.connect(
            lambda: self.show_tri_gaussian_kernel(self.cell_size_in_metre())
        )

        # Make hillshade tab
        self.make_hillshade_tab()
        self.cmbBox_HillshadeType.currentIndexChanged.connect(self.make_hillshade_tab)
        self.btn_ShowHillshadeGaussKernel.clicked.connect(
            lambda: self.show_hillshade_gaussian_kernel(self.cell_size_in_metre())
        )

        # Make others tab
        self.hSlider_Contrast.valueChanged.connect(self.make_others_tab_change_slider)
        
        # Open document
        self.btn_OpenDoc.clicked.connect(self.open_document)

        # Close daialog
        self.pushBtn_Close.clicked.connect(self.close)
        
    def tr(self, message: str):
        return QCoreApplication.translate('TopoMapsDialog', message)

    def read_raster(self) -> CustomGdalDataset:
        """
        ## Summary:
            Rasterの読み込み。
        Returns:
            CustomGdalDataset: gdal.Dataset のラッパークラス。
        """
        msg.start_read_raster(MESSAGE_CATEGORY)
        dst = gdal_open(self.get_input_file_path())
        if not dst.check_crs_is_metre():
            # メートル単位でない場合はUTM座標系に変換。
            dst = dst.estimate_utm_and_reprojected_dataset()
        msg.end_read_raster(MESSAGE_CATEGORY)
        return dst
    
    def resampling(self) -> CustomGdalDataset:
        """
        ## Summary:
            Rasterのリサンプリング。
        Returns:
            CustomGdalDataset: リサンプリング後のgdal.Dataset のラッパークラス。
        """
        algs = {
            'Nearest Neighbour': gdal.GRA_NearestNeighbour,
            'Bilinear': gdal.GRA_Bilinear,
            'Cubic': gdal.GRA_Cubic,
            'Cubic Spline': gdal.GRA_CubicSpline,
        }
        
        spec = self.get_first_resample_spec()
        if not spec.execute:
            # リサンプリングを行わない場合はそのまま返す
            return self._dst
        
        msg.start_resampling_raster(MESSAGE_CATEGORY)
        msg.resampling_spec(MESSAGE_CATEGORY, spec)
        if spec.metre_spec:
            # メートル単位でリサンプリング
            new_dst = self._dst.resample_with_resol_spec(
                x_resolution=spec.resolution,
                y_resolution=spec.resolution,
                resample_algorithm=algs.get(spec.smooth_alg)
            )
        else:
            # セル数でリサンプリング
            x_size = int(self._dst.RasterXSize * spec.denominator)
            y_size = int(self._dst.RasterYSize * spec.denominator)
            new_dst = self._dst.resample_with_cells_spec(
                x_cells=x_size,
                y_cells=y_size,
                resample_algorithm=algs.get(spec.smooth_alg)
            )
        msg.end_resampling_raster(MESSAGE_CATEGORY)
        return new_dst

    def slope(self, dst: CustomGdalDataset, cmap: LinearColorMap) -> np.ndarray:
        """
        ## Summary:
            傾斜の計算。
        Args:
            dst (CustomGdalDataset): 傾斜が入力された gdal.Dataset のラッパークラス。
            cmap (LinearColorMap): matplotlib.colors.LinearSegmentedColormap のラッパークラス。
        Returns:
            np.ndarray: 傾斜の画像データ。
        """
        options = self.get_slope_options()
        msg.slope_spec(MESSAGE_CATEGORY, options)
        cell_size_in_metre = dst.cell_size_in_metre()
        if options.metre_spec:
            # メートル単位で傾斜を計算
            slope_dst = dst.slope_with_distance_spec(
                distance=options.distance,
                x_resolution=cell_size_in_metre.x_size,
                y_resolution=cell_size_in_metre.y_size,
            )
        else:
            # セル数で傾斜を計算
            slope_dst = dst.slope_with_cells_spec(
                x_cells=options.cells,
                y_cells=options.cells,
                x_resol=cell_size_in_metre.x_size,
                y_resol=cell_size_in_metre.y_size,
            )
        slope_ary = slope_dst.array()
        nan_idx = np.isnan(slope_ary)
        if 1 <= nan_idx.size and (options.execute_gaussian_filter):
            # Nodata(np.nan)が含まれていると、scipy.signal.convolve()でエラーが発生
            # するので、Nodataを埋める。
            slope_dst = slope_dst.fill_nodata(options.distance, 10)
            slope_ary = slope_dst.array()
            _nan_idx = np.isnan(slope_ary)
            if 1 <= _nan_idx.size:
                # Nodataが残っている場合は0.0に変更
                slope_ary[_nan_idx] = 0.0

        slope_dst = None
        if options.execute_gaussian_filter:
            # ガウシアンフィルタを適用
            kernel = kernels.gaussian_kernel(sigma=options.sigma)
            slope_ary = scipy.signal.convolve(
                slope_ary, kernel, mode='same', method='fft'
            )
            if 1 <= nan_idx.size:
                # np.nanが含まれていた場合は、元に戻す
                slope_ary[nan_idx] = np.nan
        if options.change_alpha:
            # 透過率を変更
            cmap = self.relative_alpha_change(cmap, options.alpha)
        slope_img = cmap.values_to_img(slope_ary)
        return slope_img

    def tpi(self, 
        dst: CustomGdalDataset, 
        cmap: LinearColorMap, 
        multiples: bool=False
    ) -> np.ndarray:
        """
        ## Summary:
            TPIを計算する。
        Args:
            dst (CustomGdalDataset): TPIが入力された gdal.Dataset のラッパークラス。
            cmap (LinearColorMap): matplotlib.colors.LinearSegmentedColormap のラッパークラス。
            multiples (bool): 2枚目のTPIを計算するかどうか。
        Returns:
            np.ndarray: TPIの画像データ。
        """
        options = self.get_tpi_options()
        msg.tpi_spec(MESSAGE_CATEGORY, options)
        if multiples:
            # 2枚目のTPIを計算する場合は'multiples'に距離の倍数を指定
            kernel = self.generate_kernel(dst, multiples=options.multiples_distance)
        else:
            kernel = self.generate_kernel(dst)
        if kernel is None:
            # gdal.DEMProcessing()を使ってTPIを計算
            tpi_ary = dst.tpi(return_array=True)
        else:
            # 作成したカーネルを使ってTPIを計算
            tpi_ary = dst.tpi(
                kernel=kernel, return_array=True, outlier_treatment=False
            )
        if options.execute_outlier_treatment:
            tpi_ary = dst.outlier_treatment_array_by_iqr(
                threshold=options.iqr, raster_ary=tpi_ary
            )
        if options.change_alpha:
            # 透過率を変更
            cmap = self.relative_alpha_change(cmap, options.alpha)
        tpi_img = cmap.values_to_img(tpi_ary)
        if multiples:
            tpi_img[:, :, -1] = (tpi_img[:, :, -1] * 0.6).astype('uint8')
        return tpi_img
        
    def tri(self, dst: CustomGdalDataset, cmap: LinearColorMap) -> np.ndarray:
        """
        ## Summary:
            TRIを計算する。
        Args:
            dst (CustomGdalDataset): TRIが入力された gdal.Dataset のラッパークラス。
            cmap (LinearColorMap): matplotlib.colors.LinearSegmentedColormap のラッパークラス。
        Returns:
            np.ndarray: TRIの画像データ。
        """
        options = self.get_tri_options()
        if not options.execute:
            # TRIを計算しない場合
            return None
        msg.tri_spec(MESSAGE_CATEGORY, options)
        tri_dst = dst.tri()
        tri_ary = tri_dst.array()
        tri_dst = None
        if options.execute_gaussian_filter:
            # ガウシアンフィルタを適用
            kernel = kernels.gaussian_kernel(options.sigma)
            tri_ary = scipy.signal.convolve(
                tri_ary, kernel, mode='same', method='fft'
            )
        if options.execute_outlier_treatment:
            # 外れ値処理を実行
            tri_ary = dst.outlier_treatment_array_by_iqr(
                threshold=options.iqr, raster_ary=tri_ary
            )
        if options.change_alpha:
            # 透過率を変更
            cmap = self.relative_alpha_change(cmap, options.alpha)
        tri_img = cmap.values_to_img(tri_ary)
        return tri_img

    def hillshade(self, dst: CustomGdalDataset, cmap: LinearColorMap) -> np.ndarray:
        """
        ## Summary:
            陰影起伏図を計算する。
        Args:
            dst (CustomGdalDataset): 陰影起伏図が入力された gdal.Dataset のラッパークラス。
            cmap (LinearColorMap): matplotlib.colors.LinearSegmentedColormap のラッパークラス。
        Returns:
            np.ndarray: 陰影起伏図の画像データ
        """
        options = self.get_hillshade_options()
        msg.hillshade_spec(MESSAGE_CATEGORY, options)
        kwargs = {
            'azimuth': options.azimuth,
            'altitude': options.altitude,
            'z_factor': options.z_factor,
            'combined': options.combined,
            'multiDirectional': options.hillshade_type == 'multiple',
            'return_array': True,
        }
        hillshade_ary = dst.hillshade(**kwargs)
        if options.execute_gaussian_filter:
            # ガウシアンフィルタを適用
            kernel = kernels.gaussian_kernel(options.sigma)
            hillshade_ary = scipy.signal.convolve(
                hillshade_ary, kernel, mode='same', method='fft'
            )
        if options.execute_outlier_treatment:
            # 外れ値処理を実行
            hillshade_ary = dst.outlier_treatment_array_by_iqr(
                threshold=options.iqr, raster_ary=hillshade_ary
            )
        hillshade_img = cmap.values_to_img(hillshade_ary)
        return hillshade_img

    def composite_images(self, 
        slope_img: np.ndarray, 
        tpi_img: np.ndarray, 
        mtpi_img: np.ndarray,
        tri_img: np.ndarray, 
        hillshade_img: np.ndarray
    ) -> np.ndarray:
        """
        ## Summary:
            画像を合成する。
        Args:
            slope_img (np.ndarray): 傾斜の画像データ。
            tpi_img (np.ndarray): TPIの画像データ。
            mtpi_img (np.ndarray): 2枚目のTPIの画像データ。
            tri_img (np.ndarray): TRIの画像データ。
            hillshade_img (np.ndarray): 陰影起伏図の画像データ。
        Returns:
            np.ndarray: 合成された画像データ。
        """
        msg.start_composite_image(MESSAGE_CATEGORY)
        """画像を合成する。"""
        slope_img = Image.fromarray(slope_img.astype('uint8'))
        tpi_img = Image.fromarray(tpi_img.astype('uint8'))
        hillshade_img = Image.fromarray(hillshade_img.astype('uint8'))
        if tri_img is not None:
            # TRIの画像がある場合は陰影起伏図の上に合成
            tri_img = Image.fromarray(tri_img.astype('uint8'))
            composited = Image.alpha_composite(hillshade_img, tri_img)
        else:
            composited = hillshade_img

        if mtpi_img is not None:
            # 2枚目のTPIの画像がある場合は陰影起伏図かTRIの上に合成
            mtpi_img = Image.fromarray(mtpi_img.astype('uint8'))
            composited = Image.alpha_composite(composited, mtpi_img)
        # 残りのTPIと傾斜を合成
        composited = Image.alpha_composite(composited, tpi_img)
        composited = Image.alpha_composite(composited, slope_img)
        msg.end_composite_image(MESSAGE_CATEGORY)
        return composited
        
    def show_convolution_kernel(self) -> None:
        """
        ## Summary:
            設定したカーネルをmatplotlibで表示する。
        """
        self._dst = self.read_raster()
        if self.get_first_resample_spec().execute:
            reply = QMessageBox.question(
                None, 
                'Message ...', 
                self.tr('リサンプルを行いますか？'), 
                QMessageBox.Yes | QMessageBox.No, 
                QMessageBox.No
            )
            if reply == QMessageBox.Yes:
                self._dst = self.resampling()
        self.show_kernel(self._dst)
        self._dst = None
    
    def cell_size_in_metre(self) -> float:
        """
        ## Summary:
            Rasterのセルサイズをメートル単位で取得する。
        Returns:
            float: ラスタのセルサイズ。
        """
        dst = self.read_raster()
        if dst is not None:
            return dst.cell_size_in_metre().x_size
        else:
            return self.unit

    def relative_alpha_change(self, cmap: LinearColorMap, coef: float) -> LinearColorMap:
        """
        ## Summary:
            カラーマップの透過率を変更する。
        Args:
            cmap (LinearColorMap): matplotlib.colors.LinearSegmentedColormap のラッパークラス。
            coef (float): 透過率の係数。
        Returns:
            LinearColorMap: 透過率を変更したカラーマップ。
        """
        new_colors = []
        for color in cmap.get_registered_color('rgba'):
            alpha = color[-1] * coef
            if 1.0 < alpha:
                alpha = 1.0
            elif alpha < 0.0:
                alpha = 0.0
            new_colors.append([color[0], color[1], color[2], alpha])
        new_cmap = custom_cmap.color_list_to_linear_cmap(new_colors)
        return new_cmap
    
    def unsharpn_mask(self, img: Image.Image) -> Image.Image:
        """
        ## Summary:
            unsharpn maskを適用して画像をシャープにする。
        Args:
            img (Image.Image): 画像データ。
        Returns:
            Image.Image: unsharpn maskを適用した画像データ。
        """
        options = self.get_others_options()
        if not options.execute_unsharpn_mask:
            return img
        filter_ = ImageFilter.UnsharpMask(
            radius=options.unsharpn_radius, 
            percent=options.unsharpn_percent, 
            threshold=options.unsharpn_threshold
        )
        return img.filter(filter_)
    
    def change_contrast(self, img: Image.Image) -> Image.Image:
        """
        ## Summary:
            画像のコントラストを変更する。
        Args:
            img (Image.Image): 画像データ。
        Returns:
            Image.Image: コントラストを変更した画像データ。
        """
        options = self.get_others_options()
        if not options.execute_contrast:
            return img
        enhancer = ImageEnhance.Contrast(img)
        return enhancer.enhance(options.contrast_value)

    def image_to_gdal_dataset(self, 
        img: np.ndarray, 
        dst: CustomGdalDataset
    ) -> CustomGdalDataset:
        """
        ## Summary:
            画像をGDALデータセットに変換する。
        Args:
            img (np.ndarray): 画像データ。
            dst (CustomGdalDataset): gdal.Dataset のラッパークラス。
        Returns:
            CustomGdalDataset: 画像をGDALデータセットに変換したもの。
        """
        raster = np.array([img[:, :, i] for i in range(img.shape[-1])])
        new_dst = dst.write_ary_to_mem(
            raster,
            data_type=gdal.GDT_Byte,
            out_nodata=-9999,
            raster_count=3,
        )
        return new_dst

    def show_sample_dst(self, dst: CustomGdalDataset) -> None:
        """
        ## Summary:
            サンプルのチェックボックスが選択された場合、一部の範囲を表示する。
        Args:
            dst (CustomGdalDataset): gdal.Dataset のラッパークラス。
        """
        msg.show_raster(MESSAGE_CATEGORY)
        fig, ax = plt.subplots()
        dst.plot_raster(fig, ax)
        plt.show()

    def open_document(self):
        """
        ## Summary:
            ドキュメントを開く。
        """
        reply = QMessageBox.question(
            None,
            'Message ...',
            'Do you want to open a web page?',
            QMessageBox.Yes | QMessageBox.No,
            QMessageBox.No
        )
        if reply == QMessageBox.No:
            return
        local = QSettings().value('locale/userLocale')[0:2]
        if local == 'ja':
            webbrowser.open(config.doc_jp)
        else:
            webbrowser.open(config.doc_en)



class GenerateMapTask(QgsTask):
    MESSAGE_CATEGORY = 'TopoMaps Plugin'
    taskCompleted = pyqtSignal(bool)

    def __init__(self, topo_maps_dlg: TopoMapsDialog, callback):
        super().__init__('Generate TopoMaps', QgsTask.CanCancel)
        self.dlg = topo_maps_dlg
        self.in_crs = False
        self.callback = callback
        self.progress = 0
        self.setProgress(self.progress)
        self.exception = None
        self.new_dst = None
    
    @staticmethod
    def start_to_end(func):
        """
        ## Summary:
            タスクの実行時間をログに出力するデコレータ。
        """
        def wrapper(self, *args, **kwargs):
            QgsMessageLog.logMessage(
                f"Start {MESSAGE_CATEGORY} Task",
                MESSAGE_CATEGORY, 
                Qgis.Info
            )
            result = func(self, *args, **kwargs)
            QgsMessageLog.logMessage(
                f"End of task",
                MESSAGE_CATEGORY, 
                Qgis.Info
            )
            return result
        return wrapper
    
    @start_to_end
    def run(self):
        """
        ## Summary:
            タスクの実行。
        """
        self.setProgress(self.progress)
        self.dlg._dst = gdal_open(self.dlg.get_input_file_path())
        if not self.dlg._dst.check_crs_is_metre():
            # メートル単位でない場合はUTM座標系に変換。
            self.in_crs = self.dlg._dst.GetProjection()
            self.dlg._dst = self.dlg._dst.estimate_utm_and_reprojected_dataset()
        # リサンプリング
        dst = self.dlg.resampling()
        self.progress += 10
        self.setProgress(self.progress)
        # 出力設定を取得
        output_spec = self.dlg.get_output_spec()
        if output_spec.sample_only:
            # サンプルを表示する場合は一部の範囲のみを切り抜く
            sampling_raster = SamplingRaster(
                dst, 
                output_spec.sampling_max_rows, 
                output_spec.sampling_max_cols
            )
            dst = sampling_raster.sample_dst
        cmaps = self.dlg.get_cmaps()
        # 並列処理で各材料を計算
        with concurrent.futures.ThreadPoolExecutor() as executor:
            # 傾斜の計算
            slope_future = executor.submit(
                self.start_generating_slope,
                dst,
                cmaps.get('slope')
            )
            # TPIの計算
            tpi_future = executor.submit(
                self.start_generating_tpi,
                dst,
                cmaps.get('tpi')
            )
            # 2枚目のTPIの計算
            if self.dlg.get_tpi_options().multiple_tpi:
                mtpi_future = executor.submit(
                    self.start_generating_multi_tpi,
                    dst,
                    cmaps.get('tpi')
                )
            # TRIの計算
            tri_future = executor.submit(
                self.start_generating_tri,
                dst,
                cmaps.get('tri')
            )
            # 陰影起伏図の計算
            hillshade_future = executor.submit(
                self.start_generating_hillshade,
                dst,
                cmaps.get('hillshade')
            )
            slope_img = slope_future.result()
            tpi_img = tpi_future.result()
            tri_img = tri_future.result()
            if self.dlg.get_tpi_options().multiple_tpi:
                mtpi_img = mtpi_future.result()
            else:
                mtpi_img = None
            hillshade_img = hillshade_future.result()
        # 画像の合成
        composited_img = self.dlg.composite_images(
            slope_img, tpi_img, mtpi_img, tri_img, hillshade_img
        )
        self.progress += 5
        self.setProgress(self.progress)
        # 画像をシャープにする
        composited_img = self.dlg.unsharpn_mask(composited_img)
        self.progress += 3
        self.setProgress(self.progress)
        # コントラストを変更
        composited_img = self.dlg.change_contrast(composited_img)
        self.progress += 3
        self.setProgress(self.progress)
        # 画像をGDALデータセットに変換
        self.new_dst = self.dlg.image_to_gdal_dataset(np.array(composited_img), dst)
        if self.in_crs:
            # 元の座標系に戻す
            self.new_dst = self.new_dst.reprojected_dataset(self.in_crs)
        # メモリを解放
        self.dlg._dst = dst = None
        self.setProgress(100)
        if self.isCanceled():
            return False
        return True
    
    def start_generating_slope(self, 
        dst: CustomGdalDataset, 
        cmap: LinearColorMap
    ) -> np.ndarray:
        """
        ## Summary:
            Task内で傾斜を計算する。
        Args:
            dst (CustomGdalDataset): 傾斜が入力された gdal.Dataset のラッパークラス。
            cmap (LinearColorMap): matplotlib.colors.LinearSegmentedColormap のラッパークラス。
        """
        msg.start_slope_calculation(MESSAGE_CATEGORY)
        slope_img = self.dlg.slope(dst, cmap)
        self.progress += 20
        msg.end_slope_calculation(MESSAGE_CATEGORY)
        self.setProgress(self.progress)
        return slope_img

    def start_generating_tpi(self, 
        dst: CustomGdalDataset,
        cmap: LinearColorMap
    ) -> np.ndarray:
        """
        ## Summary:
            Task内でTPIを計算する。
        Args:
            dst (CustomGdalDataset): TPIが入力された gdal.Dataset のラッパークラス。
            cmap (LinearColorMap): matplotlib.colors.LinearSegmentedColormap のラッパークラス。
        """
        msg.start_tpi_calculation(MESSAGE_CATEGORY)
        tpi_img = self.dlg.tpi(dst, cmap, multiples=False)
        self.progress += 20
        msg.end_tpi_calculation(MESSAGE_CATEGORY)
        self.setProgress(self.progress)
        return tpi_img

    def start_generating_multi_tpi(self, 
        dst: CustomGdalDataset, 
        cmap: LinearColorMap
    ) -> np.ndarray:
        """
        ## Summary:
            Task内で2枚目のTPIを計算する。
        Args:
            dst (CustomGdalDataset): 2枚目のTPIが入力された gdal.Dataset のラッパークラス。
            cmap (LinearColorMap): matplotlib.colors.LinearSegmentedColormap のラッパークラス。
        """
        mtpi_img = self.dlg.tpi(dst, cmap, multiples=True)
        self.progress += 25
        self.setProgress(self.progress)
        return mtpi_img
        
    def start_generating_tri(self, 
        dst: CustomGdalDataset,
        cmap: LinearColorMap
    ) -> np.ndarray:
        """
        ## Summary:
            Task内でTRIを計算する。
        Args:
            dst (CustomGdalDataset): TRIが入力された gdal.Dataset のラッパークス。
            cmap (LinearColorMap): matplotlib.colors.LinearSegmentedColormap
        """
        msg.start_tri_calculation(MESSAGE_CATEGORY)
        tri_img = self.dlg.tri(dst, cmap)
        self.progress += 5
        msg.end_tri_calculation(MESSAGE_CATEGORY)
        self.setProgress(self.progress)
        return tri_img

    def start_generating_hillshade(self, 
        dst: CustomGdalDataset,
        cmap: LinearColorMap
    ) -> np.ndarray:
        """
        ## Summary:
            Task内で陰影図を計算する。
        Args:
            dst (CustomGdalDataset): 陰影図が入力された gdal.Dataset のラッパークラス。
            cmap (LinearColorMap): matplotlib.colors.LinearSegmentedColormap のラッパークラス。
        """
        msg.start_hillshade_calculation(MESSAGE_CATEGORY)
        hillshade_img = self.dlg.hillshade(dst, cmap)
        self.progress += 5
        msg.end_hillshade_calculation(MESSAGE_CATEGORY)
        self.setProgress(self.progress)
        return hillshade_img 
    
    def finished(self, result):
        """
        ## Summary:
            タスクが終了したときの処理。
        Args:
            result (bool): タスクが正常に終了したかどうか。
        """
        super().finished(result)
        self.taskCompleted.emit(result)
        if result:
            # 問題なく計算が終了した場合
            msg.finished_msg_on_the_bar()
            if self.callback is not None:
                self.callback(self)
        else:
            if self.exception is None:
                # タスクが手動でキャンセルされた場合
                msg.user_cancel_msg_on_the_bar()
                msg.user_cancel_msg(self.MESSAGE_CATEGORY)
            else:
                # 例外が発生した場合
                msg.exception_msg_on_the_bar()
                msg.exception_msg(self.MESSAGE_CATEGORY, self.exception)
                raise self.exception
            self.dlg.setEnabled(True)
        self.taskCompleted.emit(result)

    def cancel(self):
        """
        ## Summary:
            タスクがキャンセルされたときの処理。
        """
        msg.user_cancel_msg(self.MESSAGE_CATEGORY)
        self.dlg.setEnabled(True)
        super().cancel()