text: from __future__ import absolute_import
  
  import os
  import copy
  import math
  from ..model.fileio import FileIOModel
  from ..model.lineplot import LinePlotModel
  from ..model.parameters import ParamModel, save_as, fit_strategy_list, bound_options
  from ..model.draw_image import DrawImageAdvanced
  from ..model.draw_image_rgb import DrawImageRGB
  from ..model.fit_spectrum import Fit1D, get_cs
  from ..model.roi_model import ROIModel
  from ..model.param_data import param_data
  
  from ..core.xrf_utils import get_eline_energy
  
  import logging
  
  logger = logging.getLogger(__name__)
  
  
  class GlobalProcessingClasses:
      def __init__(self):
          self.defaults = None
          self.io_model = None
          self.param_model = None
          self.plot_model = None
          self.fit_model = None
          self.roi_model = None
          self.img_model_adv = None
          self.img_model_rgb = None
  
      def _get_defaults(self):
          # Set working directory to current working directory (if PyXRF is started from shell)
          working_directory = os.getcwd()
          logger.info(f"Starting PyXRF in the current working directory '{working_directory}'")
          default_parameters = param_data
          return working_directory, default_parameters
  
      def initialize(self):
          """
          Run the sequence of actions needed to initialize PyXRF modules.
          """
          working_directory, default_parameters = self._get_defaults()
          self.io_model = FileIOModel(working_directory=working_directory)
          self.param_model = ParamModel(default_parameters=default_parameters, io_model=self.io_model)
          self.plot_model = LinePlotModel(param_model=self.param_model, io_model=self.io_model)
          self.fit_model = Fit1D(
              param_model=self.param_model, io_model=self.io_model, working_directory=working_directory
          )
          self.roi_model = ROIModel(param_model=self.param_model, io_model=self.io_model)
          self.img_model_adv = DrawImageAdvanced(io_model=self.io_model)
          self.img_model_rgb = DrawImageRGB(io_model=self.io_model, img_model_adv=self.img_model_adv)
  
          # Initialization needed to eliminate program crash
          self.plot_model.roi_dict = self.roi_model.roi_dict
  
          # send working directory changes to different models
          self.io_model.observe("working_directory", self.fit_model.result_folder_changed)
          self.io_model.observe("working_directory", self.roi_model.result_folder_changed)
          self.io_model.observe("selected_file_name", self.fit_model.data_title_update)
          self.io_model.observe("selected_file_name", self.plot_model.exp_label_update)
          self.io_model.observe("selected_file_name", self.roi_model.data_title_update)
  
          # send the same file to fit model, as fitting results need to be saved
          self.io_model.observe("file_name", self.fit_model.filename_update)
          self.io_model.observe("file_name", self.plot_model.plot_exp_data_update)
          self.io_model.observe("file_name", self.roi_model.filename_update)
          self.io_model.observe("runid", self.fit_model.runid_update)
  
          # Perform updates when 'io_model.data' is changed (no data is passed)
          self.io_model.observe("data", self.plot_model.exp_data_update)
  
          # send img dict to img_model for visualization
          self.io_model.observe("img_dict_is_updated", self.fit_model.img_dict_updated)
          self.io_model.observe("img_dict_is_updated", self.img_model_adv.img_dict_updated)
          self.io_model.observe("img_dict_is_updated", self.img_model_rgb.img_dict_updated)
  
          self.io_model.observe("incident_energy_set", self.plot_model.set_incident_energy)
          self.io_model.observe("incident_energy_set", self.img_model_adv.set_incident_energy)
  
          self.img_model_adv.observe("scaler_name_index", self.fit_model.scaler_index_update)
  
          self.img_model_adv.observe("dict_to_plot", self.fit_model.dict_to_plot_update)
          self.img_model_adv.observe("img_title", self.fit_model.img_title_update)
  
          self.param_model.observe("energy_bound_high_buf", self.plot_model.energy_bound_high_update)
          self.param_model.observe("energy_bound_low_buf", self.plot_model.energy_bound_low_update)
  
          logger.info("pyxrf started.")
  
      def add_parameters_changed_cb(self, cb):
          """
          Add callback to the list of callback function that are called after parameters are updated.
          """
          self.param_model.add_parameters_changed_cb(cb)
  
      def remove_parameters_changed_cb(self, cb):
          """
          Remove reference from the list of callback functions.
          """
          self.param_model.remove_parameters_changed_cb(cb)
  
      def fitting_parameters_changed(self):
          self.param_model.parameters_changed()
  
      def get_window_title(self):
          """
          Returns current title of the Main Window of the application.
          """
          return self.io_model.window_title
  
      def is_databroker_available(self):
          return self.io_model.is_databroker_available()
  
      def open_data_file(self, file_path):
  
          self.io_model.data_ready = False
          # only load one file
          # 'temp' is used to reload the same file, otherwise file_name will not update
          self.io_model.file_name = "temp"
          f_dir, f_name = os.path.split(file_path)
          self.io_model.working_directory = f_dir
  
          def _update_data():
              self.fit_model.fit_img = {}  # clear dict in fitmodel to rm old results
              # This will draw empty (hidden) preview plot, since no channels are selected.
              self.plot_model.update_preview_spectrum_plot()
              self.plot_model.update_total_count_map_preview(new_plot=True)
  
          # The following statement initiates file loading. It may raise exceptions
          try:
              self.io_model.file_name = f_name
  
          except Exception:
              _update_data()
              self.fitting_parameters_changed()
  
              # For plotting purposes, otherwise plot will not update
              self.plot_model.plot_exp_opt = False
  
              self.plot_model.show_fit_opt = False
  
              logger.info(f"Failed to load the file '{f_name}'.")
              # Clear file name or scan id from window title. This does not update
              #   the displayed title.
              self.io_model.window_title_clear()
              raise
          else:
              _update_data()
              self.fitting_parameters_changed()
  
              # For plotting purposes, otherwise plot will not update
              try:
                  # TODO: permanent fix for the following issue is needed
                  # Suppress Matplotlib exception.
                  #   draw_glyph_to_bitmap: glyph num is out of range
                  # This is a temporary fix, proper debugging is needed so that try..except
                  #   could be removed
                  self.plot_model.plot_exp_opt = False
              except Exception as ex:
                  logger.error(f"Exception was raised while removing experimental data plot: {str(ex)}")
              try:
                  self.plot_model.plot_exp_opt = True
              except Exception as ex:
                  logger.error(f"Exception was raised while plotting experimental data: {str(ex)}")
              try:
                  self.plot_model.show_fit_opt = False
              except Exception as ex:
                  logger.error(f"Exception was raised while removing fitted data plot: {str(ex)}")
              try:
                  self.plot_model.show_fit_opt = True
              except Exception as ex:
                  logger.error(f"Exception was raised while plotting fitted data: {str(ex)}")
  
              # Change window title (include file name). This does not update the visible title,
              #   only the text attribute of 'io_model' class.
              self.io_model.window_title_set_file_name(f_name)
              print("======== Set window title")
  
              if not self.io_model.incident_energy_available:
                  msg = (
                      "Incident energy is not available in scan metadata and must be set manually. "
                      "Incident energy may be set by changing 'Incident energy, keV' parameter "
                      "in the dialog boxes opened using 'Find Automatically...' ('Find Elements "
                      "in sample' or 'General...' ('General Settings for Fitting Alogirthm') "
                      "buttons in 'Model' tab."
                  )
              else:
                  msg = ""
  
              logger.info(f"Loading of the file '{file_path}' is completed.")
  
              return msg
  
      def load_run_from_db(self, run_id_uid):
          """
          Parameters
          ----------
          run_id_uid: int or str
              int - run ID (e.g. -1, 45326), str - run UID
          """
  
          # Copy value to 'runid' field ('runid' field should not change
          #   during processing unless new data is loaded)
          self.io_model.data_ready = False
  
          def _update_data():
              self.fit_model.fit_img = {}  # clear dict in fitmodel to rm old results
              # This will draw empty (hidden) preview plot, since no channels are selected.
              self.plot_model.update_preview_spectrum_plot()
              self.plot_model.update_total_count_map_preview(new_plot=True)
  
          try:
              self.io_model.load_data_runid(run_id_uid)
  
          except Exception:
              _update_data()
              self.fitting_parameters_changed()
  
              # For plotting purposes, otherwise plot will not update
              self.plot_model.plot_exp_opt = False
              self.plot_model.show_fit_opt = False
  
              logger.info(f"Failed to load the run #{run_id_uid}.")
              # Clear file name or scan id from window title. This does not update
              #   the displayed title.
              self.io_model.window_title_clear()
              raise
  
          else:
              _update_data()
              self.fitting_parameters_changed()
  
              # For plotting purposes, otherwise plot will not update
              self.plot_model.plot_exp_opt = False
              self.plot_model.plot_exp_opt = True
              self.plot_model.show_fit_opt = False
              self.plot_model.show_fit_opt = True
  
              # Change window title (include file name). This does not update the visible title,
              #   only the text attribute of 'io_model' class.
              self.io_model.window_title_set_run_id(self.io_model.runid)
  
              file_name = self.io_model.file_name
              msg = ""
  
              logger.info("Loading of the run is completed")
  
              return msg, file_name
  
      def select_preview_dataset(self, *, dset_name, is_visible):
          """
          Select datasets for preview. A dataset may be selected or deselected by
          providing the name of the dataset (a key in `io_model.datasets` dictionary)
          and `is_visible` flag. Multiple datasets may be visualized by selecting the
          datasets one by one.
  
          Parameters
          ----------
          dset_name: str
              dataset name (a key in `io_model.datasets` dictionary)
          is_visible: bool
              True - show the dataset, False - hide the dataset
          """
          self.io_model.data_sets[dset_name].selected_for_preview = True if is_visible else False
          self.io_model.update_data_set_buffers()
          self.plot_model.update_preview_spectrum_plot()
          self.plot_model.update_total_count_map_preview()
  
      def get_current_working_directory(self):
          """
          Return current working directory (defined in 'io_model')
          """
          return self.io_model.working_directory
  
      def set_current_working_directory(self, working_directory):
          """
          Sets current working directory ('io_model')
          """
          self.io_model.working_directory = working_directory
  
      def get_load_each_channel(self):
          """
          Get the value of `load_each_channel` flag, which indicates if data from
          all detector channels will be loaded.
          """
          return self.io_model.load_each_channel
  
      def set_load_each_channel(self, load_each_channel):
          """
          Get the value of `load_each_channel` flag, which indicates if data from
          all detector channels will be loaded.
          """
          self.io_model.load_each_channel = load_each_channel
  
      def get_file_channel_list(self):
          """
          Get the list of channels in the currently loaded file (or run)
          """
          return self.io_model.file_channel_list
  
      def is_dset_item_selected_for_preview(self, item):
          """
          Check if dataset item is selected for preview.
          """
          return bool(self.io_model.data_sets[item].selected_for_preview)
  
      def get_loaded_file_name(self):
          """
          Returns the name of currently loaded file.
          """
          return self.io_model.file_name
  
      def is_scan_metadata_available(self):
          """
          Returns the flag that indicates if scan metadata is loaded
          """
          return bool(self.io_model.scan_metadata_available)
  
      def get_formatted_metadata(self):
          """
          Returns formatted metadata (for displaying or printing)
          """
          return self.io_model.scan_metadata.get_formatted_output()
  
      def get_metadata_scan_id(self):
          """
          Reads Run ID from metadata. Check if metadata exists using `is_scan_metadata_available()`
          before calling this function.
          """
          if self.is_scan_metadata_available():
              scan_id = self.io_model.scan_metadata["scan_id"]
          else:
              scan_id = 0  # This is mostly for preventing crashes.
          return scan_id
  
      def get_metadata_scan_uid(self):
          """
          Reads Run UID from metadata. Check if metadata exists using `is_scan_metadata_available()`
          before calling this function.
          """
          if self.is_scan_metadata_available():
              scan_uid = self.io_model.scan_metadata["scan_uid"]
          else:
              scan_uid = ""  # This is mostly for preventing crashes.
          return scan_uid
  
      def get_current_run_id(self):
          """
          Returns value of currently set Run ID (in `io_model` class)
          """
          return self.io_model.runid
  
      def is_xrf_maps_available(self):
          """
          Returns the flag that indicates if XRF maps are currently available (loaded or computed).
          """
          return bool(self.io_model.is_xrf_maps_available())
  
      def set_data_channel(self, channel_index):
          self.io_model.file_opt = channel_index
  
          # For plotting purposes, otherwise plot will not update
          self.plot_model.plot_exp_opt = False
          self.plot_model.plot_exp_opt = True
          self.plot_model.show_fit_opt = False
          self.plot_model.show_fit_opt = True
  
      def is_roi_selection_active(self):
          """
          Returns the flag that indicates if spatial ROI is activated.
          """
          return bool(self.io_model.roi_selection_active)
  
      def set_roi_selection_active(self, active):
          """
          Sets the flag that indicates if spatial ROI is activated.
          """
          self.io_model.roi_selection_active = active
  
      def get_preview_spatial_roi(self):
          """
          Returns currently selected spatial ROI (for preview)
          """
          spatial_roi = {
              "row_start": self.io_model.roi_row_start,
              "col_start": self.io_model.roi_col_start,
              "row_end": self.io_model.roi_row_end,
              "col_end": self.io_model.roi_col_end,
          }
          return spatial_roi
  
      def set_preview_spatial_roi(self, spatial_roi):
          """
          Set spatial ROI (for preview)
          """
          self.io_model.roi_row_start = spatial_roi["row_start"]
          self.io_model.roi_col_start = spatial_roi["col_start"]
          self.io_model.roi_row_end = spatial_roi["row_end"]
          self.io_model.roi_col_end = spatial_roi["col_end"]
  
      def is_mask_selection_active(self):
          """
          Returns the flag that indicates if mask for ROI selection is activated.
          """
          return bool(self.io_model.mask_active)
  
      def set_mask_selection_active(self, active):
          """
          Sets the flag that indicates if mask for ROI selection is activated.
          """
          self.io_model.mask_active = active
  
      def get_mask_selection_file_path(self):
          """
          Returns currently selected path to mask file (ROI selection)
          """
          return self.io_model.mask_file_path
  
      def set_mask_selection_file_path(self, file_path):
          """
          Sets path to mask file (ROI selection). It also sets mask name.
          """
          self.io_model.mask_file_path = file_path
          # At this point the mask name is just the file name
          self.io_model.mask_name = os.path.split(file_path)[-1]
  
      def apply_mask_to_datasets(self):
          self.io_model.apply_mask_to_datasets()
          self.plot_model.update_preview_spectrum_plot()
  
      # ==========================================================================
      #        The following methods are used by the preview widgets
  
      def get_preview_plot_type(self):
          return self.plot_model.plot_type_preview.value
  
      def set_preview_plot_type(self, plot_type):
          self.plot_model.plot_type_preview = plot_type
  
      def get_preview_energy_range(self):
          return self.plot_model.energy_range_preview
  
      def set_preview_energy_range(self, energy_range):
          self.plot_model.energy_range_preview = energy_range
  
      def update_preview_spectrum_plot(self):
          self.plot_model.update_preview_spectrum_plot()
  
      def get_preview_map_color_scheme(self):
          return self.plot_model.map_preview_color_scheme
  
      def set_preview_map_color_scheme(self, color_scheme):
          self.plot_model.map_preview_color_scheme = color_scheme
  
      def get_preview_map_type(self):
          return self.plot_model.map_type_preview
  
      def set_preview_map_type(self, map_type):
          self.plot_model.map_type_preview = map_type
  
      def get_preview_map_axes_units(self):
          return self.plot_model.map_axes_units_preview
  
      def set_preview_map_axes_units(self, map_axes_units):
          self.plot_model.map_axes_units_preview = map_axes_units
  
      def get_preview_map_range(self):
          return self.plot_model.map_preview_range_low, self.plot_model.map_preview_range_high
  
      def set_preview_map_range(self, low, high):
          self.plot_model.set_map_preview_range(low=low, high=high)
  
      def get_dataset_preview_count_map_range(self):
          return self.io_model.get_dataset_preview_count_map_range()
  
      def update_preview_total_count_map(self):
          self.plot_model.update_total_count_map_preview()
  
      # ==========================================================================
      #          The following methods are used by widgets XRF Maps tab
      #                  to read/write data from model classes
  
      def get_maps_info_table(self):
          """
          The function builds and returns two tables: a table of value ranges for each map
          in the dataset; a table of limit values that represent selection for the displayed
          values for each map of the dataset.
  
          Returns
          -------
          range_table: list(list)
              The table is represented as list of lists. Every element of the outer list is
              a table row. Each row has 3 elements: 0 - key (emission line or map name, str),
              1 - lower boundary (float), 2 - upper boundary (float).
          limit_table: list(list)
              The table is represented as list of lists. Every element of the outer list is
              a table row. Each row has 3 elements: 0 - key (emission line or map name, str),
              1 - lower limit value (float), 2 - upper limit value (float).
          """
          # Check if 'range_dict' and 'limit_dict' have the same set of keys
          ks = self.img_model_adv.map_keys
          ks_range = list(self.img_model_adv.range_dict.keys())
          ks_limit = list(self.img_model_adv.limit_dict.keys())
          ks_show = list(self.img_model_adv.stat_dict.keys())
          if set(ks) != set(ks_limit):
              raise RuntimeError(
                  "The list of keys in 'limit_dict' is not as expected: "
                  f"limit_dict.keys(): {ks_limit} expected: {ks}"
              )
          if set(ks) != set(ks_range):
              raise RuntimeError(
                  "The list of keys in 'range_dict' is not as expected: "
                  f"range_dict.keys(): {ks_range} expected: {ks}"
              )
          if set(ks) != set(ks_show):
              raise RuntimeError(
                  "The list of keys in 'stat_dict' is not as expected: "
                  f"stat_dict.keys(): {ks_show} expected: {ks}"
              )
  
          range_table = []
          limit_table = []
          show_table = []
          for key in ks:
              rng_low = self.img_model_adv.range_dict[key]["low"]
              rng_high = self.img_model_adv.range_dict[key]["high"]
              limit_low_norm = self.img_model_adv.limit_dict[key]["low"]
              limit_high_norm = self.img_model_adv.limit_dict[key]["high"]
              limit_low = rng_low + (rng_high - rng_low) * limit_low_norm / 100.0
              limit_high = rng_low + (rng_high - rng_low) * limit_high_norm / 100.0
              range_table.append([key, rng_low, rng_high])
              limit_table.append([key, limit_low, limit_high])
              show_table.append([key, bool(self.img_model_adv.stat_dict[key])])
  
          return range_table, limit_table, show_table
  
      def set_maps_limit_table(self, limit_table, show_table):
          """
          Write updated range limits to 'img_model_adv.limit_dict'. Used by 'Image Wizard'.
  
          Parameters
          ----------
          limit_table: list(list)
              The table is represented as list of lists. Every element of the outer list is
              a table row. Each row has 3 elements: 0 - key (eline or map name, str),
              1 - low limit (float), 2 - high limit (float).
          """
          # Verify: the keys in both tables must match 'self.img_model_adv.map_keys'
          limit_table_keys = [_[0] for _ in limit_table]
          if set(limit_table_keys) != set(self.img_model_adv.map_keys):
              raise ValueError(
                  "GlobalProcessingClasses:set_maps_info_table: keys don't match:"
                  f"limit_table has keys {limit_table_keys}, "
                  f"original keys {self.img_model_adv.map_keys}"
              )
  
          show_table_keys = [_[0] for _ in show_table]
          if set(show_table_keys) != set(self.img_model_adv.map_keys):
              raise ValueError(
                  "GlobalProcessingClasses:set_maps_info_table: keys don't match:"
                  f"show_table has keys {show_table_keys}, "
                  f"original keys {self.img_model_adv.map_keys}"
              )
  
          # Copy limits
          for row in limit_table:
              key, v_low, v_high = row
  
              rng_low = self.img_model_adv.range_dict[key]["low"]
              rng_high = self.img_model_adv.range_dict[key]["high"]
  
              v_low_norm = (v_low - rng_low) / (rng_high - rng_low) * 100.0
              v_high_norm = (v_high - rng_low) / (rng_high - rng_low) * 100.0
  
              self.img_model_adv.limit_dict[key]["low"] = v_low_norm
              self.img_model_adv.limit_dict[key]["high"] = v_high_norm
  
          # Copy 'show' status (whether the map should be shown)
          for row in show_table:
              key, show_status = row
              self.img_model_adv.stat_dict[key] = bool(show_status)
  
      def get_maps_dataset_list(self):
          dsets = list(self.io_model.img_dict_keys)
          dset_sel = self.get_maps_selected_dataset()  # The index in the list + 1 (0 - nothing is selected)
          return dsets, dset_sel
  
      def get_maps_scaler_list(self):
          "Return the list of available scalers for maps"
          scalers = list(self.img_model_adv.scaler_items)
          scaler_sel = self.get_maps_scaler_index()  # The index in the list + 1 (0 - nothing is selected)
          return scalers, scaler_sel
  
      def get_maps_selected_dataset(self):
          """Returns selected dataset (XRF Maps tab). Index: 0 - no dataset is selected,
          1, 2, ... datasets with index 0, 1, ... is selected"""
          # return int(self.img_model_adv.data_opt)
          return int(self.io_model.img_dict_default_selected_item)
  
      def set_maps_selected_dataset(self, dataset_index):
          """Select dataset (XRF Maps tab)"""
          # self.img_model_adv.select_dataset(dataset_index)
          self.io_model.select_img_dict_item(dataset_index)
  
      def get_maps_scaler_index(self):
          return self.img_model_adv.scaler_name_index
  
      def set_maps_scaler_index(self, scaler_index):
          self.img_model_adv.set_scaler_index(scaler_index)
  
      def get_maps_quant_norm_enabled(self):
          return bool(self.img_model_adv.quantitative_normalization)
  
      def set_maps_quant_norm_enabled(self, enable):
          self.img_model_adv.enable_quantitative_normalization(enable)
  
      def get_maps_scale_opt(self):
          """Returns selected plot type: `Linear` or `Log`"""
          return self.img_model_adv.scale_opt
  
      def set_maps_scale_opt(self, scale_opt):
          """Set plot type. Allowed values: `Linear` and `Log`"""
          self.img_model_adv.set_scale_opt(scale_opt)
  
      def get_maps_color_opt(self):
          """Returns selected plot color option: `Linear` or `Log`"""
          return self.img_model_adv.color_opt
  
      def set_maps_color_opt(self, color_opt):
          """Set plot type. Allowed values: `Linear` and `Log`"""
          self.img_model_adv.set_color_opt(color_opt)
  
      def get_maps_pixel_or_pos(self):
          """Get the current axes options for the plotted maps. Returned values:
          `Pixes` or `Positions`."""
          values = ["Pixels", "Positions"]
          return values[self.img_model_adv.pixel_or_pos]
  
      def set_maps_pixel_or_pos(self, pixel_or_pos):
          values = ["Pixels", "Positions"]
          self.img_model_adv.set_pixel_or_pos(values.index(pixel_or_pos))
  
      def get_maps_show_scatter_plot(self):
          return self.img_model_adv.scatter_show
  
      def set_maps_show_scatter_plot(self, is_scatter):
          self.img_model_adv.set_plot_scatter(is_scatter)
  
      def get_maps_grid_interpolate(self):
          return self.img_model_adv.grid_interpolate
  
      def set_maps_grid_interpolate(self, grid_interpolate):
          self.img_model_adv.set_grid_interpolate(grid_interpolate)
  
      def compute_map_ranges(self):
          self.img_model_adv.set_low_high_value()
  
      def redraw_maps(self):
          """Redraw maps in XRF Maps tab"""
          self.img_model_adv.show_image()
  
      # ==========================================================================
      #          The following methods are used by widgets RGB tab
      #                  to read/write data from model classes
  
      def get_rgb_maps_info_table(self):
          """
          The function builds and returns two tables: a table of value ranges for each map
          in the dataset; a table of limit values that represent selection for the displayed
          values for each map of the dataset.
  
          Returns
          -------
          range_table: list(list)
              The table is represented as list of lists. Every element of the outer list is
              a table row. Each row has 3 elements: 0 - key (emission line or map name, str),
              1 - lower boundary (float), 2 - upper boundary (float).
          limit_table: list(list)
              The table is represented as list of lists. Every element of the outer list is
              a table row. Each row has 3 elements: 0 - key (emission line or map name, str),
              1 - lower limit value (float), 2 - upper limit value (float).
          rgb_dict: dict
              dictionary that hold the displayed items: key - color ("red", "green" or "blue"),
              value - selected map represented by the key of `self.img_model_rgb.dict_to_plot`
              or `self.io_model.img_dict[<dataset>]` dictionaries.
          """
          # Check if 'range_dict' and 'limit_dict' have the same set of keys
          ks = self.img_model_rgb.map_keys
          ks_range = list(self.img_model_rgb.range_dict.keys())
          ks_limit = list(self.img_model_rgb.limit_dict.keys())
          rgb_dict = self.img_model_rgb.rgb_dict
          if set(ks) != set(ks_limit):
              raise RuntimeError(
                  "The list of keys in 'limit_dict' is not as expected: "
                  f"limit_dict.keys(): {ks_limit} expected: {ks}"
              )
          if set(ks) != set(ks_range):
              raise RuntimeError(
                  "The list of keys in 'range_dict' is not as expected: "
                  f"range_dict.keys(): {ks_range} expected: {ks}"
              )
          if len(rgb_dict) != len(self.img_model_rgb.rgb_keys) or set(rgb_dict.keys()) != set(
              self.img_model_rgb.rgb_keys
          ):
              raise ValueError(
                  "GlobalProcessingClasses.get_rgb_maps_info_table(): "
                  f"incorrect set of keys in 'rgb_dict': {list(rgb_dict.keys())}, "
                  f"expected: {self.img_model_rgb.rgb_keys}"
              )
          for v in rgb_dict.values():
              if (v is not None) and (v not in ks):
                  raise ValueError(
                      f"GlobalProcessingClasses.get_rgb_maps_info_table(): Invalid key {v}. Allowed keys: {ks}"
                  )
  
          range_table = []
          limit_table = []
          for key in ks:
              rng_low = self.img_model_rgb.range_dict[key]["low"]
              rng_high = self.img_model_rgb.range_dict[key]["high"]
              limit_low_norm = self.img_model_rgb.limit_dict[key]["low"]
              limit_high_norm = self.img_model_rgb.limit_dict[key]["high"]
              limit_low = rng_low + (rng_high - rng_low) * limit_low_norm / 100.0
              limit_high = rng_low + (rng_high - rng_low) * limit_high_norm / 100.0
              range_table.append([key, rng_low, rng_high])
              limit_table.append([key, limit_low, limit_high])
  
          return range_table, limit_table, rgb_dict
  
      def set_rgb_maps_limit_table(self, limit_table, rgb_dict):
          """
          Write updated range limits to 'img_model_adv.limit_dict'. Used by 'Image Wizard'.
  
          Parameters
          ----------
          limit_table: list(list)
              The table is represented as list of lists. Every element of the outer list is
              a table row. Each row has 3 elements: 0 - key (eline or map name, str),
              1 - low limit (float), 2 - high limit (float).
          rgb_dict: dict
              dictionary that hold the displayed items: key - color ("red", "green" or "blue"),
              value - selected map represented by the key of `self.img_model_rgb.dict_to_plot`
              or `self.io_model.img_dict[<dataset>]` dictionaries.
          """
          # Verify: the keys in both tables must match 'self.img_model_adv.map_keys'
          limit_table_keys = [_[0] for _ in limit_table]
          if set(limit_table_keys) != set(self.img_model_rgb.map_keys):
              raise ValueError(
                  "GlobalProcessingClasses:set_maps_info_table: keys don't match:"
                  f"limit_table has keys {limit_table_keys}, "
                  f"original keys {self.img_model_rgb.map_keys}"
              )
  
          # Copy limits
          for row in limit_table:
              key, v_low, v_high = row
  
              rng_low = self.img_model_rgb.range_dict[key]["low"]
              rng_high = self.img_model_rgb.range_dict[key]["high"]
  
              v_low_norm = (v_low - rng_low) / (rng_high - rng_low) * 100.0
              v_high_norm = (v_high - rng_low) / (rng_high - rng_low) * 100.0
  
              self.img_model_rgb.limit_dict[key]["low"] = v_low_norm
              self.img_model_rgb.limit_dict[key]["high"] = v_high_norm
  
          self.img_model_rgb.rgb_dict = rgb_dict.copy()
  
      def get_rgb_maps_dataset_list(self):
          dsets = list(self.io_model.img_dict_keys)
          dset_sel = self.get_maps_selected_dataset()  # The index in the list + 1 (0 - nothing is selected)
          return dsets, dset_sel
  
      def get_rgb_maps_scaler_list(self):
          "Return the list of available scalers for maps"
          scalers = list(self.img_model_rgb.scaler_items)
          scaler_sel = self.get_maps_scaler_index()  # The index in the list + 1 (0 - nothing is selected)
          return scalers, scaler_sel
  
      def get_rgb_maps_selected_dataset(self):
          """Returns selected dataset (XRF Maps tab). Index: 0 - no dataset is selected,
          1, 2, ... datasets with index 0, 1, ... is selected"""
          # return int(self.img_model_rgb.data_opt)
          return int(self.io_model.img_dict_default_selected_item)
  
      def set_rgb_maps_selected_dataset(self, dataset_index):
          """Select dataset (XRF Maps tab)"""
          # self.img_model_rgb.select_dataset(dataset_index)
          self.io_model.select_img_dict_item(dataset_index)
  
      def get_rgb_maps_scaler_index(self):
          return self.img_model_rgb.scaler_name_index
  
      def set_rgb_maps_scaler_index(self, scaler_index):
          self.img_model_rgb.set_scaler_index(scaler_index)
  
      def get_rgb_maps_pixel_or_pos(self):
          """Get the current axes options for the plotted maps. Returned values:
          `Pixes` or `Positions`."""
          values = ["Pixels", "Positions"]
          return values[self.img_model_rgb.pixel_or_pos]
  
      def set_rgb_maps_pixel_or_pos(self, pixel_or_pos):
          values = ["Pixels", "Positions"]
          self.img_model_rgb.set_pixel_or_pos(values.index(pixel_or_pos))
  
      def get_rgb_maps_grid_interpolate(self):
          return self.img_model_rgb.grid_interpolate
  
      def set_rgb_maps_grid_interpolate(self, grid_interpolate):
          self.img_model_rgb.set_grid_interpolate(grid_interpolate)
  
      def get_rgb_maps_quant_norm_enabled(self):
          return bool(self.img_model_rgb.quantitative_normalization)
  
      def set_rgb_maps_quant_norm_enabled(self, enable):
          self.img_model_rgb.enable_quantitative_normalization(enable)
  
      def compute_rgb_map_ranges(self):
          self.img_model_rgb.set_low_high_value()
  
      def redraw_rgb_maps(self):
          """Redraw maps in XRF Maps tab"""
          self.img_model_rgb.show_image()
  
      # ==========================================================================
      #          The following methods are used by widgets related
      #      to loading and management of quantitative calibration data
      def load_quantitative_calibration_data(self, file_path):
          self.img_model_adv.load_quantitative_calibration_data(file_path)
  
      def get_quant_calibration_data(self):
          return self.img_model_adv.param_quant_analysis.calibration_data
  
      def get_quant_calibration_settings(self):
          return self.img_model_adv.param_quant_analysis.calibration_settings
  
      def get_quant_calibration_text_preview(self, file_path):
          return self.img_model_adv.param_quant_analysis.get_entry_text_preview(file_path)
  
      def quant_calibration_remove_entry(self, file_path):
          return self.img_model_adv.param_quant_analysis.remove_entry(file_path)
  
      def get_quant_calibration_file_path_list(self):
          return self.img_model_adv.param_quant_analysis.get_file_path_list()
  
      def get_quant_calibration_is_eline_selected(self, eline, file_path):
          return self.img_model_adv.param_quant_analysis.is_eline_selected(eline, file_path)
  
      def set_quant_calibration_select_eline(self, eline, file_path):
          self.img_model_adv.param_quant_analysis.select_eline(eline, file_path)
  
      def get_quant_calibration_distance_to_sample(self):
          return self.img_model_adv.param_quant_analysis.experiment_distance_to_sample
  
      def set_quant_calibration_distance_to_sample(self, distance_to_sample):
          self.img_model_adv.param_quant_analysis.set_experiment_distance_to_sample(distance_to_sample)
  
      # ==========================================================================
      #    The following methods are used by Fitting Model tab (pectrum plots)
  
      def get_line_plot_state(self):
          plot_spectrum = bool(self.plot_model.plot_exp_opt)
          plot_fit = bool(self.plot_model.show_fit_opt)
          return plot_spectrum, plot_fit
  
      def show_plot_spectrum(self, state):
          self.plot_model.plot_exp_opt = bool(state)
  
      def show_plot_fit(self, state):
          self.plot_model.show_fit_opt = bool(state)
  
      def get_plot_fit_energy_range(self):
          return self.plot_model.energy_range_fitting
  
      def set_plot_fit_energy_range(self, range_name):
          if range_name not in self.plot_model.energy_range_names:
              raise ValueError(
                  f"Range name {range_name} is not in the list of allowed "
                  f"names {self.plot_model.energy_range_name}"
              )
          self.plot_model.set_energy_range_fitting(range_name)
  
      def get_plot_fit_linlog(self):
          if self.plot_model.scale_opt == 0:
              return "linlog"
          else:
              return "linear"
  
      def set_plot_fit_linlog(self, scale):
          """Scale may have values 'linlog' or 'linear'.
          Raises `ValueError` if other value is passed."""
          self.plot_model.scale_opt = ["linlog", "linear"].index(scale)
  
      def update_plot_fit(self):
          self.plot_model.plot_experiment()
  
      def get_incident_energy(self):
          return self.plot_model.incident_energy
  
      def get_escape_peak_params(self):
          plot_escape_peak = bool(self.plot_model.plot_escape_line)
          materials = ["Si", "Ge"]
          try:
              detector_material = materials[self.plot_model.det_materials]
          except Exception:
              detector_material = ""
          return plot_escape_peak, detector_material
  
      def set_escape_peak_params(self, plot_escape_peak, detector_material):
          materials = ["Si", "Ge"]
          if detector_material not in materials:
              raise ValueError(f"Detector material '{detector_material}' is unknown: {materials}")
          self.plot_model.change_escape_peak_settings(
              1 if plot_escape_peak else 0, materials.index(detector_material)
          )
  
      # ==========================================================================
      #          The following methods are used by Model tab
      def get_autofind_elements_params(self):
          """Assemble the parameters managed by 'Find Elements in Sample` dialog box."""
          dialog_params = {}
          keys1 = ["e_offset", "e_linear", "e_quadratic", "fwhm_offset", "fwhm_fanoprime", "coherent_sct_energy"]
          for k in keys1:
              dialog_params[k] = {}
              dialog_params[k]["value"] = self.param_model.param_new[k]["value"]
              dialog_params[k]["default"] = self.param_model.param_new[k]["default"]
          keys2 = ["energy_bound_high", "energy_bound_low"]
          for k in keys2:
              dialog_params[k] = {}
              dialog_params[k]["value"] = self.param_model.param_new["non_fitting_values"][k]["value"]
              dialog_params[k]["default"] = self.param_model.param_new["non_fitting_values"][k]["default_value"]
  
          # Some values were taken from different places
          dialog_params["coherent_sct_energy"]["value"] = self.plot_model.incident_energy
  
          return dialog_params
  
      def set_autofind_elements_params(self, dialog_params, *, update_model=True, update_fitting_params=True):
          """Save the parameters changed in 'Find Elements in Sample` dialog box.
  
          Parameters
          ----------
          dialog_params : dict
              parameters from dialog box
          update_model : boolean
              True - update the spectra based on new parameters. Updating the spectra
              may take long time if model contains a lot of parameters, therefore it
              should not be used for the default model which contains all supported emission
              lines.
          update_fitting_params : bool
              True - execute callback functions after updating fitting parameters
  
          Returns
          -------
          boolean
              True - selected range or incident energy were changed, False otherwise
          """
          logger.debug("Saving parameters from 'DialogFindElements'")
  
          # Check if the critical parameters were changed
          if (
              (self.plot_model.incident_energy != dialog_params["coherent_sct_energy"]["value"])
              or (self.param_model.energy_bound_high_buf != dialog_params["energy_bound_high"]["value"])
              or (self.param_model.energy_bound_low_buf != dialog_params["energy_bound_low"]["value"])
              or (self.param_model.param_new["e_offset"]["value"] != dialog_params["e_offset"]["value"])
              or (self.param_model.param_new["e_linear"]["value"] != dialog_params["e_linear"]["value"])
              or (self.param_model.param_new["e_quadratic"]["value"] != dialog_params["e_quadratic"]["value"])
          ):
              return_value = True
          else:
              return_value = False
  
          keys = ["e_offset", "e_linear", "e_quadratic", "fwhm_offset", "fwhm_fanoprime"]
          for k in keys:
              self.param_model.param_new[k]["value"] = dialog_params[k]["value"]
  
          self.io_model.incident_energy_set = dialog_params["coherent_sct_energy"]["value"]
          self.param_model.energy_bound_high_buf = dialog_params["energy_bound_high"]["value"]
          self.param_model.energy_bound_low_buf = dialog_params["energy_bound_low"]["value"]
  
          # Also change incident energy
          self.param_model.param_new["coherent_sct_energy"]["value"] = dialog_params["coherent_sct_energy"]["value"]
          self.param_model.param_new["non_fitting_values"]["energy_bound_low"]["value"] = dialog_params[
              "energy_bound_low"
          ]["value"]
          self.param_model.param_new["non_fitting_values"]["energy_bound_high"]["value"] = dialog_params[
              "energy_bound_high"
          ]["value"]
  
          if update_model and return_value:
              self.param_model.create_spectrum_from_param_dict(reset=False)
  
          if update_fitting_params:
              self.fitting_parameters_changed()
  
          return return_value
  
      def get_general_fitting_params(self):
          params = dict()
  
          # Another option is self.param_model.param_new
          source_dict = self.param_model.param_new
  
          params["max_iterations"] = self.fit_model.fit_num
          params["tolerance"] = self.fit_model.ftol
          params["escape_peak_ratio"] = source_dict["non_fitting_values"]["escape_ratio"]
  
          params["incident_energy"] = source_dict["coherent_sct_energy"]["value"]
          params["range_low"] = source_dict["non_fitting_values"]["energy_bound_low"]["value"]
          params["range_high"] = source_dict["non_fitting_values"]["energy_bound_high"]["value"]
  
          params["subtract_baseline_linear"] = self.fit_model.linear_bg
          params["subtract_baseline_snip"] = self.fit_model.use_snip
          params["add_bias"] = self.fit_model.raise_bg
  
          params["snip_window_size"] = source_dict["non_fitting_values"]["background_width"]
  
          return params
  
      def set_general_fitting_params(self, params):
  
          # Another option is self.param_model.param_new
          dest_dict = self.param_model.param_new
  
          self.fit_model.fit_num = params["max_iterations"]
          self.fit_model.ftol = params["tolerance"]
          dest_dict["non_fitting_values"]["escape_ratio"] = params["escape_peak_ratio"]
  
          dest_dict["coherent_sct_energy"]["value"] = params["incident_energy"]
          dest_dict["non_fitting_values"]["energy_bound_low"]["value"] = params["range_low"]
          dest_dict["non_fitting_values"]["energy_bound_high"]["value"] = params["range_high"]
          self.io_model.incident_energy_set = params["incident_energy"]
  
          self.fit_model.linear_bg = params["subtract_baseline_linear"]
          self.fit_model.use_snip = params["subtract_baseline_snip"]
          self.fit_model.raise_bg = params["add_bias"]
  
          dest_dict["non_fitting_values"]["background_width"] = params["snip_window_size"]
  
          self.param_model.create_spectrum_from_param_dict(reset=False)
          self.apply_to_fit()
  
          self.fitting_parameters_changed()
  
      def get_fit_strategy_list(self):
          return fit_strategy_list.copy()
  
      def get_detailed_fitting_params_lines(self):
          # Dictionary of fitting parameters. Create a copy !!!
          param_dict = copy.deepcopy(self.param_model.param_new)
          # Ordered list of emission lines
          eline_list = self.param_model.get_sorted_element_list()
          # Dictionary: emission line -> [list of keys in 'params' dictionary]
          eline_key_dict = dict()
          eline_energy_dict = dict()
          for eline in eline_list:
              key = None
              eline_category = self.get_eline_name_category(eline)
              if eline_category == "eline":
                  key = eline[:-1] + eline[-1].lower()
              elif eline_category == "pileup":
                  key = f"pileup_{eline.replace('-', '_')}"
              elif eline_category == "userpeak":
                  key = eline
  
              if key:
                  e_list = [_ for _ in param_dict.keys() if _.startswith(key)]
              else:
                  e_list = []
  
              energy_list = []
              if eline_category == "eline":
                  for key in e_list:
                      el = "_".join(key.split("_")[:2])
                      energy_list.append(get_eline_energy(el))
              elif eline_category == "pileup":
                  energy = self.param_model.get_pileup_peak_energy(eline)
                  energy_list = [energy] * len(e_list)
              elif eline_category == "userpeak":
                  key = eline + "_delta_center"
                  energy = param_dict[key]["value"] + 5.0
                  energy_list = [energy] * len(e_list)
  
              eline_key_dict[eline] = e_list
              eline_energy_dict[eline] = energy_list
  
          # List of other parameters in 'params' dictionary (except non_fitting_values).
          #   Currently all the parameters not related to emission lines are strictly lower-case.
          other_param_list = [_ for _ in param_dict.keys() if (_ == _.lower()) and (_ != "non_fitting_values")]
          other_param_list.sort()
          # eline_list = ["Shared parameters"] + eline_list
  
          _fit_strategy_list = fit_strategy_list.copy()
          _bound_options = bound_options.copy()
  
          return {
              "param_dict": param_dict,
              "eline_list": eline_list,
              "eline_key_dict": eline_key_dict,
              "eline_energy_dict": eline_energy_dict,
              "other_param_list": other_param_list,
              "fit_strategy_list": _fit_strategy_list,
              "bound_options": _bound_options,
          }
  
      def get_detailed_fitting_params_shared(self):
          # Dictionary of fitting parameters. Create a copy !!!
          param_dict = copy.deepcopy(self.param_model.param_new)
          # Ordered list of emission lines
          eline_list = ["Shared parameters"]
          # Dictionary: emission line -> [list of keys in 'params' dictionary]
          eline_key_dict = dict()
          eline_energy_dict = dict()
  
          # List of other parameters in 'params' dictionary (except non_fitting_values).
          #   Currently all the parameters not related to emission lines are strictly lower-case.
          other_param_list = [_ for _ in param_dict.keys() if (_ == _.lower()) and (_ != "non_fitting_values")]
          other_param_list.sort()
  
          _fit_strategy_list = fit_strategy_list.copy()
          _bound_options = bound_options.copy()
  
          return {
              "param_dict": param_dict,
              "eline_list": eline_list,
              "eline_key_dict": eline_key_dict,
              "eline_energy_dict": eline_energy_dict,
              "other_param_list": other_param_list,
              "fit_strategy_list": _fit_strategy_list,
              "bound_options": _bound_options,
          }
  
      def set_detailed_fitting_params(self, dialog_data):
          param_dict = dialog_data["param_dict"]
          # 'param_dict' is expected to have identical structure as 'param_model.param_new'.
          # We don't want to change the reference to the parameters, so we copy references to
          #   dictionary elements (which are also dictionaries).
  
          # Check if the energy axis parameters were changed
          e_axis_changed = False
          e_axis_keys = ("e_offset", "e_linear", "e_quadratic")
          for key in e_axis_keys:
              if self.param_model.param_new[key]["value"] != param_dict[key]["value"]:
                  e_axis_changed = True
                  break
  
          for key, val in param_dict.items():
              self.param_model.param_new[key] = val
  
          self.io_model.incident_energy_set = param_dict["coherent_sct_energy"]["value"]
  
          self.param_model.create_spectrum_from_param_dict(reset=False)
          self.apply_to_fit()
  
          # This is needed in case the coefficients that define energy axis have changed
          if e_axis_changed:
              self.update_preview_spectrum_plot()
  
          self.fitting_parameters_changed()
  
      def get_quant_standard_list(self):
          self.fit_model.param_quant_estimation.clear_standards()
          self.fit_model.param_quant_estimation.load_standards()
  
          qe_param_built_in = self.fit_model.param_quant_estimation.standards_built_in
          qe_param_custom = self.fit_model.param_quant_estimation.standards_custom
  
          # The current selection is set to the currently selected standard
          #   held in 'fit_model.qe_standard_selected_copy'
          qe_standard_selected = self.fit_model.qe_standard_selected_copy
          qe_standard_selected = self.fit_model.param_quant_estimation.set_selected_standard(qe_standard_selected)
  
          return qe_param_built_in, qe_param_custom, qe_standard_selected
  
      def set_selected_quant_standard(self, selected_standard):
          if selected_standard is not None:
              self.fit_model.param_quant_estimation.set_selected_standard(selected_standard)
              self.fit_model.qe_standard_selected_copy = copy.deepcopy(selected_standard)
          else:
              self.fit_model.param_quant_estimation.clear_standards()
              self.fit_model.qe_standard_selected_copy = None
  
          self.fitting_parameters_changed()
  
      def is_quant_standard_custom(self, standard=None):
          return self.fit_model.param_quant_estimation.is_standard_custom(standard)
  
      def process_peaks_from_quantitative_sample_data(self):
          incident_energy = self.param_model.param_new["coherent_sct_energy"]["value"]
          # Generate the data structure for the results of processing of standard data
          self.fit_model.param_quant_estimation.gen_fluorescence_data_dict(incident_energy)
          # Obtain the list (dictionary) of elemental lines from the generated structure
          elemental_lines = self.fit_model.param_quant_estimation.fluorescence_data_dict["element_lines"]
  
          self.param_model.find_peak(elemental_lines=elemental_lines.keys())
          self.param_model.EC.order()
          self.param_model.update_name_list()
  
          self.param_model.EC.turn_on_all()
          self.param_model.data_for_plot()
  
          # update parameter for fit
          self.param_model.create_full_param()  # Not sure it is necessary
          self.fit_model.apply_default_param()
  
          # update experimental plots in case the coefficients change
          self.plot_model.plot_experiment()
  
          self.plot_model.plot_fit(
              self.param_model.prefit_x, self.param_model.total_y, self.param_model.auto_fit_all
          )
  
          # Update displayed intensity of the selected peak
          self.plot_model.compute_manual_peak_intensity()
  
          # Show the summed spectrum used for fitting
          self.plot_model.plot_exp_opt = False
          self.plot_model.plot_exp_opt = True
          # For plotting purposes, otherwise plot will not update
          self.plot_model.show_fit_opt = False
          self.plot_model.show_fit_opt = True
  
          self.fitting_parameters_changed()
  
      def load_parameters_from_file(self, parameter_file_path, incident_energy_from_param_file=None):
  
          try:
              self.param_model.read_param_from_file(parameter_file_path)
          except Exception as ex:
              msg = f"Error occurred while reading parameter file: {ex}"
              logger.error(msg)
              raise IOError(msg)
          else:
              # Make a decision, if the incident energy from metadata should be replaced
              #   with the incident energy from the parameter file
              overwrite_metadata_incident_energy = False
  
              # Incident energy from the parameter file
              param_incident_energy = self.param_model.param_new["coherent_sct_energy"]["value"]
  
              if self.io_model.incident_energy_available:
  
                  # Incident energy from datafile metadata
                  mdata_incident_energy = self.io_model.scan_metadata.get_mono_incident_energy()
  
                  # If two energies have very close values (say 1 eV), then the difference doesn't matter
                  #   Consider two energies equal (they probably ARE equal) and use the value
                  #   from datafile metadata.
                  if not math.isclose(param_incident_energy, mdata_incident_energy, abs_tol=0.001):
                      # TODO: the following text is not properly formatted for QMessageBox
                      #       It's not very important now, but should be resolved in the future.
                      msg = (
                          f"The values of incident energy from data file metadata "
                          f"and parameter file are different.\n"
                          f"Incident energy from metadata: {mdata_incident_energy} keV.\n"
                          f"Incident energy from the loaded parameter file: {param_incident_energy} keV.\n"
                          f"Would you prefer to use the incident energy from the parameter file for processing?"
                      )
  
                      if incident_energy_from_param_file is None:
                          return False, msg
                      else:
                          overwrite_metadata_incident_energy = bool(incident_energy_from_param_file)
  
              else:
  
                  # If incident energy is not present in file metadata, then
                  #   just load the incident energy from the parameter file.
                  overwrite_metadata_incident_energy = True
  
              if overwrite_metadata_incident_energy:
                  logger.info(f"Using incident energy from the parameter file: {param_incident_energy} keV")
              else:
                  # Keep the incident energy from the file
                  logger.info(f"Using incident energy from the datafile metadata: {mdata_incident_energy} keV")
                  incident_energy = round(mdata_incident_energy, 6)
                  self.param_model.param_new["coherent_sct_energy"]["value"] = incident_energy
                  self.param_model.param_new["non_fitting_values"]["energy_bound_high"]["value"] = (
                      incident_energy + 0.8
                  )
  
              self.fit_model.apply_default_param()
  
              # update experimental plots
              self.plot_model.plot_experiment()
              self.plot_model.plot_exp_opt = False
              self.plot_model.plot_exp_opt = True
  
              # calculate profile and plot
              self.fit_model.get_profile()
  
              # update experimental plot with new calibration values
              self.plot_model.plot_fit(
                  self.fit_model.cal_x, self.fit_model.cal_y, self.fit_model.cal_spectrum, self.fit_model.residual
              )
  
              self.plot_model.plot_exp_opt = False
              self.plot_model.plot_exp_opt = True
              # For plotting purposes, otherwise plot will not update
              self.plot_model.show_fit_opt = False
              self.plot_model.show_fit_opt = True
  
              # The following statement is necessary mostly to set the correct value of
              #   the upper boundary of the energy range used for emission line search.
              self.plot_model.change_incident_energy(self.param_model.param_new["coherent_sct_energy"]["value"])
  
              # update parameter for fit
              # self.param_model.create_full_param()
              self.fit_model.apply_default_param()
  
              # Update displayed intensity of the selected peak
              self.plot_model.compute_manual_peak_intensity()
  
              self.fitting_parameters_changed()
  
              return True, ""
  
      def find_elements_automatically(self):
          logger.debug("Starting automated element search")
          self.param_model.find_peak()
  
          self.param_model.EC.order()
          self.param_model.update_name_list()
  
          self.param_model.EC.turn_on_all()
  
          self.plot_model.plot_exp_opt = True
          self.apply_to_fit()
  
          self.fitting_parameters_changed()
  
      def get_full_eline_list(self):
          """Returns full list of supported emission lines."""
          return self.param_model.get_user_peak_list()
  
      def set_marker_reporter(self, callback):
          self.plot_model.report_marker_state = callback
  
      def select_eline(self, eline):
          self.param_model.e_name = eline
          n_id = 0  # No element is selected
          if eline != "":
              eline_list = self.get_full_eline_list()
              try:
                  n_id = eline_list.index(eline) + 1  # Elements are numbered starting with 1.
              except ValueError:
                  pass
          self.plot_model.element_id = n_id
  
      def set_selected_eline(self, eline):
          """Sets and display the location of current emission line"""
          self.select_eline(eline)
  
          eline_category = self.get_eline_name_category(eline)
          if eline_category == "userpeak":
              self.fit_model.select_index_by_eline_name(eline)
              # This will delete the peak lines (no peak lines are plotted for user peak)
              self.plot_model.plot_current_eline(eline)
              # Show the marker at the center of the userpeak
              energy, _ = self.param_model.get_selected_eline_energy_fwhm(eline)
              self.plot_model.set_plot_vertical_marker(energy)
          elif eline_category == "pileup":
              # The lines for pileup peak are not displayed as 'plot_model.element_id' is changed.
              #   So call plotting function explicitly
              self.plot_model.plot_current_eline(eline)
              self.plot_model.hide_plot_vertical_marker()
          elif eline_category == "eline":
              self.plot_model.hide_plot_vertical_marker()
          else:
              # This will delete the peak lines
              self.plot_model.plot_current_eline(eline)
              self.plot_model.hide_plot_vertical_marker()
  
      def get_eline_intensity(self, eline):
          line_category = self.get_eline_name_category(eline)
          if line_category == "eline":
              return self.param_model.add_element_intensity
          elif line_category in ("userpeak", "pileup"):
              return self.param_model.EC.element_dict[eline].maxv
          else:
              return None
  
      def get_selected_eline_table(self):
          """Returns full list of supported emission lines."""
          eline_names = self.param_model.get_sorted_result_dict_names()
          eline_table = []
          for eline in eline_names:
              sel_status = self.param_model.EC.element_dict[eline].status
              z = self.param_model.EC.element_dict[eline].z
              energy = self.param_model.EC.element_dict[eline].energy
              peak_int = self.param_model.EC.element_dict[eline].maxv
              rel_int = self.param_model.EC.element_dict[eline].norm
              cs = get_cs(eline, self.param_model.param_new["coherent_sct_energy"]["value"])
              row_data = {
                  "eline": eline,
                  "sel_status": sel_status,
                  "z": z,
                  "energy": energy,
                  "peak_int": peak_int,
                  "rel_int": rel_int,
                  "cs": cs,
              }
              eline_table.append(row_data)
          return eline_table
  
      def get_unused_userpeak_name(self):
          eline_names = self.param_model.get_sorted_result_dict_names()
          for n in range(1, 10000):  # Set some arbitrarily high limit to avoid infinite loops
              userpeak_name = f"Userpeak{n}"
              if userpeak_name not in eline_names:
                  return userpeak_name
  
      def get_eline_name_category(self, eline_name):
          return self.param_model.get_eline_name_category(eline_name)
  
      def set_checked_emission_lines(self, eline_list, eline_checked_list):
          for eline, checked in zip(eline_list, eline_checked_list):
              self.param_model.EC.element_dict[eline].status = checked
              self.param_model.EC.element_dict[eline].stat_copy = self.param_model.EC.element_dict[eline].status
  
          self.param_model.data_for_plot()
  
          self.plot_model.plot_fit(
              self.param_model.prefit_x, self.param_model.total_y, self.param_model.auto_fit_all
          )
          # For plotting purposes, otherwise plot will not update
          self.plot_model.show_fit_opt = False
          self.plot_model.show_fit_opt = True
  
      def get_current_userpeak_energy_fwhm(self):
          return self.param_model.get_selected_eline_energy_fwhm(self.param_model.e_name)
  
      def generate_pileup_peak_name(self, eline1, eline2):
          return self.param_model.generate_pileup_peak_name(eline1, eline2)
  
      def get_pileup_peak_energy(self, eline):
          return self.param_model.get_pileup_peak_energy(eline)
  
      def is_peak_already_selected(self, eline):
          """Verifies if emission line with the same name is already selected"""
          line_list = list(self.param_model.EC.element_dict.keys())
          if eline in line_list:
              return True
          else:
              return False
  
      def convert_full_eline_name(self, eline, to_upper=True):
          """Convert emission lines to the form used for displayed emission line names"""
          try:
              index = eline.index("_")
              eline_as_list = list(eline)
              if to_upper:
                  eline_as_list[index + 1] = eline_as_list[index + 1].upper()
              else:
                  eline_as_list[index + 1] = eline_as_list[index + 1].lower()
              eline = "".join(eline_as_list)
          except Exception:
              pass
          return eline
  
      def get_guessed_pileup_peak_components(self, energy, tolerance=0.05):
          """
          Returns tuple (line1, line2, pileup_energy), that contains the best guess
          for the pileup peak placed `energy`.
  
          Parameters
          ----------
          energy: float
              Approximate (selected) energy of pileup peak location
          tolerance: float
              Allowed deviation of the sum of component energies from the selected energy, keV
  
          Returns
          -------
          tuple(str, str, float)
              Component emission lines (such as Ca_ka1, K_ka1 etc) and the energy of
              the resulting pileup peak.
          """
          eline_info = self.param_model.guess_pileup_peak_components(energy=energy, tolerance=tolerance)
          if eline_info is not None:
              e1, e2, energy = eline_info
              eline_info = (
                  self.convert_full_eline_name(e1, to_upper=True),
                  self.convert_full_eline_name(e2, to_upper=True),
                  energy,
              )
          return eline_info
  
      def get_selected_energy_range(self):
          """
          Returns the energy range selected for processing. No peaks are supposed to be selected
          outside this range.
  
          Returns
          -------
          tuple(float, float)
              Selected range (low, high) in keV
          """
          e_low = self.param_model.param_new["non_fitting_values"]["energy_bound_low"]["value"]
          e_high = self.param_model.param_new["non_fitting_values"]["energy_bound_high"]["value"]
          return e_low, e_high
  
      def add_peak_manual(self, eline):
          """Manually add a peak (emission line) using 'Add' button"""
          self.select_eline(eline)
  
          # Verify if we are adding a userpeak
          is_userpeak = self.get_eline_name_category(eline) == "userpeak"
  
          # The following set of conditions is not complete, but sufficient
          if self.param_model.x0 is None or self.param_model.y0 is None:
              err_msg = "Experimental data is not loaded or initial\nspectrum fitting is not performed"
              raise RuntimeError(err_msg)
  
          elif is_userpeak and (not self.plot_model.vertical_marker_is_visible):
              err_msg = (
                  "Select position of userpeak by clicking on the spectrum plot.\n"
                  "Note: plot toolbar options, such as Pan and Zoom, \n"
                  "must be turned off before selecting the position."
              )
              raise RuntimeError(err_msg)
  
          else:
              try:
                  # Energy is used for user-defined peaks only
                  energy, _ = self.get_suggested_manual_peak_energy()
  
                  self.param_model.add_peak_manual(userpeak_center=energy)
                  self.apply_to_fit()
                  self.fit_model.select_index_by_eline_name(eline)
              except Exception as ex:
                  raise RuntimeError(str(ex))
  
          self.fitting_parameters_changed()
  
      def remove_peak_manual(self, eline):
          """Manually add a peak (emission line) using 'Remove' button"""
          try:
              self.select_eline(eline)
              self.param_model.remove_peak_manual()
              self.apply_to_fit()
              self.fitting_parameters_changed()
          except Exception as ex:
              raise RuntimeError(str(ex))
  
      def update_userpeak(self, eline, energy, maxv, fwhm):
          """
          Update parameters of the user defined peak. Exception may be raised in
          case of an error. Note, that if energy is changed, FWHM will change to
          adjust for the variation of peak width.
  
          Parameters
          ----------
          eline: str
              name of the userpeak (e.g. 'Userpeak2')
          energy: float
              new position of the peak center, keV
          maxv: float
              new value of the amplitude
          fwhm: float
              new value of FWHM of the peak.
          """
          self.select_eline(eline)
          self.param_model.modify_userpeak_params(maxv, fwhm, energy)
          self.apply_to_fit()
          self.fitting_parameters_changed()
  
      def update_eline_peak_height(self, eline, maxv):
          self.select_eline(eline)
          self.param_model.modify_peak_height(maxv)
          self.apply_to_fit()
          self.fitting_parameters_changed()
  
      def get_suggested_manual_peak_energy(self):
          """
          Returns the suggested (pointed by the vertical marker) energy of the center peak in keV
          and the status of the marker.
  
          Returns
          -------
          float
              Energy of the manual peak center in keV. The energy is determined
              by vertical marker on the screen.
          bool
              True if the vertical marker is visible, otherwise False.
  
          """
          return self.plot_model.get_suggested_new_manual_peak_energy()
  
      def show_marker_at_current_position(self):
          """Force marker to visible state at the current position"""
          energy, marker_visible = self.get_suggested_manual_peak_energy()
          self.plot_model.set_plot_vertical_marker(energy)
  
      def get_peak_threshold(self):
          return self.param_model.bound_val
  
      def remove_peaks_below_threshold(self, peak_threshold):
          self.param_model.bound_val = peak_threshold
          self.param_model.remove_elements_below_threshold(threshv=peak_threshold)
          self.param_model.update_name_list()
          self.apply_to_fit()
          self.fitting_parameters_changed()
  
      def remove_unchecked_peaks(self):
          self.param_model.remove_elements_unselected()
          self.param_model.update_name_list()
          self.apply_to_fit()
          self.fitting_parameters_changed()
  
      def apply_to_fit(self):
          """
          Update plot, and apply updated parameters to fitting process.
          Note: this is an original function from 'fit.enaml' file.
          """
          self.param_model.EC.update_peak_ratio()
          self.param_model.data_for_plot()
  
          # update experimental plots in case the coefficients change
          self.plot_model.plot_experiment()
  
          self.plot_model.plot_fit(
              self.param_model.prefit_x, self.param_model.total_y, self.param_model.auto_fit_all
          )
  
          # For plotting purposes, otherwise plot will not update
          if self.plot_model.plot_exp_opt:
              self.plot_model.plot_exp_opt = False
              self.plot_model.plot_exp_opt = True
          else:
              self.plot_model.plot_exp_opt = True
              self.plot_model.plot_exp_opt = False
          self.plot_model.show_fit_opt = False
          self.plot_model.show_fit_opt = True
  
          # update parameter for fit
          self.param_model.create_full_param()  # Not sure this is needed
          self.fit_model.apply_default_param()
  
          # Update displayed intensity of the selected peak
          self.plot_model.compute_manual_peak_intensity()
  
      def total_spectrum_fitting(self):
  
          # Update parameter for fit. This may be unnecessary
          self.apply_to_fit()
  
          self.fit_model.fit_multiple()
  
          # BUG: line color for pileup is not correct from fit
          self.fit_model.get_profile()
  
          # update experimental plot with new calibration values
          self.plot_model.plot_experiment()
  
          self.plot_model.plot_fit(
              self.fit_model.cal_x, self.fit_model.cal_y, self.fit_model.cal_spectrum, self.fit_model.residual
          )
  
          # For plotting purposes, otherwise plot will not update
          self.plot_model.plot_exp_opt = False
          self.plot_model.plot_exp_opt = True
          self.plot_model.show_fit_opt = False
          self.plot_model.show_fit_opt = True
  
          # update autofit param
          self.param_model.update_new_param(self.param_model.param_new)
          # param_model.get_new_param_from_file(parameter_file_path)
          self.param_model.update_name_list()
          self.param_model.EC.turn_on_all()
  
          # Update displayed intensity of the selected peak
          self.plot_model.compute_manual_peak_intensity()
  
          self.fitting_parameters_changed()
  
      def save_param_to_file(self, path):
          save_as(path, self.param_model.param_new)
  
      def save_spectrum(self, dir, save_fit):
          self.fit_model.result_folder = dir
          self.fit_model.output_summed_data_fit(save_fit=save_fit)
  
      def compute_current_rfactor(self, save_fit):
          return self.fit_model.compute_current_rfactor(save_fit)
  
      def get_iter_and_var_number(self):
          return {"var_number": self.fit_model.nvar, "iter_number": self.fit_model.function_num}
  
      # ==========================================================================
      #          The following methods are used by Maps tab
      def fit_individual_pixels(self):
          self.apply_to_fit()
  
          self.fit_model.fit_single_pixel()
  
          # add scalers to fit dict
          scaler_keys = [v for v in self.io_model.img_dict.keys() if "scaler" in v]
          if len(scaler_keys) > 0:
              self.fit_model.fit_img[list(self.fit_model.fit_img.keys())[0]].update(
                  self.io_model.img_dict[scaler_keys[0]]
              )
  
          self.io_model.update_img_dict(self.fit_model.fit_img)
  
      # ==========================================================================
      #          The following methods are used by ROI window
      def get_roi_selected_element_list(self):
          element_list = self.roi_model.element_for_roi
          # TODO: should probably be sorted differently
          return element_list
  
      def set_roi_selected_element_list(self, elements_for_roi):
          self.roi_model.element_for_roi = elements_for_roi
          self.plot_model.plot_roi_bound()
  
      def clear_roi_element_list(self):
          for r in self.roi_model.element_list_roi:
              self.plot_model.roi_dict[r].show_plot = False
          self.plot_model.plot_roi_bound()
          self.roi_model.clear_selected_elements()
  
      def load_roi_element_list_from_selected(self):
          for r in self.roi_model.element_list_roi:
              self.plot_model.roi_dict[r].show_plot = False
          self.plot_model.plot_roi_bound()
          selected_element_list = self.param_model.EC.get_element_list()
          selected_element_list = [
              _ for _ in selected_element_list if self.param_model.get_eline_name_category(_) == "eline"
          ]
          self.roi_model.select_elements_from_list(selected_element_list)
  
      def get_roi_subtract_background(self):
          return self.roi_model.subtract_background
  
      def set_roi_subtract_background(self, subtract_background):
          self.roi_model.subtract_background = bool(subtract_background)
  
      def compute_rois(self):
          if len(self.roi_model.element_for_roi) == 0:
              raise RuntimeError("No elements are selected for ROI computation. Select at least one element.")
          roi_result = self.roi_model.get_roi_sum()
  
          self.io_model.update_img_dict(roi_result)
  
      def show_roi(self, eline, show_status):
          self.plot_model.roi_dict = self.roi_model.roi_dict
          if show_status:
              # self.plot_model.plot_exp_opt = True
              self.plot_model.roi_dict[eline].show_plot = True
              self.plot_model.plot_roi_bound()
          else:
              self.plot_model.roi_dict[eline].show_plot = False
              self.plot_model.plot_roi_bound()
  
      def change_roi(self, eline, low, high):
          # Convert keV to eV (current implementation of ROIModel is using eV.
          self.roi_model.roi_dict[eline].left_val = int(low * 1000)
          self.roi_model.roi_dict[eline].right_val = int(high * 1000)
          self.plot_model.plot_roi_bound()
  
      def get_roi_settings(self):
          roi_settings = []
  
          eline_list = list(self.roi_model.element_list_roi)
  
          for eline in eline_list:
              # We display values in keV, but current implementation of ROIModel
              #   is using eV.
              energy_center = self.roi_model.roi_dict[eline].line_val / 1000.0
              energy_left = self.roi_model.roi_dict[eline].left_val / 1000.0
              energy_right = self.roi_model.roi_dict[eline].right_val / 1000.0
              range_displayed = self.roi_model.roi_dict[eline].show_plot
  
              energy_left_default = self.roi_model.roi_dict[eline].default_left / 1000.0
              energy_right_default = self.roi_model.roi_dict[eline].default_right / 1000.0
  
              roi_settings.append(
                  {
                      "eline": eline,
                      "energy_center": energy_center,
                      "energy_left": energy_left,
                      "energy_right": energy_right,
                      "range_displayed": range_displayed,
                      "energy_left_default": energy_left_default,
                      "energy_right_default": energy_right_default,
                  }
              )
  
          roi_settings.sort(key=lambda _: _["energy_center"])
          return roi_settings
  
      # ==========================================================================
      #          The following methods are used by ROI window
      def is_quant_standard_selected(self):
          """
          Returns True if quantitative standard was selected from the list.
          The result can be used to determine if saving quantitative standard is a valid
          operation.
          """
          return bool(self.fit_model.param_quant_estimation.fluorescence_data_dict)
  
      def is_quant_standard_fitting_available(self):
          """
          Returns True if fitted map is available.
          """
          result_map, selected_det_channel, scaler_name = self.fit_model.get_selected_fitted_map_data()
          return bool(result_map) and bool(selected_det_channel)
  
      def get_suggested_quant_file_name(self):
          """
          Return suggested 'standard' file name used for saving quantitative calibration
          to a JSON file
          """
          return self.fit_model.param_quant_estimation.get_suggested_json_fln()
  
      def get_displayed_quant_calib_parameters(self):
  
          fluorescence_data_dict = self.fit_model.param_quant_estimation.fluorescence_data_dict
          if fluorescence_data_dict is None:
              raise RuntimeError("Attempt to obtain quantitative standard parameters while no standard is selected")
  
          result_map, selected_det_channel, scaler_name = self.fit_model.get_selected_fitted_map_data()
  
          if not result_map:
              raise RuntimeError("No XRF maps were found. Run Individual Pixel Fitting to generate XRF maps.")
  
          if not selected_det_channel:
              raise RuntimeError("Selected XRF map does not contain fitted XRF maps.")
  
          suggested_fln = self.fit_model.param_quant_estimation.get_suggested_json_fln()
  
          # Fill the available fluorescence data
          self.fit_model.param_quant_estimation.fill_fluorescence_data_dict(
              xrf_map_dict=result_map, scaler_name=scaler_name
          )
          self.fit_model.param_quant_estimation.set_detector_channel_in_data_dict(
              detector_channel=selected_det_channel
          )
  
          distance_to_sample = self.fit_model.qe_standard_distance_to_sample
          self.fit_model.param_quant_estimation.set_distance_to_sample_in_data_dict(
              distance_to_sample=distance_to_sample
          )
  
          # Set optional parameters
          if self.io_model.scan_metadata_available:
              _scan_id = None
              _scan_uid = None
              if "scan_id" in self.io_model.scan_metadata:
                  _scan_id = self.io_model.scan_metadata["scan_id"]
              if "scan_uid" in self.io_model.scan_metadata:
                  _scan_uid = self.io_model.scan_metadata["scan_uid"]
              self.fit_model.param_quant_estimation.set_optional_parameters(scan_id=_scan_id, scan_uid=_scan_uid)
  
          preview = self.fit_model.param_quant_estimation.get_fluorescence_data_dict_text_preview()
  
          return {"suggested_file_name": suggested_fln, "distance_to_sample": distance_to_sample, "preview": preview}
  
      def save_distance_to_sample(self, distance_to_sample):
          self.fit_model.qe_standard_distance_to_sample = distance_to_sample
          self.fit_model.param_quant_estimation.set_distance_to_sample_in_data_dict(
              distance_to_sample=distance_to_sample
          )
  
      def save_quantitative_standard(self, file_path, overwrite_existing):
          """Will raise exception if unsuccessful"""
          self.fit_model.param_quant_estimation.save_fluorescence_data_dict(
              file_path, overwrite_existing=overwrite_existing
          )
  
      # ==========================================================================
      #     The following methods are used for exporting maps as TIFF and TXT
      def get_parameters_for_exporting_maps(self):
          dataset_list, dset_sel = self.get_maps_dataset_list()
          scalers, scaler_sel = self.get_maps_scaler_list()
          interpolate_on = self.get_maps_grid_interpolate()
          quant_norm_on = self.get_maps_quant_norm_enabled()
  
          return {
              "dset_list": dataset_list,
              "dset_sel": dset_sel,  # We want it to start from 0
              "scaler_list": scalers,
              "scaler_sel": scaler_sel,
              "interpolate_on": interpolate_on,
              "quant_norm_on": quant_norm_on,
          }
  
      def export_xrf_maps(
          self, *, results_path, dataset_name, scaler_name, interpolate_on, quant_norm_on, file_formats
      ):
          # We don't want to make any changes to 'param_quant_analysis' at the original location
          #   This is a temporary copy.
          param_quant_analysis = copy.deepcopy(self.img_model_adv.param_quant_analysis)
          param_quant_analysis.experiment_detector_channel = self.img_model_adv.get_detector_channel_name(
              dataset_name
          )
          for file_format in file_formats:
              self.fit_model.output_2Dimage(
                  results_path=results_path,
                  dataset_name=dataset_name,
                  scaler_name=scaler_name,
                  interpolate_on=interpolate_on,
                  quant_norm_on=quant_norm_on,
                  param_quant_analysis=param_quant_analysis,
                  file_format=file_format,
              )
  
      # ==========================================================================
      #     Selection of the region pixels for saved spectra in Maps tab
      def get_enable_save_spectra(self):
          return self.fit_model.save_point
  
      def set_enable_save_spectra(self, enabled):
          self.fit_model.save_point = enabled
  
      def get_dataset_map_size(self):
          return self.io_model.get_dataset_map_size()
  
      def get_selection_area_save_spectra(self):
          return {
              "row_min": self.fit_model.point1v + 1,
              "row_max": self.fit_model.point2v,
              "col_min": self.fit_model.point1h + 1,
              "col_max": self.fit_model.point2h,
          }
  
      def set_selection_area_save_spectra(self, area):
          map_size = self.get_dataset_map_size()
          map_size = (1, 1) if map_size is None else map_size
          n_rows, n_cols = map_size
  
          def _fit_to_range(val, val_min, val_max):
              if val < val_min:
                  val = val_min
              if val > val_max:
                  val = val_max
              return val
  
          # Modify the selection so that at least one row or column is selected
          #   (area is at least 1 pixel wide in every direction)
          row_min = _fit_to_range(area["row_min"] - 1, 0, n_rows - 1)
          row_max = _fit_to_range(area["row_max"], row_min + 1, n_rows)
          col_min = _fit_to_range(area["col_min"] - 1, 0, n_cols - 1)
          col_max = _fit_to_range(area["col_max"], col_min + 1, n_cols)
          self.fit_model.point1v = row_min
          self.fit_model.point2v = row_max
          self.fit_model.point1h = col_min
          self.fit_model.point2h = col_max
  