Blob Blame Raw


#include "tvectorgl.h"
#include "tgl.h"
#include "tpalette.h"
#include "tproperty.h"
#include "tthreadmessage.h"
#include "tvectorimage.h"
#include "drawutil.h"
#include "tcurveutil.h"
#include "tstroke.h"
#include "tstrokeutil.h"
#include "tvectorrenderdata.h"
#include "tstrokedeformations.h"
#include "tmathutil.h"

// Toonz includes
#include "toonz/tobjecthandle.h"
#include "toonz/txshlevelhandle.h"

// TnzTools includes
#include "tools/tool.h"
#include "tools/toolutils.h"
#include "tools/cursors.h"

// Qt includes
#include <QCoreApplication>  // For Qt translation support

using namespace ToolUtils;

//*****************************************************************************
//    PumpTool declaration
//*****************************************************************************

class PumpTool final : public TTool {
  Q_DECLARE_TR_FUNCTIONS(PumpTool)

  int m_strokeStyleId, m_strokeIndex;  //!< Edited stroke indices
  TStroke *m_inStroke, *m_outStroke;   //!< Input/Output strokes
  std::vector<TStroke *>
      m_splitStrokes;  //!< Merging these, m_inStroke is reformed
  int m_stroke1Idx,
      m_stroke2Idx;  //!< Indices of deformed strokes among split ones

  TUndo *m_undo;  //!< Undo to be added upon non-trivial button up

  double m_actionW;               //!< The action center stroke parameter
  double m_actionS1, m_actionS2;  //!< Action center length in m_stroke
  double m_actionRadius;          //!< Tool action radius in curve length

  std::vector<double>
      m_splitPars;  //!< Split parameters for action localization
  std::vector<double> m_cpLenDiff1,
      m_cpLenDiff2;  //!< Distorted CPs' length distances from action center

  bool m_active;         //!< Whether a stroke is currently being edited
  bool m_enabled;        //!< Tells whether the image allows editing
  bool m_cursorEnabled;  //!< Whether the 'pump preview cursor' can be seen
  bool m_draw;           //!< Should be removed...?

  bool m_isCtrlPressed;  //!< Whether control key is held down or not
  int m_lockedStrokeIndex;

  TPointD m_oldPoint, m_downPoint;  //!< Mouse positions upon editing
  TThickPoint m_cursor;             //!< Pump preview cursor data
  int m_cursorId;

  double m_errorTol;  //!< Allowed approximation error during edit

  TDoubleProperty m_toolSize;
  TIntProperty m_accuracy;
  TPropertyGroup m_prop;

public:
  PumpTool()
      : TTool("T_Pump")
      , m_active(false)
      , m_actionW(0)
      , m_strokeIndex((std::numeric_limits<UINT>::max)())
      , m_inStroke(0)
      , m_outStroke(0)
      , m_stroke1Idx(-1)
      , m_stroke2Idx(-1)
      , m_cursorEnabled(false)
      , m_cursorId(ToolCursor::PumpCursor)
      , m_actionRadius(1)
      , m_draw(false)
      , m_undo(0)
      , m_toolSize("Size:", 1, 100, 20)
      , m_accuracy("Accuracy:", 0, 100, 40)
      , m_enabled(false) {
    bind(TTool::VectorImage);

    m_splitPars.resize(2);

    m_prop.bind(m_toolSize);
    m_prop.bind(m_accuracy);
  }

  ToolType getToolType() const override { return TTool::LevelWriteTool; }

  TPropertyGroup *getProperties(int targetType) override { return &m_prop; }

  void updateTranslation() override {
    m_toolSize.setQStringName(tr("Size:"));
    m_accuracy.setQStringName(tr("Accuracy:"));
  }

  void onEnter() override;
  void onLeave() override;

  void draw() override;

  void leftButtonDown(const TPointD &pos, const TMouseEvent &e) override;
  void leftButtonDrag(const TPointD &pos, const TMouseEvent &e) override;
  void leftButtonUp(const TPointD &pos, const TMouseEvent &e) override;

  void mouseMove(const TPointD &pos, const TMouseEvent &e) override;
  bool moveCursor(const TPointD &pos);

  int getCursorId() const override {
    if (m_viewer && m_viewer->getGuidedStrokePickerMode())
      return m_viewer->getGuidedStrokePickerCursor();
    return m_cursorId;
  }
  void invalidateCursorArea();

