Android Open Source - gti350-lab2-android-demo Point2 D Util






From Project

Back to project page gti350-lab2-android-demo.

License

The source code is released under:

MIT License

If you think the Android project gti350-lab2-android-demo listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

package log350.example.example6;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.ListIterator;
//ww w. java  2 s .c o  m


// This is used to sort points by some "score",
// which could be an angle or other metric associated with each point.
class Point2DAndScore {
  public Point2D point;
  public float score;
  public boolean isScorePositiveInfinity; // if true, ``score'' is ignored
  public Point2DAndScore(Point2D p,float s,boolean isPosInf) {
    point = p; score = s; isScorePositiveInfinity = isPosInf;
  }
}
class Point2DAndScoreComparator implements Comparator<Point2DAndScore> {
  public int compare( Point2DAndScore a, Point2DAndScore b ) {
    if ( a.isScorePositiveInfinity ) {
      if ( b.isScorePositiveInfinity ) return 0; // equal
      else return 1; // a is greater
    }
    else if ( b.isScorePositiveInfinity ) {
      return -1; // b is greater
    }
    else return (a.score<b.score) ? -1 : ( (a.score>b.score) ? 1 : 0 );
  }
}



public class Point2DUtil {

  static public Point2D computeCentroidOfPoints( ArrayList<Point2D> points ) {
    float x = 0, y = 0;
    for ( Point2D p : points ) {
      x += p.x();
      y += p.y();
    }
    if ( points.size() > 1 ) {
      x /= points.size();
      y /= points.size();
    }
    return new Point2D( x, y );
  }

  static public boolean isPointInsidePolygon( ArrayList< Point2D > polygonPoints, Point2D q ) {
    // This code was copied, with minor changes, from
    //    http://local.wasp.uwa.edu.au/~pbourke/geometry/insidepoly/
    // where it (the code, not the algorithm) is attributed to Randolph Franklin.
    // The idea behind the algorithm is to imagine a ray projecting
    // from the point toward the right, and then count how many times
    // that ray intersects an edge of the polygon.
    // If the number is odd, the point is inside the polygon.

    boolean returnValue = false;
    int i, j;

    for (i = 0, j = polygonPoints.size()-1; i < polygonPoints.size(); j = i++) {

      Point2D pi = polygonPoints.get(i);
      float xi = pi.x();
      float yi = pi.y();
      Point2D pj = polygonPoints.get(j);
      float xj = pj.x();
      float yj = pj.y();

      if (
        (((yi <= q.y()) && (q.y() < yj)) || ((yj <= q.y()) && (q.y() < yi)))
        && (q.x() < (xj - xi) * (q.y() - yi) / (yj - yi) + xi)
      ) {
        returnValue = ! returnValue;
      }
    }
    return returnValue;
  }

