text: # coding: utf-8
  # /*##########################################################################
  #
  # Copyright (C) 2016-2018 European Synchrotron Radiation Facility
  #
  # Permission is hereby granted, free of charge, to any person obtaining a copy
  # of this software and associated documentation files (the "Software"), to deal
  # in the Software without restriction, including without limitation the rights
  # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  # copies of the Software, and to permit persons to whom the Software is
  # furnished to do so, subject to the following conditions:
  #
  # The above copyright notice and this permission notice shall be included in
  # all copies or substantial portions of the Software.
  #
  # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  # THE SOFTWARE.
  #
  # ###########################################################################*/
  
  __authors__ = ["V. Valls"]
  __license__ = "MIT"
  __date__ = "16/10/2020"
  
  import logging
  import functools
  import numpy
  
  from silx.gui import qt
  
  from ..model import MarkerModel
  from ..utils import unitutils
  from ..utils import eventutils
  from ..CalibrationContext import CalibrationContext
  from pyFAI.ext.invert_geometry import InvertGeometry
  import pyFAI.units
  
  _logger = logging.getLogger(__name__)
  
  
  class MarkerManager(object):
      """Synchronize the display of markers from MarkerModel to a plot."""
  
      _ITEM_TEMPLATE = "__markers__%s"
  
      def __init__(self, plot, markerModel, pixelBasedPlot=False):
          self.__plot = plot
          self.__markerModel = markerModel
          self.__markerModel.changed.connect(self.__updateMarkers)
          self.__geometry = None
          self.__markers = []
          self.__pixelBasedPlot = pixelBasedPlot
          self.__radialUnit = None
          self.__mustBeUpdated = False
  
          eventutils.createShowSignal(self.__plot)
          self.__plot.sigShown.connect(self.__plotIsShown)
  
      def updateProjection(self, geometry, radialUnit, wavelength, directDist, redraw=True):
          if self.__pixelBasedPlot:
              raise RuntimeError("Invalide operation for this kind of plot")
          self.__geometry = geometry
          self.__radialUnit = radialUnit
          self.__invertGeometry = InvertGeometry(
              self.__geometry.array_from_unit(typ="center", unit=self.__radialUnit, scale=True),
              numpy.rad2deg(self.__geometry.chiArray()))
          self.__wavelength = wavelength
          self.__directDist = directDist
          if redraw:
              self.__updateMarkers()
  
      def updatePhysicalMarkerPixels(self, geometry):
          self.__geometry = geometry
          if geometry is None:
              self.__wavelength = None
              self.__directDist = None
          else:
              self.__wavelength = geometry.wavelength
              try:
                  self.__directDist = geometry.getFit2D()["directDist"]
              except Exception:
                  # The geometry could not fit this param
                  _logger.debug("Backtrace", exc_info=True)
                  self.__directDist = None
  
          if geometry is not None:
              invertGeometry = InvertGeometry(
                  geometry.array_from_unit(typ="center", unit=pyFAI.units.TTH_RAD, scale=False),
                  geometry.chiArray())
  
          self.__markerModel.lockSignals()
          for marker in self.__markerModel:
              if not isinstance(marker, MarkerModel.PhysicalMarker):
                  continue
  
              chiRad, tthRad = marker.physicalPosition()
              pixel = None
              if geometry is not None:
                  pixel = invertGeometry(tthRad, chiRad, True)
  
                  ax, ay = numpy.array([pixel[1]]), numpy.array([pixel[0]])
                  tth = geometry.tth(ay, ax)[0]
                  chi = geometry.chi(ay, ax)[0]
  
                  error = numpy.sqrt((tthRad - tth) ** 2 + (chiRad - chi) ** 2)
                  if error > 0.05:
                      # The identified pixel is far from the requested chi/tth. Marker ignored.
                      pixel = None
              if pixel is not None:
                  marker.setPixelPosition(pixel[1], pixel[0])
              else:
                  marker.removePixelPosition()
          # TODO: should be managed by the model
          self.__markerModel.wasChanged()
          self.__markerModel.unlockSignals()
  
      def __plotIsShown(self):
          if self.__mustBeUpdated:
              self.__updateMarkers()
  
      def __updateMarkers(self):
          if not self.__plot.isVisible():
              # Update the display later
              self.__mustBeUpdated = True
              return
  
          self.__mustBeUpdated = False
  
          for item in self.__markers:
              self.__plot.removeMarker(item.getLegend())
  
          color = CalibrationContext.instance().getMarkerColor(0, mode="html")
          for marker in self.__markerModel:
              position = self.getMarkerLocation(marker)
              if position is None:
                  continue
              legend = self._ITEM_TEMPLATE % marker.name()
              self.__plot.addMarker(x=position[0], y=position[1], color=color, legend=legend, text=marker.name())
              item = self.__plot._getMarker(legend)
              self.__markers.append(item)
  
      def getMarkerLocation(self, marker):
          """
          Returns the location of the marker in the plot axes
          """
          if self.__pixelBasedPlot:
              if isinstance(marker, MarkerModel.PhysicalMarker):
                  return marker.pixelPosition()
              elif isinstance(marker, MarkerModel.PixelMarker):
                  return marker.pixelPosition()
              else:
                  _logger.debug("Unsupported marker %s", type(marker))
                  return None
          else:
              if isinstance(marker, MarkerModel.PhysicalMarker):
                  chiRad, tthRad = marker.physicalPosition()
              elif isinstance(marker, MarkerModel.PixelMarker):
                  if self.__geometry is None:
                      return None
                  x, y = marker.pixelPosition()
                  ax, ay = numpy.array([x]), numpy.array([y])
                  chiRad = self.__geometry.chi(ay, ax)[0]
                  tthRad = self.__geometry.tth(ay, ax)[0]
              else:
                  _logger.debug("Unsupported marker %s", type(marker))
                  return None
  
              if self.__radialUnit is None:
                  return
  
              try:
                  tth = unitutils.from2ThRad(tthRad,
                                             unit=self.__radialUnit,
                                             wavelength=self.__wavelength,
                                             directDist=self.__directDist)
                  chi = numpy.rad2deg(chiRad)
              except Exception:
                  _logger.debug("Backtrace", exc_info=True)
                  return None
              return tth, chi
  
      def findClosestMarker(self, mousePos, delta=20):
          delta = delta ** 2.0
          if isinstance(mousePos, qt.QPoint):
              mousePos = mousePos.x(), mousePos.y()
          currentMarker = None
          currentDistance = float("inf")
          for marker in self.__markerModel:
              location = self.getMarkerLocation(marker)
              if location is None:
                  continue
              location = self.__plot.dataToPixel(x=location[0], y=location[1], check=False)
              distance = (mousePos[0] - location[0]) ** 2.0 + (mousePos[1] - location[1]) ** 2.0
              if distance < currentDistance and distance < delta:
                  currentDistance = distance
                  currentMarker = marker
          return currentMarker
  
      def createRemoveClosestMaskerAction(self, parent, mousePos):
          action = qt.QAction(parent)
  
          marker = self.findClosestMarker(mousePos)
          if marker is None:
              return None
  
          action.setText("Remove marker '%s'" % marker.name())
          action.triggered.connect(functools.partial(self.__removeMarker, marker))
          return action
  
      def createMarkPixelAction(self, parent, mousePos):
          maskPixelAction = qt.QAction(parent)
          maskPixelAction.setText("Mark this pixel coord")
          maskPixelAction.triggered.connect(functools.partial(self.__createPixelMarker, mousePos))
          if not self.__pixelBasedPlot:
              maskPixelAction.setEnabled(self.__geometry is not None)
          return maskPixelAction
  
      def createMarkGeometryAction(self, parent, mousePos):
          maskGeometryAction = qt.QAction(parent)
          maskGeometryAction.setText(u"Mark this χ/2θ coord")
          maskGeometryAction.triggered.connect(functools.partial(self.__createGeometryMarker, mousePos))
          maskGeometryAction.setEnabled(self.__geometry is not None)
          return maskGeometryAction
  
      def dataToChiTth(self, data):
          """Returns chi and 2theta angles in radian from data coordinate"""
  
          if self.__pixelBasedPlot:
              x, y = data
              ax, ay = numpy.array([x]), numpy.array([y])
              chi = self.__geometry.chi(ay, ax)[0]
              tth = self.__geometry.tth(ay, ax)[0]
              return chi, tth
          else:
              try:
                  tthRad = unitutils.tthToRad(data[0],
                                              unit=self.__radialUnit,
                                              wavelength=self.__wavelength,
                                              directDist=self.__directDist)
              except Exception:
                  _logger.debug("Backtrace", exc_info=True)
                  tthRad = None
  
              chiDeg = data[1]
              if chiDeg is not None:
                  chiRad = numpy.deg2rad(chiDeg)
              else:
                  chiRad = None
  
              return chiRad, tthRad
  
      def __removeMarker(self, marker):
          self.__markerModel.remove(marker)
  
      def __createPixelMarker(self, pos):
          pos = self.__plot.pixelToData(pos.x(), pos.y())
          if self.__pixelBasedPlot:
              pixel = pos[1], pos[0]
          else:
              pixel = self.__invertGeometry(pos[0], pos[1], True)
  
              # Check if the result is accurate
              # TODO: This could be avoided by checking it inside invertGeometry
              chiRad, tthRad = self.dataToChiTth(pos)
              if tthRad is not None and chiRad is not None:
                  ax, ay = numpy.array([pixel[1]]), numpy.array([pixel[0]])
                  tth = self.__geometry.tth(ay, ax)[0]
                  chi = self.__geometry.chi(ay, ax)[0]
  
                  error = numpy.sqrt((tthRad - tth) ** 2 + (chiRad - chi) ** 2)
                  if error > 0.05:
                      _logger.error("The identified pixel is far from the requested chi/tth. Marker ignored.")
                      return
  
          name = self.__findUnusedMarkerName()
          marker = MarkerModel.PixelMarker(name, pixel[1], pixel[0])
          self.__markerModel.add(marker)
  
      def __createGeometryMarker(self, pos):
          pos = self.__plot.pixelToData(pos.x(), pos.y())
          chiRad, tthRad = self.dataToChiTth(pos)
          name = self.__findUnusedMarkerName()
          marker = MarkerModel.PhysicalMarker(name, chiRad, tthRad)
  
          if self.__pixelBasedPlot:
              marker.setPixelPosition(pos[0], pos[1])
          else:
              if self.__geometry is not None:
                  pixel = self.__invertGeometry(pos[0], pos[1], True)
                  marker.setPixelPosition(pixel[1], pixel[0])
              else:
                  marker.removePixelPosition()
  
          self.__markerModel.add(marker)
  
      def __findUnusedMarkerName(self):
          template = "mark%d"
          markerNames = set([m.name() for m in self.__markerModel])
          for i in range(0, 1000):
              name = template % i
              if name not in markerNames:
                  return name
          # Returns something
          return "mark"
  