  void onDeactivate() override;

private:
  double actionRadius(double strokeLength);
  void splitStroke(TStroke *s);
  TStroke *mergeStrokes(const std::vector<TStroke *> &strokes);

  bool getNearestStrokeWithLock(const TPointD &p, double &outW,
                                UINT &strokeIndex, double &dist2,
                                bool onlyInCurrentGroup = false);

} PumpToolInstance;

//*****************************************************************************
//    PumpTool implementation
//*****************************************************************************

void PumpTool::onEnter() {
  m_draw = true;
  if (TTool::getApplication()->getCurrentObject()->isSpline() ||
      !(TVectorImageP)getImage(false)) {
    m_enabled  = false;
    m_cursorId = ToolCursor::CURSOR_NO;
  } else {
    m_enabled  = true;
    m_cursorId = ToolCursor::PumpCursor;
  }
}

//----------------------------------------------------------------------

void PumpTool::draw() {
  if (!m_draw || !m_enabled) return;

  TVectorImageP vi = TImageP(getImage(false));
  if (!vi) return;

  QMutexLocker lock(vi->getMutex());

  TPalette *palette = vi->getPalette();
  assert(palette);
  if (m_active) {
    // Editing with the tool
    assert(m_outStroke);

    TRectD bboxD(m_outStroke->getBBox());
    TRect bbox(tfloor(bboxD.x0), tfloor(bboxD.y0), tceil(bboxD.x1) - 1,
               tceil(bboxD.y1) - 1);

    tglDraw(TVectorRenderData(TAffine(), bbox, palette, 0, true), m_outStroke);
  } else {
    // Hovering

    double w, dist;
    UINT index;

    if (m_cursorEnabled) {
      // Draw cursor
      glColor3d(1.0, 0.0, 1.0);
      if (m_cursor.thick > 0) tglDrawCircle(m_cursor, m_cursor.thick);
      tglDrawCircle(m_cursor, m_cursor.thick + 4 * getPixelSize());
    }

    if (getNearestStrokeWithLock(m_cursor, w, index, dist, true)) {
      TStroke *stroke  = vi->getStroke(index);
      double totalLen  = stroke->getLength();
      double actionLen = actionRadius(totalLen);

      tglColor(TPixel32::Red);

      if (totalLen < actionLen ||
          (stroke->isSelfLoop() && totalLen < actionLen + actionLen))
        drawStrokeCenterline(*stroke, getPixelSize());
      else {
        int i, chunckIndex1, chunckIndex2;
        double t, t1, t2, w1, w2;

        double len = stroke->getLength(w);

        double len1 = len - actionLen;
        if (len1 < 0) {
          if (stroke->isSelfLoop()) {
            len1 += totalLen;
          } else {
            len1 = 0;
          }
        }

        double len2 = len + actionLen;
        if (len2 > totalLen) {
          if (stroke->isSelfLoop()) {
            len2 -= totalLen;
          } else {
            len2 = totalLen;
          }
        }

        w1 = stroke->getParameterAtLength(len1);
        w2 = stroke->getParameterAtLength(len2);

        int chunkCount = stroke->getChunkCount();

        stroke->getChunkAndT(w1, chunckIndex1, t1);
        stroke->getChunkAndT(w2, chunckIndex2, t2);
        double step;

        const TThickQuadratic *q = 0;

        glBegin(GL_LINE_STRIP);

        q    = stroke->getChunk(chunckIndex1);
        step = computeStep(*q, getPixelSize());

        if (chunckIndex1 == chunckIndex2 && t1 < t2) {
          for (t = t1; t < t2; t += step) tglVertex(q->getPoint(t));

          tglVertex(stroke->getPoint(w2));
          glEnd();
          return;
        }

        for (t = t1; t < 1; t += step) tglVertex(q->getPoint(t));

        for (i = chunckIndex1 + 1; i != chunckIndex2; i++) {
          if (i == chunkCount) i = 0;

          if (i == chunckIndex2) break;

          q    = stroke->getChunk(i);
          step = computeStep(*q, getPixelSize());
          for (t = 0; t < 1; t += step) tglVertex(q->getPoint(t));
        }

        q    = stroke->getChunk(chunckIndex2);
        step = computeStep(*q, getPixelSize());
        for (t = 0; t < t2; t += step) tglVertex(q->getPoint(t));

        tglVertex(stroke->getPoint(w2));

        glEnd();
      }
    }
  }
}