  // Returns the points on the convex hull in counter-clockwise order
  // (assuming a coordinate system with x+ right and y+ up).
  // Uses the well known algorithm "Graham's scan" for computing the convex hull in 2D,
  // an algorithm nicely explained at
  //    http://www.personal.kent.edu/~rmuhamma/Compgeometry/MyCG/ConvexHull/GrahamScan/grahamScan.htm
  static public ArrayList< Point2D > computeConvexHull(
    // input
    ArrayList< Point2D > points
  ) {
    if ( points == null ) return null;
    if ( points.size() < 3 ) {
      ArrayList< Point2D > returnValue = new ArrayList< Point2D >();
      for ( Point2D p : points ) {
        returnValue.add( p );
      }
      return returnValue;
    }

    // There could be one or more points with minimal y coordinate.
    // We'll call these the "bottom" points.
    // Of these, we find the one with minimal x coordinate (the "bottom left" point)
    // and maximal x coordinate (the "bottom right" point).
    int indexOfBottomLeftPoint = 0;
    Point2D bottomLeftPoint = points.get( 0 );
    int indexOfBottomRightPoint = 0;
    Point2D bottomRightPoint = points.get( 0 );
    for ( int i = 1; i < points.size(); ++i ) {
      Point2D candidatePoint = points.get( i );
      if ( candidatePoint.y() < bottomLeftPoint.y() ) {
        indexOfBottomLeftPoint = indexOfBottomRightPoint = i;
        bottomLeftPoint = bottomRightPoint = candidatePoint;
      }
      else if ( candidatePoint.y() == bottomLeftPoint.y() ) {
        if ( candidatePoint.x() < bottomLeftPoint.x() ) {
          indexOfBottomLeftPoint = i;
          bottomLeftPoint = candidatePoint;
        }
        else if ( candidatePoint.x() > bottomRightPoint.x() ) {
          indexOfBottomRightPoint = i;
          bottomRightPoint = candidatePoint;
        }
      }
    }

    // Imagine that for each point, we compute the point's angle with respect to bottomLeftPoint,
    // and then sort the points by this angle.
    // This is equivalent to sorting the points by their cotangent, which is faster to compute.
    // Points with minimal y coordinate (i.e., "bottom" points)
    // will be given a cotangent of +infinity and dealt with later.
    Point2DAndScore [] pointsWithCotangents = new Point2DAndScore[ points.size() ];
    for ( int i = 0; i < points.size(); ++i ) {
      Point2D p = points.get( i );
      float delta_y = p.y() - bottomLeftPoint.y();
      assert delta_y >= 0;
      if ( delta_y == 0 ) {
        pointsWithCotangents[i] = new Point2DAndScore( p, 0, true );
      }
      else {
        float delta_x = p.x() - bottomLeftPoint.x();
        pointsWithCotangents[i] = new Point2DAndScore( p, delta_x/delta_y /* the cotangent */, false );
      }
    }
    // sort the points by their cotangent
    Arrays.sort(pointsWithCotangents, new Point2DAndScoreComparator());

    // We'll need to be able to efficiently remove points from consideration,
    // so we copy them into a linked list.
    // In doing this, we also reverse the order of points
    // (so they are in descending order of cotangent, i.e., in counter-clockwise order).
    // The points with +infinity cotangent (i.e. the "bottom" points)
    // can also be removed from consideration here,
    // so long as we keep the "bottom left" and "bottom right" points.
    LinkedList< Point2D > orderedPoints = new LinkedList< Point2D >();
    orderedPoints.add( bottomLeftPoint );
    // check if the "bottom left" and "bottom right" points are distinct
    if ( indexOfBottomLeftPoint != indexOfBottomRightPoint )
      orderedPoints.add( bottomRightPoint );
    for ( int i = pointsWithCotangents.length - 1; i >= 0; --i ) {
      if ( ! pointsWithCotangents[i].isScorePositiveInfinity ) {
        orderedPoints.add( pointsWithCotangents[i].point );
      }
    }

    if ( orderedPoints.size() > 2 ) {
      // We will loop through the ordered points, processing 3 consecutive points at a time.
      // Two iterators are used to backup and move forward.
      Point2D p0 = orderedPoints.get(0);
      Point2D p1 = orderedPoints.get(1);
      Point2D p2 = orderedPoints.get(2);
      ListIterator< Point2D > it3 = orderedPoints.listIterator(3);
      assert it3.nextIndex() == 3;
      while ( true ) {
        assert orderedPoints.size() > 2;
        Vector2D v01 = new Vector2D( p1.x()-p0.x(), p1.y()-p0.y() );
        Vector2D v12 = new Vector2D( p2.x()-p1.x(), p2.y()-p1.y() );

        // Compute the z component of the cross product of v1 and v2
        // (Note that the x and y components of the cross product are zero,
        // because the z components of a and b are both zero)
        float crossProduct_z = v01.x()*v12.y() - v01.y()*v12.x();

        if ( crossProduct_z > 0 ) {
          // we have a left turn; try to step forward
          if ( it3.hasNext() ) {
            p0 = p1;
            p1 = p2;
            p2 = it3.next();
          }
          else {
            // we can't step forward
            break;
          }
        }
        else {
          // Either we have a right-hand turn,
          // or the points are collinear (with the 3rd point either in front, or behind, the 2nd)
          // In any case, we remove the 2nd point from consideration and (try to) backup.
          assert it3.hasPrevious();
          it3.previous();
          assert it3.hasPrevious();
          it3.previous();
          it3.remove(); // deletes the 2nd point
          assert it3.hasNext();
          it3.next(); // now the iterator is back to where it used to be

          // now we try to backup
          assert it3.hasPrevious();
          it3.previous();
          assert it3.hasPrevious();
          it3.previous();
          if ( it3.hasPrevious() ) {
            p1 = p0;
            p0 = it3.previous();
            it3.next();
            it3.next();
            it3.next();
          }
          else {
            it3.next();
            it3.next();
            // we step forward instead
            if ( it3.hasNext() ) {
              p1 = p2;
              p2 = it3.next();
            }
            else {
              // we can't move in either direction
              break;
            }
          }
        }
      } // while
    }

    // copy the results to the appropriate output format
    ArrayList< Point2D > returnValue = new ArrayList< Point2D >();

    for ( Point2D p : orderedPoints ) {
      returnValue.add( p );
    }
    return returnValue;
  }

