package org.gbcpainter.game.view.animations;
import org.gbcpainter.geom.ImmutableSegment2D;
import org.gbcpainter.geom.MutableSegment2D;
import org.gbcpainter.geom.PERPENDICULAR_DIRECTION;
import org.gbcpainter.geom.Segment;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.*;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Implementation of the {@link org.gbcpainter.game.view.animations.PathAnimation} interface.
*
* @author Lorenzo Pellegrini
*/
public class PathAnimationImpl implements PathAnimation {
private static final double HALF_POINT = 0.5;
private static final int NOT_STARTED_TIME = 0;
private long startTime = NOT_STARTED_TIME;
private long endTime = NOT_STARTED_TIME;
@NotNull
private final java.util.Map<ImmutableSegment2D, PERPENDICULAR_DIRECTION> segmentsList = new LinkedHashMap<>();
@Nullable
private MutableSegment2D currentSegment = null;
@NotNull
private PERPENDICULAR_DIRECTION currentDirection = PERPENDICULAR_DIRECTION.DOWN;
private int currentLength = 0;
private boolean firstPoint = true;
private int totalLength = 0;
/**
* Creates a void path
*/
public PathAnimationImpl() {
super();
}
private static boolean contains( @NotNull Line2D segment, @NotNull Point2D point ) {
if ( segment.getX2() == segment.getX1() ) {
final double minY = Math.min( segment.getY2(), segment.getY1() );
final double maxY = Math.max( segment.getY2(), segment.getY1() );
if ( ( point.getX() != segment.getX1() ) || point.getY() > maxY || point.getY() < minY ) {
return false;
}
} else {
final double minX = Math.min( segment.getX2(), segment.getX1() );
final double maxX = Math.max( segment.getX2(), segment.getX1() );
if ( ( point.getY() != segment.getY1() ) || point.getX() > maxX || point.getX() < minX ) {
return false;
}
}
return true;
}
private static boolean isVertex( @NotNull Line2D segment, @NotNull Point2D point ) {
return segment.getP1().equals( point ) || segment.getP2().equals( point );
}
@NotNull
private static Point getStartPoint( @NotNull Segment segment, @NotNull PERPENDICULAR_DIRECTION direction ) {
if ( ! segment.isPerpendicular() ) {
throw new IllegalArgumentException();
}
switch ( direction ) {
case LEFT:
return new Point( segment.getMaxX(), segment.getYA() );
case RIGHT:
return new Point( segment.getMinX(), segment.getYA() );
case UP:
return new Point( segment.getXA(), segment.getMaxY() );
case DOWN:
return new Point( segment.getXA(), segment.getMinY() );
default:
throw new AssertionError( "Invalid enum constant " + direction );
}
}
@NotNull
private static Point2D getEndPoint( @NotNull Line2D segment, @NotNull PERPENDICULAR_DIRECTION direction ) {
if ( ! ( ( segment.getX1() == segment.getX2() ) || ( segment.getY1() == segment.getY2() ) ) ) {
throw new IllegalArgumentException();
}
switch ( direction ) {
case LEFT:
return new Point2D.Double( Math.min( segment.getX1(), segment.getX2() ), segment.getY1() );
case RIGHT:
return new Point2D.Double( Math.max( segment.getX1(), segment.getX2() ), segment.getY1() );
case UP:
return new Point2D.Double( segment.getX1(), Math.min( segment.getY1(), segment.getY2() ) );
case DOWN:
return new Point2D.Double( segment.getX1(), Math.max( segment.getY1(), segment.getY2() ) );
default:
throw new AssertionError( "Invalid enum constant " + direction );
}
}
private static double getPercentageOfNonStartPoint( @NotNull Segment segment,
@NotNull PERPENDICULAR_DIRECTION direction,
double movementLength, @NotNull Point2D point ) {
final Line2D directionSegment = walkSegment( getStartPoint( segment, direction ),
direction,
movementLength );
final Point2D endVertex = getEndPoint( directionSegment, direction );
switch ( direction ) {
case LEFT:
if ( endVertex.getX() > point.getX() + HALF_POINT ) {
return 0.0;
}
return Math.min( ( point.getX() + HALF_POINT ) - endVertex.getX(), 1.0 );
case RIGHT:
if ( endVertex.getX() < point.getX() - HALF_POINT ) {
return 0.0;
}
return Math.min( endVertex.getX() - point.getX() + HALF_POINT, 1.0 );
case UP:
if ( endVertex.getY() > point.getY() + HALF_POINT ) {
return 0.0;
}
return Math.min( ( point.getY() + HALF_POINT ) - endVertex.getY(), 1.0 );
case DOWN:
if ( endVertex.getY() < point.getY() - HALF_POINT ) {
return 0.0;
}
return Math.min( endVertex.getY() - point.getY() + HALF_POINT, 1.0 );
default:
throw new AssertionError( "Invalid enum constant " + direction );
}
}
private static double getPercentageOfStartPoint( @NotNull Segment segment, @NotNull PERPENDICULAR_DIRECTION direction, double movementLength, @NotNull Point2D point ) {
//A start point can only be walked in its 0-50% length
final Line2D directionSegment = walkSegment( getStartPoint( segment, direction ),
direction,
movementLength );
final Point2D endVertex = getEndPoint( directionSegment, direction );
switch ( direction ) {
case LEFT:
if ( endVertex.getX() >= point.getX() ) {
return 0.0;
}
return Math.min( point.getX() - endVertex.getX(), 0.5 );
case RIGHT:
if ( endVertex.getX() <= point.getX() ) {
return 0.0;
}
return Math.min( endVertex.getX() - point.getX(), 0.5 );
case UP:
if ( endVertex.getY() >= point.getY() ) {
return 0.0;
}
return Math.min( point.getY() - endVertex.getY(), 0.5 );
case DOWN:
if ( endVertex.getY() <= point.getY() ) {
return 0.0;
}
return Math.min( endVertex.getY() - point.getY(), 0.5 );
default:
throw new AssertionError( "Invalid enum constant " + direction );
}
}
/**
* Utility method that returns the segment walked given a start point, direction and length
*
* @param startPosition The starting position
* @param direction The direction
* @param movement The length of the segment
*
* @return A segment with the given start point, direction and length
*/
@NotNull
private static Line2D walkSegment( @NotNull Point2D startPosition,
@NotNull PERPENDICULAR_DIRECTION direction,
double movement ) {
switch ( direction ) {
case LEFT:
return new Line2D.Double( startPosition.getX() - movement, startPosition.getY(), startPosition.getX(), startPosition.getY() );
case RIGHT:
return new Line2D.Double( startPosition.getX(), startPosition.getY(), startPosition.getX() + movement, startPosition.getY() );
case UP:
return new Line2D.Double( startPosition.getX(), startPosition.getY() - movement, startPosition.getX(), startPosition.getY() );
case DOWN:
return new Line2D.Double( startPosition.getX(), startPosition.getY(), startPosition.getX(), startPosition.getY() + movement );
default:
throw new AssertionError( "Invalid enum constant " + direction );
}
}
/**
* Utility method that returns the length of a segment, using the specified direction
* <p/>
* This can be used to get the X or Y component of a vector.
*
* @param originalSegment The segment
* @param direction The direction
*
* @return The length of the X or Y component of the segment
*/
private static double getLength( @NotNull Segment originalSegment, @NotNull PERPENDICULAR_DIRECTION direction ) {
switch ( direction ) {
case LEFT:
case RIGHT:
return originalSegment.getMaxX() - originalSegment.getMinX();
case UP:
case DOWN:
return originalSegment.getMaxY() - originalSegment.getMinY();
default:
throw new AssertionError( "Invalid enum constant " + direction );
}
}
/**
* Returns the intersection between 2 parallel segments that have one intersecting point at
* least
*
* @param donePart The first segment
* @param originalSegment The second segment
* @param direction The direction of the segments
*
* @return The intersection of the 2 segments
*/
@NotNull
private static Line2D getIntersection( @NotNull Line2D donePart, @NotNull Segment originalSegment, @NotNull PERPENDICULAR_DIRECTION direction ) {
switch ( direction ) {
case LEFT:
case RIGHT:
final double maxX = Math.max( donePart.getX1(), donePart.getX2() );
final double minX = Math.min( donePart.getX1(), donePart.getX2() );
final double segmentY = originalSegment.getYA();
return new Line2D.Double( Math.max( minX, originalSegment.getMinX() ), segmentY, Math.min( maxX, originalSegment.getMaxX() ), segmentY );
case UP:
case DOWN:
final double maxY = Math.max( donePart.getY1(), donePart.getY2() );
final double minY = Math.min( donePart.getY1(), donePart.getY2() );
final double segmentX = originalSegment.getXA();
return new Line2D.Double( segmentX, Math.max( minY, originalSegment.getMinY() ), segmentX, Math.min( maxY, originalSegment.getMaxY() ) );
default:
throw new AssertionError( "Invalid enum constant " + direction );
}
}
@NotNull
@Override
public Segment startNewPart( @NotNull final PERPENDICULAR_DIRECTION direction ) throws IllegalStateException {
this.checkNotStarted();
if ( currentSegment != null ) {
segmentsList.put( new ImmutableSegment2D( (Segment) currentSegment ), currentDirection );
currentSegment = new MutableSegment2D( currentSegment.getXB(), currentSegment.getYB(), currentSegment.getXB(), currentSegment.getYB() );
totalLength += currentLength;
} else {
currentSegment = new MutableSegment2D( 0, 0, 0, 0 );
}
currentLength = 0;
currentDirection = direction;
return currentSegment;
}
@Override
public void addPoint( @NotNull final Point point ) throws IllegalArgumentException, IllegalStateException {
checkNotStarted();
if ( currentSegment == null ) {
throw new IllegalStateException();
}
if ( segmentsList.isEmpty() && firstPoint ) {
firstPoint = false;
currentSegment.setA( point );
} else {
switch ( currentDirection ) {
case LEFT:
if ( point.y != currentSegment.getA().y || point.x > currentSegment.getB().x ) {
throw new IllegalArgumentException("Point in not at the previously " +
"defined direction and line");
} else {
currentLength = currentSegment.getA().x - point.x;
}
break;
case RIGHT:
if ( point.y != currentSegment.getA().y || point.x < currentSegment.getB().x ) {
throw new IllegalArgumentException("Point in not at the previously " +
"defined direction and line");
} else {
currentLength = point.x - currentSegment.getA().x;
}
break;
case UP:
if ( point.x != currentSegment.getA().x || point.y > currentSegment.getB().y ) {
throw new IllegalArgumentException("Point in not at the previously " +
"defined direction and line");
} else {
currentLength = currentSegment.getA().y - point.y;
}
break;
case DOWN:
if ( point.x != currentSegment.getA().x || point.y < currentSegment.getB().y ) {
throw new IllegalArgumentException("Point in not at the previously " +
"defined direction and line");
} else {
currentLength = point.y - currentSegment.getA().y;
}
break;
}
}
currentSegment.setB( point );
}
@NotNull
@Override
public PERPENDICULAR_DIRECTION getNewPartDirection() throws IllegalStateException {
this.checkNotStarted();
if ( this.firstPoint ) {
throw new IllegalStateException( "First part not created" );
}
return this.currentDirection;
}
@NotNull
@Override
public Segment getNewPart() throws IllegalStateException {
if ( this.currentSegment == null ) {
/* A.K.A. if(this.firstPoint), used because of nullity annotations */
throw new IllegalStateException( "First part not created" );
}
return this.currentSegment;
}
@Override
public void startAnimation( final long millisStart, final long millisEnd ) throws IllegalStateException {
this.checkNotStarted();
if ( this.currentSegment != null ) {
this.segmentsList.put( new ImmutableSegment2D( (Segment) this.currentSegment ), this.currentDirection );
this.totalLength += this.currentLength;
this.currentSegment = null;
} else {
if ( this.segmentsList.isEmpty() ) {
throw new IllegalStateException( "Empty path" );
}
}
this.startTime = millisStart;
this.endTime = millisEnd;
}
@Override
@Nullable
public Line2D getDoneAnimation( @NotNull Segment originalSegment ) throws IllegalArgumentException, IllegalStateException {
this.checkStarted();
this.checkExists( new ImmutableSegment2D( originalSegment ) );
final long millisTime = System.currentTimeMillis();
if ( this.isTerminated() ) {
return originalSegment.getLine2D();
}
double movementLength = this.getMovementLength( millisTime );
/*
Iterates over the path and searches for the segment
If the segment was not touched by the cursor return null,
else return the walked part
*/
for (Map.Entry<ImmutableSegment2D, PERPENDICULAR_DIRECTION> part : this.segmentsList.entrySet()) {
final Segment actualSegment = part.getKey();
final PERPENDICULAR_DIRECTION direction = part.getValue();
if ( actualSegment.equals( originalSegment ) ) {
/*
direction segment is a segment that may be longer than the parameter segment,
see walkSegment for details.
We need to intersect it with the parameter segment
*/
Line2D directionSegment = walkSegment( getStartPoint( actualSegment, direction ),
direction,
movementLength );
return getIntersection( directionSegment, actualSegment, direction );
} else {
movementLength -= getLength( actualSegment, direction );
}
if ( movementLength <= 0.0 ) {
/*
Segment was not reached yet, return null
*/
break;
}
}
return null;
}
@NotNull
@Override
public Segment getCurrentPart() throws IllegalStateException, TerminatedAnimationException {
this.checkStarted();
final long millisTime = System.currentTimeMillis();
if ( this.isTerminated() ) {
throw new TerminatedAnimationException();
}
double movementLength = this.getMovementLength( millisTime );
Segment resultPart = null;
//Iterate the path and return the path segment the cursor is over
for (Map.Entry<ImmutableSegment2D, PERPENDICULAR_DIRECTION> part : this.segmentsList.entrySet()) {
final Segment actualSegment = part.getKey();
final PERPENDICULAR_DIRECTION direction = part.getValue();
resultPart = actualSegment;
movementLength -= getLength( actualSegment, direction );
if ( movementLength <= 0.0 ) {
//The cursor is over the actualSegment
break;
}
}
assert resultPart != null;
return resultPart;
}
@NotNull
@Override
public Point2D getActualPosition() throws IllegalStateException, TerminatedAnimationException {
this.checkStarted();
final long millisTime = System.currentTimeMillis();
if ( this.isTerminated() ) {
throw new TerminatedAnimationException();
}
double movementLength = this.getMovementLength( millisTime );
Point2D resultPoint = null;
//Iterate over the path and return the actual position
for (Map.Entry<ImmutableSegment2D, PERPENDICULAR_DIRECTION> part : this.segmentsList.entrySet()) {
final Segment actualSegment = part.getKey();
final PERPENDICULAR_DIRECTION direction = part.getValue();
movementLength -= getLength( actualSegment, direction );
if ( movementLength <= 0.0 ) {
//restore previous state
movementLength += getLength( actualSegment, direction );
//walk the candidate segment
final Line2D donePiece = walkSegment( getStartPoint(
actualSegment, direction ), direction, movementLength );
resultPoint = getEndPoint( donePiece, direction );
break;
}
}
assert resultPoint != null;
return resultPoint;
}
@Override
public double getPercentageDoneAnimation( @NotNull final Point anyPathPoint ) throws IllegalStateException {
this.checkStarted();
final long millisTime = System.currentTimeMillis();
if ( millisTime >= this.endTime ) {
return 1.0;
}
double movementLength = this.getMovementLength( millisTime );
Map.Entry<ImmutableSegment2D, PERPENDICULAR_DIRECTION> firstSegment = this.segmentsList.entrySet().iterator().next();
if(getStartPoint( firstSegment.getKey(), firstSegment.getValue() ).equals( anyPathPoint )) {
//It is the start Point!
return getPercentageOfStartPoint( firstSegment.getKey(),
firstSegment.getValue(),
movementLength,
anyPathPoint ) * 2;
}
Iterator<Map.Entry<ImmutableSegment2D, PERPENDICULAR_DIRECTION>> iterator = this.segmentsList.entrySet().iterator();
/*
Iterates over the path, if the actual segment contains the wanted point
then checks if the point is the extremity if the segment.
If it is the extremity percentage is calculated as percentage
before segment end + percentage after next segment start.
If is a point inside a segment, getPercentageOfNonStartPoint() is used
*/
while ( iterator.hasNext() ) {
final Map.Entry<ImmutableSegment2D, PERPENDICULAR_DIRECTION> part = iterator.next();
final Segment actualSegment = part.getKey();
final PERPENDICULAR_DIRECTION direction = part.getValue();
if ( contains( actualSegment.getLine2D(), anyPathPoint ) ) {
if ( ! isVertex( actualSegment.getLine2D(), anyPathPoint ) ) {
return getPercentageOfNonStartPoint( actualSegment, direction, movementLength, anyPathPoint );
} else {
final double beforeLineChange = Math.min( getPercentageOfNonStartPoint( actualSegment, direction, movementLength, anyPathPoint ), 0.5 );
if ( beforeLineChange == 0.5 ) {
movementLength -= getLength( actualSegment, direction );
if ( iterator.hasNext() ) {
final Map.Entry<ImmutableSegment2D, PERPENDICULAR_DIRECTION> nextEntry = iterator.next();
final Segment nextSegment = nextEntry.getKey();
final PERPENDICULAR_DIRECTION nextSegmentDirection = nextEntry.getValue();
final double afterLineChange = getPercentageOfStartPoint( nextSegment, nextSegmentDirection, movementLength, anyPathPoint );
return afterLineChange + beforeLineChange;
} else {
//It is the end of the path
return 1.0;
}
} else {
if ( ! iterator.hasNext() ) {
//It is the end point of the path, percentage is doubled
return beforeLineChange * 2;
}
return beforeLineChange;
}
}
}
movementLength -= getLength( actualSegment, direction );
if ( movementLength <= 0.0 ) {
return 0.0;
}
}
throw new IllegalArgumentException("Point not in the path");
}
@NotNull
@Override
public PERPENDICULAR_DIRECTION getDirection( @NotNull final Segment originalSegment ) throws IllegalArgumentException {
ImmutableSegment2D immutableCopy = new ImmutableSegment2D( originalSegment );
this.checkExists( immutableCopy );
return this.segmentsList.get( immutableCopy );
}
@NotNull
@Override
public PERPENDICULAR_DIRECTION getBeforeDirection( @NotNull final Point anyPathPoint ) throws IllegalArgumentException {
for (Map.Entry<ImmutableSegment2D, PERPENDICULAR_DIRECTION> part : this.segmentsList.entrySet()) {
final Segment actualSegment = part.getKey();
final PERPENDICULAR_DIRECTION direction = part.getValue();
if ( contains( actualSegment.getLine2D(), anyPathPoint ) ) {
return direction;
}
}
throw new IllegalArgumentException("Point not in the path (" + anyPathPoint + ")");
}
@Nullable
@Override
public PERPENDICULAR_DIRECTION getAfterDirection( @NotNull final Point anyPathPoint ) throws IllegalArgumentException {
Iterator<Map.Entry<ImmutableSegment2D, PERPENDICULAR_DIRECTION>> iterator = this.segmentsList.entrySet().iterator();
/*
Iterate the path, if the point is in the middle of a segment,
return the segment direction. If it is an extremity of a segment, return the
direction of the next segment or null if it is the end of path point.
*/
while ( iterator.hasNext() ) {
final Map.Entry<ImmutableSegment2D, PERPENDICULAR_DIRECTION> part = iterator.next();
final Segment actualSegment = part.getKey();
final PERPENDICULAR_DIRECTION direction = part.getValue();
if ( contains( actualSegment.getLine2D(), anyPathPoint ) ) {
if ( isVertex( actualSegment.getLine2D(), anyPathPoint ) ) {
if ( iterator.hasNext() ) {
//Not the end of the path
return iterator.next().getValue();
} else {
//The end of the path, no 'next' direction
return null;
}
} else {
return direction;
}
}
}
throw new IllegalArgumentException("Point not in the path");
}
@Override
public void terminate() throws IllegalStateException {
this.checkStarted();
this.endTime = this.startTime;
}
@Override
public boolean isTerminated() {
return this.isStarted() && System.currentTimeMillis() >= this.endTime;
}
@Override
public boolean isStarted() {
return this.startTime != NOT_STARTED_TIME;
}
private void checkNotStarted() throws IllegalStateException {
if ( this.isStarted() ) {
throw new IllegalStateException();
}
}
private void checkStarted() throws IllegalStateException {
if ( ! this.isStarted() ) {
throw new IllegalStateException();
}
}
private double getMovementLength( final long millisTime ) {
return this.totalLength
* ( (double) ( millisTime - this.startTime ) / ( this.endTime - this.startTime ) );
}
private void checkExists( @NotNull ImmutableSegment2D segment ) throws IllegalArgumentException {
if ( ! this.segmentsList.containsKey( segment ) ) {
throw new IllegalArgumentException();
}
}
@Override
@NonNls
public String toString() {
return "PathAnimationImpl{" +
"segmentsList=" + this.segmentsList +
", currentSegment=" + this.currentSegment +
", currentDirection=" + this.currentDirection +
", currentLength=" + this.currentLength +
", firstPoint=" + this.firstPoint +
", startTime=" + this.startTime +
", endTime=" + this.endTime +
", totalLength=" + this.totalLength +
'}';
}
}