//----------------------------------------------------------------------

void PumpTool::leftButtonDown(const TPointD &pos, const TMouseEvent &e) {
  if (getViewer() && getViewer()->getGuidedStrokePickerMode()) {
    getViewer()->doPickGuideStroke(pos);
    return;
  }

  if (m_active || !m_enabled) return;

  m_isCtrlPressed = e.isCtrlPressed();

  assert(m_undo == 0);
  m_active = false;

  TVectorImageP vi(getImage(true));
  if (!vi) return;

  QMutexLocker lock(vi->getMutex());

  // set current point and init parameters
  m_oldPoint  = pos;
  m_downPoint = pos;

  m_inStroke = m_outStroke = 0;
  m_stroke1Idx = m_stroke2Idx = -1;
  m_splitPars[0] = m_splitPars[1] = -2;
  m_actionW                       = 0;

  m_errorTol = (1.0 - 0.01 * m_accuracy.getValue()) * getPixelSize();

  double dist2 = 0.0;
  int cpCount;
  int i;
  UINT index;

  if (getNearestStrokeWithLock(pos, m_actionW, index, dist2)) {
    // A stroke near the pressed point was found - modify it
    m_active      = true;
    m_strokeIndex = index;

    m_inStroke  = vi->getStroke(m_strokeIndex);
    m_outStroke = new TStroke(*m_inStroke);

    double totalLength = m_inStroke->getLength();
    TXshSimpleLevel *sl =
        TTool::getApplication()->getCurrentLevel()->getSimpleLevel();
    assert(sl);
    TFrameId id = getCurrentFid();

    // Allocate the modification undo - will be assigned to the undo manager on
    // mouse release
    m_undo = new UndoModifyStrokeAndPaint(sl, id, m_strokeIndex);

    // Set the stroke's style to 'none'. This is needed to make the original
    // stroke transparent,
    // while the deformed one is shown at its place.
    m_strokeStyleId = m_inStroke->getStyle();
    m_inStroke->setStyle(0);

    if (totalLength <= 0.0) {
      // Single point case
      cpCount = m_inStroke->getControlPointCount();
      m_cpLenDiff1.resize(cpCount);

      for (i = 0; i < cpCount; i++) m_cpLenDiff1[i] = 0.0;

      m_splitStrokes.resize(1);
      m_splitStrokes[0] = new TStroke(*m_inStroke);

      m_stroke1Idx = 0;
    } else
      // Common strokes - split the stroke according to deformation requirements
      splitStroke(m_inStroke);
  }

  invalidate();
}

//----------------------------------------------------------------------

void PumpTool::leftButtonDrag(const TPointD &pos, const TMouseEvent &e) {
  if (!m_active || !m_enabled) return;

  TVectorImageP vi(getImage(true));
  if (!vi || !m_outStroke) return;

  m_isCtrlPressed = e.isCtrlPressed();

  QMutexLocker lock(vi->getMutex());

  // Revert current deformation, recovering the one from button press
  delete m_outStroke;

  // Retrieve cursor's vertical displacement
  TPointD delta = TPointD(0, (pos - m_downPoint).y);
  int deltaSign = tsign(delta.y);
  if (deltaSign == 0) {
    // Use a copy of the original stroke
    m_outStroke = new TStroke(*m_inStroke);
    m_outStroke->setStyle(m_strokeStyleId);
    invalidate();
    return;
  }

  // Build deformation upon the original stroke pieces
  TStroke *stroke1 = 0, *stroke2 = 0;

  stroke1 = new TStroke(*m_splitStrokes[m_stroke1Idx]);

  // Deform stroke1
  TStrokeThicknessDeformation deformer(stroke1, delta, m_actionS1,
                                       m_actionRadius, deltaSign);
  modifyThickness(*stroke1, deformer, m_cpLenDiff1, deltaSign < 0);

  if (m_stroke2Idx >= 0) {
    // Deform stroke2
    stroke2 = new TStroke(*m_splitStrokes[m_stroke2Idx]);

    TStrokeThicknessDeformation deformer2(stroke2, delta, m_actionS2,
                                          m_actionRadius, deltaSign);
    modifyThickness(*stroke2, deformer2, m_cpLenDiff2, deltaSign < 0);
  }

  // Apply deformation
  std::vector<TStroke *> splitStrokesCopy(m_splitStrokes);
  splitStrokesCopy[m_stroke1Idx] = stroke1;
  if (stroke2) splitStrokesCopy[m_stroke2Idx] = stroke2;

  m_outStroke = mergeStrokes(splitStrokesCopy);

  delete stroke1;
  delete stroke2;

  invalidate();
}