  static public ArrayList< Point2D > computeExpandedPolygon(
    ArrayList< Point2D > points, // input
    float marginThickness
  ) {
    ArrayList< Point2D > newPoints = new ArrayList< Point2D >();
    if ( points.size() == 0 ) {
      // do nothing
    }
    else if ( points.size() == 1 ) {
      Point2D p = points.get(0);
      newPoints.add( new Point2D( p.x()-marginThickness, p.y() ) );
      newPoints.add( new Point2D( p.x(), p.y()-marginThickness ) );
      newPoints.add( new Point2D( p.x()+marginThickness, p.y() ) );
      newPoints.add( new Point2D( p.x(), p.y()+marginThickness ) );
    }
    else if ( points.size() == 2 ) {
      Point2D p0 = points.get(0);
      Point2D p1 = points.get(1);
      Vector2D v0 = Vector2D.mult( Point2D.diff(p1,p0).normalized(), marginThickness );
      Vector2D v1 = new Vector2D( -v0.y(), v0.x() );
      newPoints.add( Point2D.sum( p0, v1 ) );
      newPoints.add( Point2D.sum( p0, v0.negated() ) );
      newPoints.add( Point2D.sum( p0, v1.negated() ) );
      newPoints.add( Point2D.sum( p1, v1.negated() ) );
      newPoints.add( Point2D.sum( p1, v0 ) );
      newPoints.add( Point2D.sum( p1, v1 ) );
    }
    else {
      for ( int i = 0; i < points.size(); ++i ) {
        Point2D p = points.get(i);
        Point2D p_previous = points.get( i==0 ? points.size()-1 : i-1 );
        Point2D p_next = points.get( (i+1) % points.size() );
        Vector2D v_previous = Point2D.diff( p, p_previous ).normalized();
        Vector2D v_next = Point2D.diff( p, p_next ).normalized();

        newPoints.add( Point2D.sum( p, Vector2D.mult(new Vector2D(v_previous.y(),-v_previous.x()),marginThickness) ) );
        newPoints.add( Point2D.sum( p, Vector2D.mult( Vector2D.sum(v_next,v_previous).normalized(), marginThickness ) ) );
        newPoints.add( Point2D.sum( p, Vector2D.mult(new Vector2D(-v_next.y(),v_next.x()),marginThickness) ) );
      }
    }
    return newPoints;
  }

