package org.gbcpainter.game.model.grid;
import net.jcip.annotations.GuardedBy;
import org.gbcpainter.env.GraphicsEnv;
import org.gbcpainter.loaders.textures.TextureNotFoundException;
import org.gbcpainter.game.view.animations.PipeColoringAnimation;
import org.gbcpainter.geom.ImmutableSegment2D;
import org.gbcpainter.geom.PERPENDICULAR_DIRECTION;
import org.gbcpainter.geom.Segment;
import org.gbcpainter.loaders.textures.TextureLoader;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import java.awt.*;
import java.awt.geom.Line2D;
import java.util.*;
/**
* Implementation of the {@link org.gbcpainter.game.model.grid.Pipe} interface
* <p/>
* Colored parts are saved as a couple of integer (start - end) instead of the state of the single points in order to save memory space.
*
* @author Lorenzo Pellegrini
*/
public final class PipeImpl implements Pipe {
@NonNls
private static final String NON_COLORED_PART_HORIZONTAL = "Pipe_nocolor_horizontal";
@NonNls
private static final String NON_COLORED_PART_VERTICAL = "Pipe_nocolor_vertical";
@NonNls
private static final String COLORED_PART_HORIZONTAL = "Pipe_color_horizontal";
@NonNls
private static final String COLORED_PART_VERTICAL = "Pipe_color_vertical";
@NonNls
private static final String COLORED_SLICE_HORIZONTAL = "Colored_pipe_horizontal";
@NonNls
private static final String NON_COLORED_SLICE_HORIZONTAL = "Not_colored_pipe_horizontal";
@NonNls
private static final String COLORED_SLICE_VERTICAL = "Colored_pipe_vertical";
@NonNls
private static final String NON_COLORED_SLICE_VERTICAL = "Not_colored_pipe_vertical";
@NotNull
private final Segment segment;
@NotNull
@GuardedBy ( "this" )
private final Collection<PipeColoringAnimation> coloringAnimations = new HashSet<>( 2 );
@NotNull
@GuardedBy ( "this" )
private final NavigableSet<PipePart> colorParts = new TreeSet<>();
@NotNull
@GuardedBy ( "this" )
private final NavigableMap<Integer, Boolean> changedParts = new TreeMap<>();
private final boolean voidPipe;
private final boolean vertical;
/**
* Creates a non colored pipe at the position defined by a segment
*
* @param segment The segment that defines the position of the pipe
*
* @throws IllegalArgumentException If the segment is not perpendicular to x/y axes
* @throws Exception If an error occurs while loading textures
* @throws org.gbcpainter.loaders.textures.InvalidTextureException If the base textures haven't the same size of the base point
*/
public PipeImpl( @NotNull Segment segment ) throws Exception {
if ( ! segment.isPerpendicular() ) {
throw new IllegalArgumentException( "Segment is not parallel to X|Y axes" );
}
if ( segment.getA().equals( segment.getB() ) ) {
throw new IllegalArgumentException( "Segment is a point" );
}
if ( isVertical( segment ) ) {
this.vertical = true;
if(segment.getMinY() == (segment.getMaxY()-1)) {
this.voidPipe = true;
this.segment = new ImmutableSegment2D( segment.getXA(), segment.getYA(),
segment.getXB(), segment.getYB());
} else {
this.voidPipe = false;
this.segment = new ImmutableSegment2D( segment.getXA(), segment.getMinY() + 1,
segment.getXA(), segment.getMaxY() - 1 );
}
} else {
this.vertical = false;
if(segment.getMinX() == (segment.getMaxX()-1)) {
this.voidPipe = true;
this.segment = new ImmutableSegment2D( segment.getXA(), segment.getYA(),
segment.getXB(), segment.getYB());
} else {
this.voidPipe = false;
this.segment = new ImmutableSegment2D( segment.getMinX() + 1, segment.getYA(),
segment.getMaxX() - 1, segment.getYA() );
}
}
final TextureLoader loader = GraphicsEnv.getInstance().getTextureLoader();
//final int baseDimension = GraphicsEnv.getInstance().getBasePointDimension();
if(!this.voidPipe) {
if ( isVertical( segment ) ) {
loader.loadTexture( NON_COLORED_PART_VERTICAL, true );
/*if ( loader.getBaseDimension( NON_COLORED_PART_VERTICAL ).height !=
baseDimension ) {
throw new InvalidTextureException( "Pipe texture " + NON_COLORED_PART_VERTICAL + " has an invalid size!" );
}*/
loader.loadTexture( COLORED_PART_VERTICAL, true );
/*if ( loader.getBaseDimension( COLORED_PART_VERTICAL ).height != baseDimension ) {
throw new InvalidTextureException( "Pipe texture " + COLORED_PART_VERTICAL + " has an invalid size!" );
}*/
loader.loadTexture( COLORED_SLICE_VERTICAL, true );
loader.loadTexture( NON_COLORED_SLICE_VERTICAL, true );
} else {
loader.loadTexture( NON_COLORED_PART_HORIZONTAL, true );
/*if ( loader.getBaseDimension( NON_COLORED_PART_HORIZONTAL ).width !=
baseDimension ) {
throw new InvalidTextureException( "Pipe texture " + NON_COLORED_PART_HORIZONTAL + " has an invalid size!" );
}*/
loader.loadTexture( COLORED_PART_HORIZONTAL, true );
/*if ( loader.getBaseDimension( COLORED_PART_HORIZONTAL ).width != baseDimension ) {
throw new InvalidTextureException( "Pipe texture " + COLORED_PART_HORIZONTAL + " has an invalid size!" );
}*/
loader.loadTexture( COLORED_SLICE_HORIZONTAL, true );
loader.loadTexture( NON_COLORED_SLICE_HORIZONTAL, true );
}
}
}
/**
* Utility method that check if a segment if vertical
*
* @param toBeTested The segment to be checked
*
* @return true if the segment is vertical
*/
private static boolean isVertical( @NotNull Segment toBeTested ) {
return toBeTested.getXA() == toBeTested.getXB();
}
/**
* Gets the position of the pipe
*
* @return The position of the pipe
*/
@NotNull
@Override
public Segment getSegment() {
return new ImmutableSegment2D( this.segment );
}
@Override
public synchronized void setColored( @NotNull Point point, boolean colored ) throws IllegalArgumentException {
if(this.isVoidPipe()) {
return;
}
final PipePart part;
if ( ! this.contains( point ) ) {
throw new IllegalArgumentException( "Point is not inside the pipe segment" );
}
if ( this.isVertical() ) {
part = new PipePart( point.y, point.y );
if ( ! this.changedParts.containsKey( point.y )
&& colored != this.isColoredAt( point.y )) {
this.changedParts.put( point.y, ! colored );
}
} else {
part = new PipePart( point.x, point.x );
if ( ! this.changedParts.containsKey( point.x )
&& colored != this.isColoredAt( point.x )) {
this.changedParts.put( point.x, !colored );
}
}
if ( colored ) {
this.doAddAndMerge( part );
} else {
this.doErase( part );
}
}
public synchronized boolean isColoredAt( @NotNull Point where ) {
if ( this.isVertical() ) {
if ( where.x != this.segment.getXA() ) {
throw new IllegalArgumentException();
}
if(this.isVoidPipe()) {
return true;
} else {
return this.isColoredAt( where.y );
}
} else {
if ( where.y != this.segment.getYA() ) {
throw new IllegalArgumentException( "Point not in the pipe: " + where + "vs" +
this.getSegment());
}
if(this.isVoidPipe()) {
return true;
} else {
return this.isColoredAt( where.x );
}
}
}
@Override
public synchronized void applyAnimations( @NotNull final Collection<PipeColoringAnimation> animations ) {
if(!this.isVoidPipe()) {
this.coloringAnimations.clear();
if ( animations.isEmpty() ) {
this.changedParts.clear();
} else {
this.coloringAnimations.addAll( animations );
}
}
}
@Override
public synchronized void clearAnimations() {
this.changedParts.clear();
this.coloringAnimations.clear();
}
@Override
public synchronized void reset() {
this.clearAnimations();
this.colorParts.clear();
}
@Override
public synchronized void draw( @NotNull final Graphics2D g2 ) throws Exception {
if(this.isVoidPipe()) {
return;
}
final Iterable<PipePart> parts = new TreeSet<>( this.colorParts );
final TextureLoader loader = GraphicsEnv.getInstance().getTextureLoader();
final Segment pipeSegment = this.getSegment();
final Dimension textureDimension;
int gamePosition;
final int upperLimit;
/*
Stampa le texure su schermo.
Game position mantiene la posizione matematica della tubatura (model), e andrà dall'ascissa dell'estremo
più a sinistra se la tubatura è orizzontale oppure dall'ordinata dell'estremo superiore se verticale.
Upperlimit è impostato al limite opposto.
Viene prima dipinta la parte non colorata, poi la parte colorata.
screenPosition mantiene la posizione che deve avere la texture su schermo.
screenPosition viene aggiornato a ogni pezzo di texture come un cursore.
Questa parte riguarda il disegno delle sole parti non modificate in questo step.
*/
Rectangle screenRect;
Point screenPosition;
final Iterator<Map.Entry<Integer, Boolean>> changedIterator = this.changedParts.entrySet()
.iterator();
Map.Entry<Integer, Boolean> nextSkip = changedIterator.hasNext() ? changedIterator.next() : null;
if ( this.isVertical() ) {
textureDimension = loader.getDimension( NON_COLORED_PART_VERTICAL );
gamePosition = pipeSegment.getMinY();
upperLimit = pipeSegment.getMaxY();
for (final PipePart part : parts) {
for (; gamePosition <= upperLimit && gamePosition < part.getStart(); gamePosition++) {
screenRect = GraphicsEnv.getInstance().gamePointAndTextureToScreen( new Point( pipeSegment.getXA(), gamePosition ), textureDimension );
screenPosition = new Point( screenRect.x, screenRect.y );
final String pickTexture;
if ( nextSkip != null && nextSkip.getKey() == gamePosition ) {
pickTexture = ( nextSkip.getValue() ) ? COLORED_PART_VERTICAL : NON_COLORED_PART_VERTICAL;
nextSkip = changedIterator.hasNext() ? changedIterator.next() : null;
} else {
pickTexture = NON_COLORED_PART_VERTICAL;
}
this.doDraw( g2, pickTexture, screenPosition );
}
for (; gamePosition <= part.getEnd(); gamePosition++) {
screenRect = GraphicsEnv.getInstance().gamePointAndTextureToScreen( new Point( pipeSegment.getXA(), gamePosition ), textureDimension );
screenPosition = new Point( screenRect.x, screenRect.y );
final String pickTexture;
if ( nextSkip != null && nextSkip.getKey() == gamePosition ) {
pickTexture = ( nextSkip.getValue() ) ? COLORED_PART_VERTICAL : NON_COLORED_PART_VERTICAL;
nextSkip = changedIterator.hasNext() ? changedIterator.next() : null;
} else {
pickTexture = COLORED_PART_VERTICAL;
}
this.doDraw( g2, pickTexture, screenPosition );
}
}
for (; gamePosition <= upperLimit; gamePosition++) {
screenRect = GraphicsEnv.getInstance().gamePointAndTextureToScreen( new Point( pipeSegment.getXA(), gamePosition ), textureDimension );
screenPosition = new Point( screenRect.x, screenRect.y );
final String pickTexture;
if ( nextSkip != null && nextSkip.getKey() == gamePosition ) {
pickTexture = ( nextSkip.getValue() ) ? COLORED_PART_VERTICAL : NON_COLORED_PART_VERTICAL;
nextSkip = changedIterator.hasNext() ? changedIterator.next() : null;
} else {
pickTexture = NON_COLORED_PART_VERTICAL;
}
this.doDraw( g2, pickTexture, screenPosition );
}
} else {
textureDimension = loader.getDimension( NON_COLORED_PART_HORIZONTAL );
gamePosition = pipeSegment.getMinX();
upperLimit = pipeSegment.getMaxX();
for (final PipePart part : parts) {
for (; gamePosition <= upperLimit && gamePosition < part.getStart(); gamePosition++) {
screenRect = GraphicsEnv.getInstance().gamePointAndTextureToScreen( new Point( gamePosition, pipeSegment.getYA() ), textureDimension );
screenPosition = new Point( screenRect.x, screenRect.y );
final String pickTexture;
if ( nextSkip != null && nextSkip.getKey() == gamePosition ) {
pickTexture = ( nextSkip.getValue() ) ? COLORED_PART_HORIZONTAL : NON_COLORED_PART_HORIZONTAL;
nextSkip = changedIterator.hasNext() ? changedIterator.next() : null;
} else {
pickTexture = NON_COLORED_PART_HORIZONTAL;
}
this.doDraw( g2, pickTexture, screenPosition );
}
for (; gamePosition <= part.getEnd(); gamePosition++) {
screenRect = GraphicsEnv.getInstance().gamePointAndTextureToScreen( new Point( gamePosition, pipeSegment.getYA() ), textureDimension );
screenPosition = new Point( screenRect.x, screenRect.y );
final String pickTexture;
if ( nextSkip != null && nextSkip.getKey() == gamePosition ) {
pickTexture = ( nextSkip.getValue() ) ? COLORED_PART_HORIZONTAL : NON_COLORED_PART_HORIZONTAL;
nextSkip = changedIterator.hasNext() ? changedIterator.next() : null;
} else {
pickTexture = COLORED_PART_HORIZONTAL;
}
this.doDraw( g2, pickTexture, screenPosition );
}
}
for (; gamePosition <= upperLimit; gamePosition++) {
screenRect = GraphicsEnv.getInstance().gamePointAndTextureToScreen( new Point( gamePosition, pipeSegment.getYA() ), textureDimension );
screenPosition = new Point( screenRect.x, screenRect.y );
final String pickTexture;
if ( nextSkip != null && nextSkip.getKey() == gamePosition ) {
pickTexture = ( nextSkip.getValue() ) ? COLORED_PART_HORIZONTAL : NON_COLORED_PART_HORIZONTAL;
nextSkip = changedIterator.hasNext() ? changedIterator.next() : null;
} else {
pickTexture = NON_COLORED_PART_HORIZONTAL;
}
this.doDraw( g2, pickTexture, screenPosition );
}
}
assert ! changedIterator.hasNext();
/*
Draws the modified parts on screen
Modified parts are drawn as slices from the lower coordinate (leftmost X coordinate
is horizontal, upper Y coordinate if vertical) to the upper one of the animation part
*/
if ( ! this.coloringAnimations.isEmpty() ) {
boolean remainingAnimations = false;
if ( this.isVertical() ) {
for (PipeColoringAnimation animation : this.coloringAnimations) {
final Line2D donePart = animation.getDonePart();
if ( donePart == null ) {
continue;
}
final boolean colored = animation.isColoring();
@NonNls
final String selectedTexture = colored ? COLORED_SLICE_VERTICAL : NON_COLORED_SLICE_VERTICAL;
final Dimension sliceDimension = loader.getDimension( selectedTexture );
final Rectangle screenRealPosition = GraphicsEnv.getInstance().gamePointAndTextureToScreen( donePart.getP1(), sliceDimension );
final Rectangle screenRealPositionUpper = GraphicsEnv.getInstance().gamePointAndTextureToScreen( donePart.getP2(), sliceDimension );
final int lowerY = Math.min( screenRealPosition.y, screenRealPositionUpper.y );
final int upperY = Math.max( screenRealPosition.y, screenRealPositionUpper.y );
Point cursor = new Point( screenRealPosition.x, lowerY );
for (; cursor.y < upperY; cursor.y++) {
this.doDraw( g2, selectedTexture, cursor );
}
if ( ! animation.isTerminated() ) {
remainingAnimations = true;
}
}
} else {
for (PipeColoringAnimation animation : this.coloringAnimations) {
final Line2D donePart = animation.getDonePart();
if ( donePart == null ) {
continue;
}
final boolean colored = animation.isColoring();
@NonNls
final String selectedTexture = colored ? COLORED_SLICE_HORIZONTAL : NON_COLORED_SLICE_HORIZONTAL;
final Dimension sliceDimension = loader.getDimension( selectedTexture );
final Rectangle screenRealPosition = GraphicsEnv.getInstance().gamePointAndTextureToScreen( donePart.getP1(), sliceDimension );
final Rectangle screenRealPositionUpper = GraphicsEnv.getInstance().gamePointAndTextureToScreen( donePart.getP2(), sliceDimension );
final int lowerX = Math.min( screenRealPosition.x, screenRealPositionUpper.x );
final int upperX = Math.max( screenRealPosition.x, screenRealPositionUpper.x );
Point cursor = new Point( lowerX, screenRealPosition.y );
for (; cursor.x < upperX; cursor.x++) {
this.doDraw( g2, selectedTexture, cursor );
}
if ( ! animation.isTerminated() ) {
remainingAnimations = true;
}
}
}
if ( ! remainingAnimations ) {
this.clearAnimations();
}
}
}
/**
* Utility method that adds a part of the pipe to the colored pipe parts data structure
*
* @param pivot The part to add
*/
@GuardedBy ( "this" )
private void doAddAndMerge( @NotNull PipePart pivot ) {
final PipePart lower = this.colorParts.floor( pivot );
if ( lower == null || ( lower.getEnd() + 1 ) < pivot.getStart() ) {
this.colorParts.add( pivot );
} else {
if ( lower.getEnd() >= pivot.getEnd() ) {
/* L'elemento esistente contiene il pivot */
return;
} else {
lower.setEnd( pivot.getEnd() );
}
pivot = lower;
}
final NavigableSet<PipePart> upperPart = this.colorParts.tailSet( pivot, false );
final Iterator<PipePart> goingUp = upperPart.iterator();
while ( goingUp.hasNext() ) {
final PipePart upper = goingUp.next();
if ( upper.getEnd() <= pivot.getEnd() ) {
goingUp.remove();
} else if ( upper.getStart() <= ( pivot.getEnd() + 1 ) ) {
pivot.setEnd( upper.getEnd() );
goingUp.remove();
break;
} else {
break;
}
}
}
/**
* Utility method that erases a part of the pipe from the colored pipe parts data structure
*
* @param pivot The part to erase
*/
@GuardedBy ( "this" )
private void doErase( @NotNull PipePart pivot ) {
final PipePart lower = this.colorParts.floor( pivot );
if ( lower != null ) {
if ( lower.getEnd() >= pivot.getStart() ) {
/* Se l'elemento sotto interseca il pivot... */
if ( lower.getStart() == pivot.getStart() ) {
/* Se iniziano allo stesso punto */
if ( lower.getEnd() <= pivot.getEnd() ) {
/* Se l'elemento sotto è completamente contenuto nel pivot */
this.colorParts.remove( lower );
} else {
/* Se l'elemento sotto è più lungo del pivot */
lower.setStart( pivot.getEnd() + 1 );
}
} else {
/* Se l'elemento sotto ha l'inizio più in basso */
if ( lower.getEnd() > pivot.getEnd() ) {
/* Se l'elemento sotto viene tagliato in 2 parti */
this.colorParts.add( new PipePart( pivot.getEnd(), lower.getEnd() ) );
}
lower.setEnd( pivot.getStart() - 1 );
}
}
}
final NavigableSet<PipePart> upperPart = this.colorParts.tailSet( pivot, false);
final Iterator<PipePart> goingUp = upperPart.iterator();
while ( goingUp.hasNext() ) {
final PipePart upper = goingUp.next();
if ( upper.getEnd() <= pivot.getEnd() ) {
/* Se l'elemento sopra è completamente contenuto dal pivot */
goingUp.remove();
} else if ( upper.getStart() <= pivot.getEnd() ) {
/* Se l'elemento sopra interseca il pivot */
upper.setStart( pivot.getEnd() );
break;
} else {
/* Se non si intersecano */
break;
}
}
}
private void doDraw( final @NotNull Graphics2D g2, final @NotNull String resourceName, final @NotNull Point where ) throws Exception {
boolean done = false;
boolean tryAcceleration = true;
final TextureLoader loader = GraphicsEnv.getInstance().getTextureLoader();
do {
final Image img = loader.loadTexture( resourceName, tryAcceleration );
g2.drawImage( img, where.x, where.y, null );
try {
if ( ! loader.mustReload( resourceName, tryAcceleration ) ) {
done = true;
}
} catch ( NoSuchElementException e ) {
if ( ! tryAcceleration ) {
throw new TextureNotFoundException( e );
}
tryAcceleration = false;
}
} while ( ! done );
}
@NotNull
@Override
public Set<PERPENDICULAR_DIRECTION> getAvailableDirections() {
if ( isVertical() ) {
return new HashSet<>( Arrays.asList( PERPENDICULAR_DIRECTION.DOWN, PERPENDICULAR_DIRECTION.UP ) );
} else {
return new HashSet<>( Arrays.asList( PERPENDICULAR_DIRECTION.RIGHT, PERPENDICULAR_DIRECTION.LEFT ) );
}
}
@Override
public boolean contains( @NotNull final Point position ) {
if(isVoidPipe()) {
return false;
}
if ( isVertical() ) {
if ( position.x == segment.getXA() && position.y >= segment.getMinY() && position.y <= segment.getMaxY() ) {
return true;
}
} else {
if ( position.y == segment.getYA() && position.x >= segment.getMinX() && position.x <= segment.getMaxX() ) {
return true;
}
}
return false;
}
@Override
public synchronized boolean isColored() {
if(isVoidPipe()) {
return true;
}
if ( this.colorParts.size() == 1 ) {
final PipePart part = this.colorParts.first();
if ( isVertical() ) {
if ( part.getStart() == segment.getMinY() && part.getEnd() == segment.getMaxY() ) {
return true;
}
} else if ( part.getStart() == segment.getMinX() && part.getEnd() == segment.getMaxX() ) {
return true;
}
}
return false;
}
@GuardedBy ( "this" )
private boolean isColoredAt( int where ) {
if ( isVertical() ) {
if ( where < this.segment.getMinY() || where > this.segment.getMaxY() ) {
throw new IllegalArgumentException();
}
} else {
if ( where < this.segment.getMinX() || where > this.segment.getMaxX() ) {
throw new IllegalArgumentException();
}
}
final PipePart pivotPart = new PipePart( where, where );
final PipePart lower = this.colorParts.floor( pivotPart );
if ( lower != null && lower.getEnd() >= where ) {
return true;
}
final PipePart upper = this.colorParts.ceiling( pivotPart );
return upper != null && upper.getStart() <= where;
}
/**
* Checks if this pipe is vertical
*
* @return true if vertical, false if horizontal
*/
private boolean isVertical() {
return this.vertical;
}
private static class PipePart implements Comparable<PipePart> {
private int start;
private int end;
PipePart( int start, int end ) {
this.start = Math.min( start, end );
this.end = Math.max( start, end );
}
@SuppressWarnings("unused")
PipePart( @NotNull PipePart copy ) {
this.start = copy.getStart();
this.end = copy.getEnd();
}
public boolean equals( Object o ) {
if ( o == null || ! ( o instanceof PipePart ) ) {
return false;
}
return o == this || ( ( (PipePart) o ).getStart() == getStart() && ( (PipePart) o ).getEnd() == getEnd() );
}
@Override
@NonNls
public String toString() {
return "PipePart{" +
"start=" + start +
", end=" + end +
'}';
}
int getStart() {
return this.start;
}
void setStart( int start ) throws IllegalArgumentException {
if ( start > getEnd() ) {
throw new IllegalArgumentException();
}
this.start = start;
}
int getEnd() {
return this.end;
}
void setEnd( int end ) throws IllegalArgumentException {
if ( end < this.getStart() ) {
throw new IllegalArgumentException();
}
this.end = end;
}
@Override
public int compareTo( final @NotNull PipePart o ) {
if ( this.getStart() > o.getStart() ) {
return 1;
}
if ( this.getStart() < o.getStart() ) {
return - 1;
}
return 0;
}
}
@Override
@NonNls
public synchronized String toString() {
return "PipeImpl{" +
"segment=" + segment +
", coloringAnimations=" + coloringAnimations +
", colorParts=" + colorParts +
", changedParts=" + changedParts +
", voidPipe=" + voidPipe +
'}';
}
public boolean isVoidPipe() {
return this.voidPipe;
}
}