//----------------------------------------------------------------------

void PumpTool::leftButtonUp(const TPointD &pos, const TMouseEvent &e) {
  TVectorImageP vi;

  if (!m_active || !m_enabled) goto cleanup;

  vi = TVectorImageP(getImage(true));
  if (!vi) goto cleanup;

  m_isCtrlPressed = e.isCtrlPressed();

  {
    m_active = false;

    QMutexLocker lock(vi->getMutex());

    // Reset cursor data
    double t;
    UINT index;
    double dist2;
    if (getNearestStrokeWithLock(pos, t, index, dist2)) {
      TStroke *nearestStroke = vi->getStroke(index);
      if (nearestStroke) m_cursor = nearestStroke->getThickPoint(t);
    }

    if (m_outStroke &&
        !areAlmostEqual(m_downPoint, pos, PickRadius * getPixelSize())) {
      // Accept action

      // Clone input stroke - it is someway needed by the stroke change
      // notifier... I wonder why...
      TStroke *oldStroke = new TStroke(*m_inStroke);

      m_outStroke->swap(*m_inStroke);

      m_inStroke->invalidate();

      delete m_outStroke;
      m_outStroke = 0;

      assert(m_undo);
      TUndoManager::manager()->add(m_undo);
      m_undo = 0;

      vi->notifyChangedStrokes(m_strokeIndex, oldStroke);
      notifyImageChanged();

      delete oldStroke;
    }
  }

cleanup:

  if (m_inStroke)
    m_inStroke->setStyle(
        m_strokeStyleId);  // Make the image stroke visible again

  m_strokeIndex = m_strokeStyleId = -1;

  clearPointerContainer(m_splitStrokes);

  delete m_outStroke;
  m_inStroke = m_outStroke = 0;

  delete m_undo;
  m_undo = 0;

  invalidate();
}

//----------------------------------------------------------------------

void PumpTool::invalidateCursorArea() {
  double r = m_cursor.thick + 6;
  TPointD d(r, r);
  invalidate(TRectD(m_cursor - d, m_cursor + d));
}

//----------------------------------------------------------------------

void PumpTool::mouseMove(const TPointD &pos, const TMouseEvent &e) {
  if (m_active || !m_enabled) return;

  m_isCtrlPressed = e.isCtrlPressed();

  // Cursor preview updates on 3-pixel steps
  if (tdistance2(pos, m_oldPoint) < 9.0 * sq(getPixelSize())) return;

  if (!m_draw) m_draw = true;

  m_oldPoint = pos;

  if (moveCursor(pos)) {
    m_cursorEnabled = true;
    invalidate();
  } else
    m_cursorEnabled = false;

  invalidate();
}

//----------------------------------------------------------------------

bool PumpTool::moveCursor(const TPointD &pos) {
  TVectorImageP vi(getImage(false));
  if (vi) {
    double t;
    UINT index;
    double dist2;
    if (getNearestStrokeWithLock(pos, t, index, dist2)) {
      TStroke *stroke = vi->getStroke(index);
      if (stroke) {
        m_cursor = stroke->getThickPoint(t);
        return true;
      }
    }
  }

  return false;
}

//----------------------------------------------------------------------

void PumpTool::onDeactivate() {
  m_draw = false;
  if (m_active) {
    m_active = false;
    TVectorImageP vi(getImage(true));
    assert(!!vi && m_outStroke);
    if (!vi || !m_outStroke) return;

    clearPointerContainer(m_splitStrokes);
    if (m_splitPars[0] == -1) {
      delete m_outStroke;
      m_outStroke = 0;
    }

    // restore previous style
    assert(m_strokeIndex >= 0);
    if (m_strokeIndex >= 0) {
      TStroke *stroke = vi->getStroke(m_strokeIndex);
      stroke->setStyle(m_strokeStyleId);
    }

    assert(m_undo);
    delete m_undo;
    m_undo = 0;

    invalidate();

    m_strokeIndex = -1;
    m_outStroke   = 0;
  }
}