  // Returns false if the PCA fails.
  static public boolean principleComponentAnalysis(
    // Input
    Point2D [] points,

    // Ouput.
    // The 1st eigenvector is the principle one.
    Vector2D eigenvector1,
    Vector2D eigenvector2,
    double [] eigenvalues // output; caller must pass in a 2-element array (for eigenvalue1 and eigenvalue2)
  ) {
    if ( points.length < 2 )
      return false;

    // Compute the mean of the points
    double meanX = 0, meanY = 0;
    int i;
    for ( i = 0; i < points.length; ++i ) {
      meanX += points[i].x();
      meanY += points[i].y();
    }
    meanX /= points.length;
    meanY /= points.length;

    // Compute the covariance matrix
    // (which is a symmetric 2x2 matrix):
    //
    //    [  covXX  covXY  ]
    //    [  covXY  covYY  ]
    //
    // where "covAB" is the covariance of A and B,
    // and "covAA" is the same as the variance of A.
    //
    double covXX = 0, covXY = 0, covYY = 0;
    for ( i = 0; i < points.length; ++i ) {
      covXX += (points[i].x()-meanX)*(points[i].x()-meanX);
      covXY += (points[i].x()-meanX)*(points[i].y()-meanY);
      covYY += (points[i].y()-meanY)*(points[i].y()-meanY);
    }
    covXX /= ( points.length - 1 );
    covXY /= ( points.length - 1 );
    covYY /= ( points.length - 1 );

    // BEGIN: Perform eigendecomposition of the covariance matrix

    double discriminant = Math.sqrt( (covXX-covYY)*(covXX-covYY) + 4*covXY*covXY );
    // Note that, if the discriminant is nearly zero,
    // this means that the variance in X and in Y are nearly the same,
    // and that X and Y are nearly uncorrelated.
    // In other words, there's no single dominant direction for the data.
    if ( Math.abs(discriminant) <= Float.MIN_VALUE )
      return false;

    double eigenvalue1 = ((covXX+covYY) + discriminant)/2;
    double eigenvalue2 = ((covXX+covYY) - discriminant)/2;

    // Note that, if eigenvalue1 is nearly zero,
    // then there's nearly no variance in the data in any direction,
    // and eigenvalue2 should be nearly zero as well.
    // However, if only eigenvalue2 is nearly zero,
    // then covXX*covYY is nearly equal to covXY^2, but that's not a problem.
    if ( Math.abs(eigenvalue1) <= Float.MIN_VALUE )
      return false;

    eigenvector1.copy( new Vector2D((float)covXY,(float)(eigenvalue1-covXX)).normalized() );
    eigenvector2.copy( new Vector2D((float)covXY,(float)(eigenvalue2-covXX)).normalized() );

    // END: Perform eigendecomposition of the covariance matrix

    eigenvalues[0] = eigenvalue1;
    eigenvalues[1] = eigenvalue2;

    return true;
  }

  // Imagine a sheet of paper on a horizontal surface,
  // and imagine the user places a finger tip on the sheet of paper
  // and then drags their finger.
  // The sheet of paper will be translated and rotated by the finger.
  // This method implements such a transformation.
  // The given points could be the corners of the sheet of paper,
  // or vertices of a polygon or other shape.
  // The old and new location of the finger is also given.
  // This method can be useful for allowing the user to drag around
  // a shape in 2D using a single point input device.
  // Implementation based on Michel Beaudouin-Lafon http://doi.acm.org/10.1145/502348.502371
  //
  static public void transformPointsBasedOnDisplacementOfOnePoint(
    ArrayList<Point2D> points,
    // these should, of course, be in the same coordinate system as the points to transform
    Point2D P_old,
    Point2D P_new
  ) {
    Point2D centroid = computeCentroidOfPoints( points );
    Vector2D v1 = Point2D.diff( P_old, centroid );
    Vector2D v2 = Point2D.diff( P_new, centroid );
    float rotationAngle = Vector2D.computeSignedAngle( v1, v2 );
    float lengthToPreserve = v1.length();
    Point2D newCentroid = Point2D.sum(
      P_new,
      Vector2D.mult( v2.normalized(), - lengthToPreserve )
    );
    Vector2D translation = Point2D.diff( newCentroid, centroid );
    float cosine = (float)Math.cos( rotationAngle );
    float sine = (float)Math.sin( rotationAngle );

    for ( Point2D p : points ) {
      float relativeX = p.x() - centroid.x();
      float relativeY = p.y() - centroid.y();
      p.get()[0] = (cosine*relativeX - sine*relativeY) + translation.x() + centroid.x();
      p.get()[1] = (sine*relativeX + cosine*relativeY) + translation.y() + centroid.y();
    }
  }

