commit c622321715458fe0a506b2664be0d859b4b741dd
Author: Leszek Koltunski <leszek@koltunski.pl>
Date:   Fri Apr 5 00:56:22 2019 +0100

    Improve the Rubik App.

diff --git a/src/main/java/org/distorted/examples/rubik/RubikRenderer.java b/src/main/java/org/distorted/examples/rubik/RubikRenderer.java
index 1029c3e..550207d 100644
--- a/src/main/java/org/distorted/examples/rubik/RubikRenderer.java
+++ b/src/main/java/org/distorted/examples/rubik/RubikRenderer.java
@@ -281,11 +281,32 @@ class RubikRenderer implements GLSurfaceView.Renderer
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-    void rotateFace(Static3D vector, int angle, int row )
+    void addNewRotation(int vector, int row )
       {
-      String vect = "("+vector.get1()+","+vector.get2()+","+vector.get3()+")";
+      String vect="?";
 
-      android.util.Log.e("rubik", "rotating by "+angle+" degrees, row="+row+" vector: "+vect);
+      switch(vector)
+        {
+        case RubikSurfaceView.VECTX: vect="X"; break;
+        case RubikSurfaceView.VECTY: vect="Y"; break;
+        case RubikSurfaceView.VECTZ: vect="Z"; break;
+        }
+
+      android.util.Log.e("rubik", "added new rotation (row="+row+" vector: "+vect+")");
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    void continueRotation(float angle)
+      {
+      android.util.Log.e("rubik", "rotating by angle: "+ (((float)((int)(angle*100.0f)))/100.0f) );
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    void finishRotation()
+      {
+      android.util.Log.e("rubik", "finishing rotation");
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/examples/rubik/RubikSurfaceView.java b/src/main/java/org/distorted/examples/rubik/RubikSurfaceView.java
index ecd7142..70897c2 100644
--- a/src/main/java/org/distorted/examples/rubik/RubikSurfaceView.java
+++ b/src/main/java/org/distorted/examples/rubik/RubikSurfaceView.java
@@ -25,7 +25,6 @@ import android.content.pm.ConfigurationInfo;
 import android.opengl.GLSurfaceView;
 import android.view.MotionEvent;
 
-import org.distorted.library.type.Static3D;
 import org.distorted.library.type.Static4D;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -33,26 +32,28 @@ import org.distorted.library.type.Static4D;
 class RubikSurfaceView extends GLSurfaceView
 {
     private final static int NONE   =-1;
-    private final static int FRONT  = 0;
-    private final static int BACK   = 1;
-    private final static int LEFT   = 2;
-    private final static int RIGHT  = 3;
-    private final static int TOP    = 4;
-    private final static int BOTTOM = 5;
+    private final static int FRONT  = 0;  // has to be 6 consecutive ints
+    private final static int BACK   = 1;  // FRONT ... BOTTOM
+    private final static int LEFT   = 2;  //
+    private final static int RIGHT  = 3;  //
+    private final static int TOP    = 4;  //
+    private final static int BOTTOM = 5;  //
 
     private static final int DIR_UP   =0;
     private static final int DIR_DOWN =1;
     private static final int DIR_LEFT =2;
     private static final int DIR_RIGHT=3;
 
-    private static final Static3D VECTX = new Static3D(1,0,0);
-    private static final Static3D VECTY = new Static3D(0,1,0);
-    private static final Static3D VECTZ = new Static3D(0,0,1);
+    static final int VECTX = 0;
+    static final int VECTY = 1;
+    static final int VECTZ = 2;
+    static final int VECTN = 3;
 
     private boolean mDragging, mRotating;
     private int mX, mY;
     private int mTouchedRow, mTouchedCol, mTouchedSli;
     private Static4D mQuatCurrent, mQuatAccumulated;
+    private int mRotationVect;
     private RubikRenderer mRenderer;
 
     private float mPoiX, mPoiY, mPoiZ, mCamX, mCamY, mCamZ;
@@ -67,6 +68,8 @@ class RubikSurfaceView extends GLSurfaceView
 
       mDragging = false;
       mRotating = false;
+      mRotationVect = VECTN;
+
       mRenderer = new RubikRenderer(this);
 
       mQuatCurrent     = new Static4D(0,0,0,1);
@@ -123,12 +126,18 @@ class RubikSurfaceView extends GLSurfaceView
 
                                          if( (mX-x)*(mX-x)+(mY-y)*(mY-y)>minimumToRotate )
                                            {
-                                           rotateFace(x,y);
+                                           addNewRotation(x,y);
                                            mRotating = false;
                                            }
                                          }
+                                       else
+                                         {
+                                         continueRotation(x,y);
+                                         }
                                        break;
-         case MotionEvent.ACTION_UP  : mDragging = false;
+         case MotionEvent.ACTION_UP  : if( !mDragging ) finishRotation();
+
+                                       mDragging = false;
                                        mRotating = false;
 
                                        mQuatAccumulated.set(quatMultiply(mQuatCurrent, mQuatAccumulated));
@@ -143,63 +152,118 @@ class RubikSurfaceView extends GLSurfaceView
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-    private void rotateFace(int x, int y)
+    private void addNewRotation(int x, int y)
       {
-      fillCameraAndTouchPoint(x,y);
+      fillTouchPoint(x,y);
+
       float cubeHalfSize= mRenderer.returnCubeSize()*0.5f;
-      float A=0f;
+      float tmp = RubikRenderer.NUM_CUBES/(2*cubeHalfSize);
+      float A=retA(mLastTouchedFace,cubeHalfSize);
 
-      switch(mLastTouchedFace)
-        {
-        case FRONT : A = ( mPoiZ!=mCamZ ? ( cubeHalfSize-mCamZ)/(mPoiZ-mCamZ) : 0f); break;
-        case BACK  : A = ( mPoiZ!=mCamZ ? (-cubeHalfSize-mCamZ)/(mPoiZ-mCamZ) : 0f); break;
-        case LEFT  : A = ( mPoiX!=mCamX ? (-cubeHalfSize-mCamX)/(mPoiX-mCamX) : 0f); break;
-        case RIGHT : A = ( mPoiX!=mCamX ? ( cubeHalfSize-mCamX)/(mPoiX-mCamX) : 0f); break;
-        case TOP   : A = ( mPoiY!=mCamY ? ( cubeHalfSize-mCamY)/(mPoiY-mCamY) : 0f); break;
-        case BOTTOM: A = ( mPoiY!=mCamY ? (-cubeHalfSize-mCamY)/(mPoiY-mCamY) : 0f); break;
-        }
       float diffX = (mPoiX-mCamX)*A + mCamX - mStartX;
       float diffY = (mPoiY-mCamY)*A + mCamY - mStartY;
       float diffZ = (mPoiZ-mCamZ)*A + mCamZ - mStartZ;
 
-      Static3D rotV=VECTX;
-      int dir, rotA =1, rotRC=1;
+      int dir, row=1;
 
       switch(mLastTouchedFace)
         {
         case FRONT : dir = retDirection( diffX, diffY);
-                     rotV = (dir==DIR_UP || dir==DIR_DOWN) ? VECTX:VECTY;
-                     rotA = (dir==DIR_UP || dir==DIR_LEFT) ? -90:90;
-                     rotRC = (int)( RubikRenderer.NUM_CUBES*((rotV==VECTX?mStartX:mStartY)+cubeHalfSize)/(2*cubeHalfSize) );
+                     mRotationVect = (dir==DIR_UP || dir==DIR_DOWN) ? VECTX:VECTY;
+                     row = (int)( tmp*((mRotationVect==VECTX?mStartX:mStartY)+cubeHalfSize) );
                      break;
         case BACK  : dir = retDirection(-diffX, diffY);
-                     rotV = (dir==DIR_UP || dir==DIR_DOWN) ? VECTX:VECTY;
-                     rotA = (dir==DIR_UP || dir==DIR_RIGHT) ? 90:-90;
-                     rotRC = (int)( RubikRenderer.NUM_CUBES*((rotV==VECTX?mStartX:mStartY)+cubeHalfSize)/(2*cubeHalfSize) );
+                     mRotationVect = (dir==DIR_UP || dir==DIR_DOWN) ? VECTX:VECTY;
+                     row = (int)( tmp*((mRotationVect==VECTX?mStartX:mStartY)+cubeHalfSize) );
                      break;
         case LEFT  : dir = retDirection( diffZ, diffY);
-                     rotV = (dir==DIR_UP || dir==DIR_DOWN) ? VECTZ:VECTY;
-                     rotA = (dir==DIR_UP || dir==DIR_LEFT) ? -90:90;
-                     rotRC = (int)( RubikRenderer.NUM_CUBES*((rotV==VECTZ?mStartZ:mStartY)+cubeHalfSize)/(2*cubeHalfSize) );
+                     mRotationVect = (dir==DIR_UP || dir==DIR_DOWN) ? VECTZ:VECTY;
+                     row = (int)( tmp*((mRotationVect==VECTZ?mStartZ:mStartY)+cubeHalfSize) );
                      break;
         case RIGHT : dir = retDirection(-diffZ, diffY);
-                     rotV = (dir==DIR_UP || dir==DIR_DOWN) ? VECTZ:VECTY;
-                     rotA = (dir==DIR_UP || dir==DIR_RIGHT) ? 90:-90;
-                     rotRC = (int)( RubikRenderer.NUM_CUBES*((rotV==VECTZ?mStartZ:mStartY)+cubeHalfSize)/(2*cubeHalfSize) );
+                     mRotationVect = (dir==DIR_UP || dir==DIR_DOWN) ? VECTZ:VECTY;
+                     row = (int)( tmp*((mRotationVect==VECTZ?mStartZ:mStartY)+cubeHalfSize) );
                      break;
         case TOP   : dir = retDirection( diffX,-diffZ);
-                     rotV = (dir==DIR_UP || dir==DIR_DOWN) ? VECTX:VECTZ;
-                     rotA = (dir==DIR_UP || dir==DIR_RIGHT) ? -90:90;
-                     rotRC = (int)( RubikRenderer.NUM_CUBES*((rotV==VECTX?mStartX:mStartZ)+cubeHalfSize)/(2*cubeHalfSize) );
+                     mRotationVect = (dir==DIR_UP || dir==DIR_DOWN) ? VECTX:VECTZ;
+                     row = (int)( tmp*((mRotationVect==VECTX?mStartX:mStartZ)+cubeHalfSize) );
                      break;
         case BOTTOM: dir = retDirection( diffX, diffZ);
-                     rotV = (dir==DIR_UP || dir==DIR_DOWN) ? VECTX:VECTZ;
-                     rotA = (dir==DIR_UP || dir==DIR_LEFT) ? -90:90;
-                     rotRC = (int)( RubikRenderer.NUM_CUBES*((rotV==VECTX?mStartX:mStartZ)+cubeHalfSize)/(2*cubeHalfSize) );
+                     mRotationVect = (dir==DIR_UP || dir==DIR_DOWN) ? VECTX:VECTZ;
+                     row = (int)( tmp*((mRotationVect==VECTX?mStartX:mStartZ)+cubeHalfSize) );
                      break;
         }
 
-      mRenderer.rotateFace(rotV,rotA,rotRC);
+      mStartX = diffX + mStartX;
+      mStartY = diffY + mStartY;
+      mStartZ = diffZ + mStartZ;
+
+      mRenderer.addNewRotation(mRotationVect,row);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void continueRotation(int x, int y)
+      {
+      fillTouchPoint(x,y);
+
+      float angle=0.0f;
+      float cubeHalfSize= mRenderer.returnCubeSize()*0.5f;
+      float A=retA(mLastTouchedFace,cubeHalfSize);
+
+      float diffX = (mPoiX-mCamX)*A + mCamX - mStartX;
+      float diffY = (mPoiY-mCamY)*A + mCamY - mStartY;
+      float diffZ = (mPoiZ-mCamZ)*A + mCamZ - mStartZ;
+
+      switch(mRotationVect)
+        {
+        case VECTX: switch(mLastTouchedFace)
+                      {
+                      case FRONT : angle = returnAngle(-diffZ, diffY); break;
+                      case BACK  : angle = returnAngle( diffZ,-diffY); break;
+                      case TOP   : angle = returnAngle(-diffY,-diffZ); break;
+                      case BOTTOM: angle = returnAngle( diffY, diffZ); break;
+                      }
+                    break;
+        case VECTY: switch(mLastTouchedFace)
+                      {
+                      case FRONT : angle = returnAngle(-diffZ, diffX); break;
+                      case BACK  : angle = returnAngle( diffZ,-diffX); break;
+                      case LEFT  : angle = returnAngle( diffX, diffZ); break;
+                      case RIGHT : angle = returnAngle(-diffX,-diffZ); break;
+                      }
+                    break;
+        case VECTZ: switch(mLastTouchedFace)
+                      {
+                      case TOP   : angle = returnAngle( diffY, diffX); break;
+                      case BOTTOM: angle = returnAngle(-diffY,-diffX); break;
+                      case LEFT  : angle = returnAngle(-diffX, diffY); break;
+                      case RIGHT : angle = returnAngle( diffX,-diffY); break;
+                      }
+                    break;
+        default   : android.util.Log.e("View", "impossible vector: "+mRotationVect);
+        }
+
+      mRenderer.continueRotation(angle);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void finishRotation()
+      {
+      mRotationVect = VECTN;
+
+      mRenderer.finishRotation();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private float returnAngle(float diff1, float diff2)
+      {
+      float len = (float)Math.sqrt(diff1*diff1 + diff2*diff2);
+      len /= mRenderer.mScreenMin;
+
+      return diff2>0 ? len : -len;
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -322,125 +386,33 @@ class RubikSurfaceView extends GLSurfaceView
       {
       float cubeHalfSize= mRenderer.returnCubeSize()*0.5f;
 
-      fillCameraAndTouchPoint(xTouch,yTouch);
+      fillTouchPoint(xTouch,yTouch);
+      fillCamera();
 
-      if( faceIsVisible(FRONT) )
+      for(int face=FRONT; face<=BOTTOM; face++)
         {
-        if( mPoiZ!= mCamZ )
+        if( faceIsVisible(face) && faceCondition(face) )
           {
-          float A = (cubeHalfSize-mCamZ)/(mPoiZ-mCamZ);
-          mStartX = (mPoiX-mCamX)*A + mCamX;
-          mStartY = (mPoiY-mCamY)*A + mCamY;
-          mStartZ = (mPoiZ-mCamZ)*A + mCamZ;
-          float qX= (mStartX+cubeHalfSize) / (2*cubeHalfSize);
-          float qY= (mStartY+cubeHalfSize) / (2*cubeHalfSize);
-
-          if( qX<=1 && qX>=0 && qY<=1 && qY>=0 )
-            {
-            mTouchedCol = (int)(qX*RubikRenderer.NUM_CUBES);
-            mTouchedRow = (int)(qY*RubikRenderer.NUM_CUBES);
-            mTouchedSli = RubikRenderer.NUM_CUBES - 1;
-            return FRONT;
-            }
-          }
-        }
-      if( faceIsVisible(BACK) )
-        {
-        if( mPoiZ!= mCamZ )
-          {
-          float A = (-cubeHalfSize-mCamZ)/(mPoiZ-mCamZ);
-          mStartX = (mPoiX-mCamX)*A + mCamX;
-          mStartY = (mPoiY-mCamY)*A + mCamY;
-          mStartZ = (mPoiZ-mCamZ)*A + mCamZ;
-          float qX= (mStartX+cubeHalfSize) / (2*cubeHalfSize);
-          float qY= (mStartY+cubeHalfSize) / (2*cubeHalfSize);
-
-          if( qX<=1 && qX>=0 && qY<=1 && qY>=0 )
-            {
-            mTouchedCol = (int)( qX*RubikRenderer.NUM_CUBES);
-            mTouchedRow = (int)( qY*RubikRenderer.NUM_CUBES);
-            mTouchedSli = 0;
-            return BACK;
-            }
-          }
-        }
-      if( faceIsVisible(LEFT) )
-        {
-        if( mPoiX!= mCamX )
-          {
-          float A = (-cubeHalfSize-mCamX)/(mPoiX-mCamX);
-          mStartX = (mPoiX-mCamX)*A + mCamX;
-          mStartY = (mPoiY-mCamY)*A + mCamY;
-          mStartZ = (mPoiZ-mCamZ)*A + mCamZ;
-          float qY= (mStartY+cubeHalfSize) / (2*cubeHalfSize);
-          float qZ= (mStartZ+cubeHalfSize) / (2*cubeHalfSize);
+          float A = retA(face,cubeHalfSize);
 
-          if( qZ<=1 && qZ>=0 && qY<=1 && qY>=0 )
-            {
-            mTouchedCol = 0;
-            mTouchedRow = (int)( qY*RubikRenderer.NUM_CUBES);
-            mTouchedSli = (int)( qZ*RubikRenderer.NUM_CUBES);
-            return LEFT;
-            }
-          }
-        }
-      if( faceIsVisible(RIGHT) )
-        {
-        if( mPoiX!= mCamX )
-          {
-          float A = (cubeHalfSize-mCamX)/(mPoiX-mCamX);
           mStartX = (mPoiX-mCamX)*A + mCamX;
           mStartY = (mPoiY-mCamY)*A + mCamY;
           mStartZ = (mPoiZ-mCamZ)*A + mCamZ;
-          float qY= (mStartY+cubeHalfSize) / (2*cubeHalfSize);
-          float qZ= (mStartZ+cubeHalfSize) / (2*cubeHalfSize);
 
-          if( qZ<=1 && qZ>=0 && qY<=1 && qY>=0 )
-            {
-            mTouchedCol = RubikRenderer.NUM_CUBES -1;
-            mTouchedRow = (int)( qY*RubikRenderer.NUM_CUBES);
-            mTouchedSli = (int)( qZ*RubikRenderer.NUM_CUBES);
-            return RIGHT;
-            }
-          }
-        }
-      if( faceIsVisible(TOP) )
-        {
-        if( mPoiY!= mCamY )
-          {
-          float A = (cubeHalfSize-mCamY)/(mPoiY-mCamY);
-          mStartX = (mPoiX-mCamX)*A + mCamX;
-          mStartY = (mPoiY-mCamY)*A + mCamY;
-          mStartZ = (mPoiZ-mCamZ)*A + mCamZ;
           float qX= (mStartX+cubeHalfSize) / (2*cubeHalfSize);
+          float qY= (mStartY+cubeHalfSize) / (2*cubeHalfSize);
           float qZ= (mStartZ+cubeHalfSize) / (2*cubeHalfSize);
 
-          if( qZ<=1 && qZ>=0 && qX<=1 && qX>=0 )
-            {
-            mTouchedCol = (int)( qX*RubikRenderer.NUM_CUBES);
-            mTouchedRow = RubikRenderer.NUM_CUBES -1;
-            mTouchedSli = (int)( qZ*RubikRenderer.NUM_CUBES);
-            return TOP;
-            }
-          }
-        }
-      if( faceIsVisible(BOTTOM) )
-        {
-        if( mPoiY!= mCamY )
-          {
-          float A = (-cubeHalfSize-mCamY)/(mPoiY-mCamY);
-          mStartX = (mPoiX-mCamX)*A + mCamX;
-          mStartY = (mPoiY-mCamY)*A + mCamY;
-          mStartZ = (mPoiZ-mCamZ)*A + mCamZ;
-          float qX= (mStartX+cubeHalfSize) / (2*cubeHalfSize);
-          float qZ= (mStartZ+cubeHalfSize) / (2*cubeHalfSize);
+          if( qX==1.0f ) qX-= 0.5f/RubikRenderer.NUM_CUBES;
+          if( qY==1.0f ) qY-= 0.5f/RubikRenderer.NUM_CUBES;
+          if( qZ==1.0f ) qZ-= 0.5f/RubikRenderer.NUM_CUBES;
 
-          if( qZ<=1 && qZ>=0 && qX<=1 && qX>=0 )
+          if( qX<1 && qX>=0 && qY<1 && qY>=0 && qZ<1 && qZ>=0 )
             {
-            mTouchedCol = (int)( qX*RubikRenderer.NUM_CUBES);
-            mTouchedRow = 0;
-            mTouchedSli = (int)( qZ*RubikRenderer.NUM_CUBES);
-            return BOTTOM;
+            mTouchedCol = (int)(qX*RubikRenderer.NUM_CUBES);
+            mTouchedRow = (int)(qY*RubikRenderer.NUM_CUBES);
+            mTouchedSli = (int)(qZ*RubikRenderer.NUM_CUBES);
+            return face;
             }
           }
         }
@@ -454,25 +426,64 @@ class RubikSurfaceView extends GLSurfaceView
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-    void fillCameraAndTouchPoint(int x, int y)
+    void fillTouchPoint(int x, int y)
       {
-      float cameraDistance = mRenderer.returnCameraDistance();
-      float halfScrWidth   = mRenderer.getScreenWidth()*0.5f;
-      float halfScrHeight  = mRenderer.getScreenHeight()*0.5f;
+      float halfScrWidth  = mRenderer.getScreenWidth() *0.5f;
+      float halfScrHeight = mRenderer.getScreenHeight()*0.5f;
+      Static4D touchPoint = new Static4D(x-halfScrWidth, halfScrHeight-y, 0, 0);
+      Static4D rotatedTouchPoint= rotateVector(touchPoint);
 
-      Static4D cameraPoint = new Static4D(             0,               0, cameraDistance, 0);
-      Static4D touchPoint  = new Static4D(x-halfScrWidth, halfScrHeight-y,              0, 0);
+      mPoiX = rotatedTouchPoint.get1();
+      mPoiY = rotatedTouchPoint.get2();
+      mPoiZ = rotatedTouchPoint.get3();
+      }
 
-      Static4D rotatedCamera    = rotateVector(cameraPoint);
-      Static4D rotatedTouchPoint= rotateVector(touchPoint);
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    void fillCamera()
+      {
+      float cameraDistance = mRenderer.returnCameraDistance();
+
+      Static4D cameraPoint = new Static4D(0, 0, cameraDistance, 0);
+      Static4D rotatedCamera= rotateVector(cameraPoint);
 
       mCamX = rotatedCamera.get1();
       mCamY = rotatedCamera.get2();
       mCamZ = rotatedCamera.get3();
+      }
 
-      mPoiX = rotatedTouchPoint.get1();
-      mPoiY = rotatedTouchPoint.get2();
-      mPoiZ = rotatedTouchPoint.get3();
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    float retA(int face, float cubeHalfSize)
+      {
+      switch(face)
+        {
+        case FRONT : return ( mPoiZ!=mCamZ ? ( cubeHalfSize-mCamZ)/(mPoiZ-mCamZ) : 0f);
+        case BACK  : return ( mPoiZ!=mCamZ ? (-cubeHalfSize-mCamZ)/(mPoiZ-mCamZ) : 0f);
+        case LEFT  : return ( mPoiX!=mCamX ? (-cubeHalfSize-mCamX)/(mPoiX-mCamX) : 0f);
+        case RIGHT : return ( mPoiX!=mCamX ? ( cubeHalfSize-mCamX)/(mPoiX-mCamX) : 0f);
+        case TOP   : return ( mPoiY!=mCamY ? ( cubeHalfSize-mCamY)/(mPoiY-mCamY) : 0f);
+        case BOTTOM: return ( mPoiY!=mCamY ? (-cubeHalfSize-mCamY)/(mPoiY-mCamY) : 0f);
+        }
+
+      return 0.0f;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    boolean faceCondition(int face)
+      {
+      switch(face)
+        {
+        case FRONT :
+        case BACK  : return ( mPoiZ!=mCamZ );
+        case LEFT  :
+        case RIGHT : return ( mPoiX!=mCamX );
+        case TOP   :
+        case BOTTOM: return ( mPoiY!=mCamY );
+        }
+
+      return false;
       }
 }
 