//----------------------------------------------------------------------

void PumpTool::onLeave() {
  if (!m_active) m_draw = false;
}

//*****************************************************************************
//    PumpTool privates
//*****************************************************************************

double PumpTool::actionRadius(double strokeLength) {
  double toolSize         = m_toolSize.getValue();
  double toolPercent      = toolSize * 0.01;
  double interpolationVal = pow(toolPercent, 5);
  double independentValue = 7.0 * toolSize;

  double actionRadius = (independentValue) * (1.0 - interpolationVal) +
                        (strokeLength * toolPercent) * interpolationVal;

  return std::max(actionRadius, independentValue);
}

//----------------------------------------------------------------------

/*
  Edited strokes are split near the corresponding editing position, in order
  to localize stroke manipulation.
  Only the localized part of the stroke will receive CP increase and thickness
  tuning needed for the tool action.
*/
void PumpTool::splitStroke(TStroke *s) {
  assert(m_splitStrokes.empty());

  TStroke *stroke1 = 0, *stroke2 = 0;

  // Build the action radius
  double totalLength = s->getLength();
  m_actionRadius     = actionRadius(totalLength);

  // Get the length at selected point and build the split (length) positions
  m_actionS1      = s->getLength(m_actionW);
  double startLen = m_actionS1 - m_actionRadius;
  double endLen   = m_actionS1 + m_actionRadius;

  // Now, perform splitting
  int i, cpCount;

  if ((startLen <= 0 && endLen >= totalLength) ||
      (s->isSelfLoop() && totalLength < (m_actionRadius + m_actionRadius))) {
    // The whole stroke is included in the action - no split
    m_splitStrokes.resize(1);

    m_splitPars[0] = -1;

    m_splitStrokes[0] = new TStroke(*s);

    m_stroke1Idx = 0;
    stroke1      = m_splitStrokes[m_stroke1Idx];

    TStrokeThicknessDeformation deformer(s, m_actionS1, m_actionRadius);
    increaseControlPoints(*stroke1, deformer, getPixelSize());
  } else {
    if (!s->isSelfLoop() || (startLen >= 0.0 && endLen <= totalLength)) {
      // Regular split positions, in the [0.0, totalLength] range.
      // Split points at extremities are dealt.

      m_splitPars[0] = s->getParameterAtLength(
          std::max(startLen, 0.0));  // Crop in the open case
      m_splitPars[1] = s->getParameterAtLength(std::min(endLen, totalLength));

      if (m_splitPars[0] ==
          0.0)  // the "&& m_splitPars[0] == totalLength" was dealt outside
      {
        m_splitStrokes.resize(2);
        m_splitStrokes[0] = new TStroke;
        m_splitStrokes[1] = new TStroke;

        s->split(m_splitPars[1], *(m_splitStrokes[0]), *(m_splitStrokes[1]));

        m_stroke1Idx = 0;
      } else {
        if (m_splitPars[1] == 1.0) {
          m_splitStrokes.resize(2);
          m_splitStrokes[0] = new TStroke;
          m_splitStrokes[1] = new TStroke;

          s->split(m_splitPars[0], *(m_splitStrokes[0]), *(m_splitStrokes[1]));
        } else
          ::splitStroke(*s, m_splitPars, m_splitStrokes);

        m_stroke1Idx = 1;

        // Update the edit point to refer to the central stroke piece
        m_actionS1 -= m_splitStrokes[0]->getLength();
      }

      stroke1 = m_splitStrokes[m_stroke1Idx];

      // Apply deformation to the middle piece
      TStrokeThicknessDeformation deformer(stroke1, m_actionS1, m_actionRadius);
      increaseControlPoints(*stroke1, deformer, getPixelSize());

      m_actionS2 = 0;
    } else {
      // Circular 'overflow' case - (exactly) one split point is outside the
      // regular scope.

      // Since the action diameter is < totalLength, these cases are mutually
      // exclusive.
      if (startLen < 0)
        startLen += totalLength;
      else {
        endLen -= totalLength;
        m_actionS1 -= totalLength;
      }

      // The deformation must be applied in two distinct strokes, since its
      // action interval crosses the junction point

      m_splitPars[0] = s->getParameterAtLength(endLen);
      m_splitPars[1] = s->getParameterAtLength(startLen);

      ::splitStroke(*s, m_splitPars, m_splitStrokes);
      assert(m_splitStrokes.size() >= 3);

      m_stroke1Idx = 0;
      m_stroke2Idx = 2;

      stroke1 = m_splitStrokes[m_stroke1Idx];
      stroke2 = m_splitStrokes[m_stroke2Idx];

      m_actionS2 = m_actionS1 + stroke2->getLength();

      TStrokeThicknessDeformation deformer(stroke1, m_actionS1, m_actionRadius);
      increaseControlPoints(*stroke1, deformer, getPixelSize());
      TStrokeThicknessDeformation deformer2(stroke2, m_actionS2,
                                            m_actionRadius);
      increaseControlPoints(*stroke2, deformer2, getPixelSize());

      cpCount = stroke2->getControlPointCount();
      m_cpLenDiff2.resize(cpCount);

      for (i = 0; i < cpCount; ++i)
        m_cpLenDiff2[i] = stroke2->getLengthAtControlPoint(i) - m_actionS2;
    }
  }

  cpCount = stroke1->getControlPointCount();
  m_cpLenDiff1.resize(cpCount);

  double diff;
  for (i = 0; i < cpCount; i++) {
    diff            = stroke1->getLengthAtControlPoint(i) - m_actionS1;
    m_cpLenDiff1[i] = (s->isSelfLoop() && stroke2 && totalLength - diff < diff)
                          ? totalLength - diff
                          : diff;
  }
}