  // This can be used to implement bimanual (2-handed) manipulation,
  // or 2-finger manipulation, as in a "pinch" gesture
  static public void compute2DTransformBasedOnDisplacementOfTwoPoints(
    // input
    Point2D A_old, Point2D B_old,
    Point2D A_new, Point2D B_new,

    // output
    Vector2D translation,
    Point2D rotationCenter,
    // Output.  Caller must pass in a 1-element array used to pass value back to caller.
    // Angle is in radians.
    // A positive value means a counterclockwise rotation in a (x+ right, y+ up) system.
    float [] rotationAngle,
    // Output.  Caller must pass in a 1-element array used to pass value back to caller.
    float [] scaleFactor
  ) {
    // Compute midpoints of each pair of points
    Point2D M1 = Point2D.average( A_old, B_old );
    Point2D M2 = Point2D.average( A_new, B_new );

    // This is the translation that the points should undergo.
    translation.copy( Point2D.diff( M2, M1 ) );

    // Compute a vector associated with each pair of points.
    Vector2D v1 = Point2D.diff( A_old, B_old );
    Vector2D v2 = Point2D.diff( A_new, B_new );

    float v1_length = v1.length();
    float v2_length = v2.length();
    scaleFactor[0] = 1;
    if ( v1_length > 0 && v2_length > 0 )
      scaleFactor[0] = v2_length / v1_length;
    rotationAngle[0] = Vector2D.computeSignedAngle( v1, v2 );

    rotationCenter.copy( M2 );
  }

  // This can be used to implement bimanual (2-handed) manipulation,
  // or 2-finger manipulation, as in a "pinch" gesture
  static public void transformPointsBasedOnDisplacementOfTwoPoints(
    ArrayList<Point2D> points,
    // these should, of course, be in the same coordinate system as the points to transform
    Point2D A_old, Point2D B_old,
    Point2D A_new, Point2D B_new
  ) {
    Vector2D translation = new Vector2D();
    Point2D rotationCenter = new Point2D();
    float [] rotationAngle = new float[1];
    float [] scaleFactor = new float[1];
    compute2DTransformBasedOnDisplacementOfTwoPoints(
      A_old, B_old, A_new, B_new,
      translation, rotationCenter, rotationAngle, scaleFactor
    );

    float cosine = (float)Math.cos(rotationAngle[0]);
    float sine = (float)Math.sin(rotationAngle[0]);

    for ( Point2D p : points ) {
      float relativeX = ( p.x() + translation.x() ) - rotationCenter.x();
      float relativeY = ( p.y() + translation.y() ) - rotationCenter.y();
      p.get()[0] = scaleFactor[0]*(cosine*relativeX - sine*relativeY) + rotationCenter.x();
      p.get()[1] = scaleFactor[0]*(sine*relativeX + cosine*relativeY) + rotationCenter.y();
    }

  }



  // Given a set of 2D orientations expressed as angles,
  // to find the average orientation,
  // we might try doing something like
  //
  //    float averageAngle = 0;
  //    int N = 0;
  //    loop {
  //       ...
  //       float angle = ...
  //       averageAngle += angle;
  //       ++ N;
  //    }
  //    averageAngle /= N;
  //
  // but this turns out to not work well.
  // For example, if there are two angles equal to
  // 10 degrees and 350 degrees, the numerical averaging above
  // will compute their average to be 180 degrees,
  // whereas the average that we should be computing is
  // 0 degrees (or, equivalently, 360 degrees).
  // To compute the average correctly, we use a "rotation vector",
  // as implemented below.
  //
  static public float computeAverageAngle(
    ArrayList< Float > angles, // angles, in radians
    float defaultAverageAngle  // average angle to return if the given angles all cancel out, or if angles.size() == 0
  ) {
    Vector2D averageRotationVector = new Vector2D();
    for ( float angle : angles ) {
      averageRotationVector = Vector2D.sum(
        averageRotationVector,
        new Vector2D( (float)Math.cos(angle), (float)Math.sin(angle) )
      );
    }
    if ( averageRotationVector.length() == 0 )
      return defaultAverageAngle;
    return averageRotationVector.angle();
  }
  static public float computeAverageAngle(
    ArrayList< Float > angles
  ) {
    return computeAverageAngle( angles, 0 );
  }

}




Java Source Code List

log350.example.example6.AlignedRectangle2D.java
log350.example.example6.DrawingView.java
log350.example.example6.GraphicsWrapper.java
log350.example.example6.Log350Example6Activity.java
log350.example.example6.MyButton.java
log350.example.example6.Point2DUtil.java
log350.example.example6.Point2D.java
log350.example.example6.ShapeContainer.java
log350.example.example6.Shape.java
log350.example.example6.Vector2D.java