//----------------------------------------------------------------------

/*
  A split stroke must be reassembled before it is output.
  In particular, it must be ensured that the merge does not add additional CPS
  at split points, leaving the output seamless.
*/
TStroke *PumpTool::mergeStrokes(const std::vector<TStroke *> &strokes) {
  assert(strokes.size() > 0);

  TStroke *mergedStroke;
  if (strokes.size() > 1) {
    if (m_errorTol > 0.0) {
      strokes[m_stroke1Idx]->reduceControlPoints(m_errorTol);
      if (m_stroke2Idx >= 0)
        strokes[m_stroke2Idx]->reduceControlPoints(m_errorTol);
    }

    // Merge split strokes
    mergedStroke = merge(strokes);
    // mergedStroke->reduceControlPoints(0.4*getPixelSize());    //Originally on
    // the whole result...

    if (m_inStroke->isSelfLoop()) {
      int cpCount = mergedStroke->getControlPointCount();

      TThickPoint p1   = mergedStroke->getControlPoint(0);
      TThickPoint p2   = mergedStroke->getControlPoint(cpCount - 1);
      TThickPoint midP = 0.5 * (p1 + p2);

      mergedStroke->setControlPoint(0, midP);
      mergedStroke->setControlPoint(cpCount - 1, midP);
      mergedStroke->setSelfLoop(true);
    }

    mergedStroke->outlineOptions() = strokes[0]->outlineOptions();
  } else {
    mergedStroke = new TStroke(*strokes[0]);
    if (m_errorTol > 0.0) mergedStroke->reduceControlPoints(m_errorTol);
  }

  mergedStroke->setStyle(m_strokeStyleId);
  mergedStroke->invalidate();

  return mergedStroke;
}

bool PumpTool::getNearestStrokeWithLock(const TPointD &p, double &outW,
                                        UINT &strokeIndex, double &dist2,
                                        bool onlyInCurrentGroup) {
  TVectorImageP vi = TImageP(getImage(false));
  if (!vi) return false;

  if (m_lockedStrokeIndex >= vi->getStrokeCount()) {
    m_lockedStrokeIndex = -1;
  }

  if (m_isCtrlPressed && m_lockedStrokeIndex >= 0) {
    TStroke *stroke = vi->getStroke(m_lockedStrokeIndex);
    strokeIndex     = m_lockedStrokeIndex;
    return stroke->getNearestW(p, outW, dist2);
  }

  UINT index;
  if (vi->getNearestStroke(p, outW, index, dist2, onlyInCurrentGroup)) {
    m_lockedStrokeIndex = index;
    strokeIndex         = index;
    return true;
  }

  return false;
}