/**
* Copyright (c) 2003-2009, Xith3D Project Group all rights reserved.
*
* Portions based on the Java3D interface, Copyright by Sun Microsystems.
* Many thanks to the developers of Java3D and Sun Microsystems for their
* innovation and design.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of the 'Xith3D Project Group' nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) A
* RISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE
*/
package org.xith3d.scenegraph;
import org.jagatoo.util.arrays.ArrayUtils;
import org.openmali.vecmath2.Point3f;
import org.xith3d.scenegraph.utils.CopyListener;
import org.xith3d.scenegraph.utils.LODWorkerThread;
import org.xith3d.utility.logging.X3DLog;
/**
* The LODSwitch is a Switch Node extension, that handles its children as
* discrete LOD items. Discrete LOD (level of detail) is a technique, that
* selects an item depending on its distance to the camera.
*
* <b>Don't use the regular addChild() methods of the NodeGroup interface, that
* are inherited, but the addLODItem() methods.</b>
*
* @see AbstractLODShape3D
*
* @author Marvin Froehlich (aka Qudus)
* @author Mathias Henze (aka cylab)
*/
public class LODSwitch extends Switch
{
private static final long ETERNAL = -1L;
private static final long INACTIVE = 0L;
private float[] minDistances = new float[ 16 ];
private float[] maxDistances = new float[ 16 ];
private long[] lastActive = new long[ 16 ];
private boolean first = true;
private boolean containsLazyLoadables = false;
private final Point3f translation = new Point3f();
private int pendingChange = -1;
// This needs to be volatile to be thread safe without synchronization (Java 1.5+ only)
private volatile boolean pendingSetUpFinished = false;
/**
* @param index the index of the LOD item of interest
*
* @return the minimal distance of the LOD item
*/
public final float getMinDist( int index )
{
if ( ( index < 0 ) || ( index >= numChildren() ) )
throw new ArrayIndexOutOfBoundsException( "There's no item with the index " + index );
return ( minDistances[ index ] );
}
/**
* @param item the LOD item of interest
*
* @return the minimal distance of the LOD item
*/
public final float getMinDist( Node item )
{
final int index = indexOf( item );
return ( getMinDist( index ) );
}
/**
* @param index the index of the LOD item of interest
*
* @return the maximal distance of the LOD item
*/
public final float getMaxDist( int index )
{
if ( ( index < 0 ) || ( index >= numChildren() ) )
throw new ArrayIndexOutOfBoundsException( "There's no item with the index " + index );
return ( maxDistances[ index ] );
}
/**
* @param item the LOD item of interest
*
* @return the maximal distance of the LOD item
*/
public final float getMaxDist( Node item )
{
final int index = indexOf( item );
return ( getMaxDist( index ) );
}
/**
* @return true, if the child is set up or starts a pending change and returns false
*
* @param index
*/
protected void setLODChild( int index )
{
final Node child = getChild( index );
// set no child if none is found...
if ( child == null )
super.setWhichChild( Switch.CHILD_NONE );
// switch to normal and always set up nodes
if ( !containsLazyLoadables || !( child instanceof LazyLoadable ) || ((LazyLoadable)child).isSetUp() )
{
super.setWhichChild( index );
return;
}
// otherwise start a pending change...
pendingChange = index;
LODWorkerThread.getInstance().enqueue( new Runnable()
{
public void run()
{
try
{
((LazyLoadable)child).prepare();
}
catch ( Throwable t )
{
// Should not happen... just log it.
X3DLog.print( t );
}
// Set the pending change to be finished, when the setUp() method returns
pendingSetUpFinished = true;
}
} );
}
/**
* <b>Unsupported for LODSwitch</b>
*
* @deprecated just because it is unsupported.
*/
@Deprecated
@Override
public void setWhichChild( int whichChild )
{
throw new UnsupportedOperationException( "The child selection is managed by the Switch." );
}
private void checkAndTearDownChildren()
{
final int currentIndex = getWhichChild();
final int num = numChildren() - 1;
final long current = System.currentTimeMillis();
// a deactivated LOD level will be cached for 1min
final long max = (60 * 1000);
// check all levels except the least detailed one, if it can be discarded
for ( int i = 0; i < num; i++ )
{
int x = ( currentIndex - i );
if ( ( x != 0 ) && ( lastActive[ i ] > INACTIVE ) && ( ( lastActive[ i ] + max ) < current ) )
{
// since only lazy loadable nodes can have an active timestamp, it's safe to cast to LazyLoadable
final LazyLoadable child = (LazyLoadable)getChild( i );
if ( child.isSetUp() )
{
lastActive[ i ] = INACTIVE;
// tear down the child on the current thread before invoking the clean up...
child.tearDown();
LODWorkerThread.getInstance().enqueue( new Runnable()
{
public void run()
{
child.cleanUp();
}
} );
}
break;
}
}
}
/**
* Called by the Renderer to make the LODSwitch select the right item for
* the given view position.
*/
public void updateWhichChild( Point3f viewPosition )
{
//super.setWhichChild( Switch.CHILD_ALL );
//if (true) return;
// TODO: this belongs to a worker thread or an Intervall
if ( containsLazyLoadables )
checkAndTearDownChildren();
// If there are no children available, set the Switch to CHILD_NONE
if ( numChildren() == 0 )
{
super.setWhichChild( Switch.CHILD_NONE );
return;
}
if ( first )
{
setLODChild( numChildren() - 1 );
first = false;
return;
}
// If there is an open pending change, process it first
if ( pendingChange != -1 )
{
// if the setup is finished, change the child
if ( pendingSetUpFinished )
{
Node child = getChild( pendingChange );
if ( ( child != null ) && ( child instanceof LazyLoadable ) )
{
( (LazyLoadable)child ).setUp();
}
// Reset the pending fields to prepare the next pending change
pendingChange = -1;
pendingSetUpFinished = false;
}
// With a pending change, don't change LODs
return;
}
// If there is currently no child set, but there are some available, set the Switch to the first Child
if ( getWhichChild() < 0 )
{
setLODChild( 0 );
return;
}
// Get the index of the currently set child
final int currentIndex = getWhichChild();
// Calculate the distance between this nodes anchor and the viewPosition
this.getWorldTransform().getTranslation( translation );
final float dist = translation.distance( viewPosition );
// If the distance is less than the minimum distance of the current LOD child,
// search the next "better" LOD child
if ( dist < minDistances[ currentIndex ] )
{
if ( lastActive[ currentIndex ] != ETERNAL )
lastActive[ currentIndex ] = System.currentTimeMillis();
// TODO: isn't just decrementing the currentIndex by one sufficient? "Skipping" a LOD level seems unlikely...
for ( int i = currentIndex - 1; i >= 0; i-- )
{
if ( ( dist <= maxDistances[ i ] ) && ( dist >= minDistances[ i ] ) )
{
setLODChild( i );
return;
}
}
// If none is found, set the Switch to CHILD_NONE
super.setWhichChild( Switch.CHILD_NONE );
}
// If the distance is greater than the maximum distance of the current LOD child
// search the next "worse" LOD child
else if ( dist > maxDistances[ currentIndex ] )
{
if ( lastActive[ currentIndex ] != ETERNAL )
lastActive[ currentIndex ] = System.currentTimeMillis();
// TODO: isn't just incrementing the currentIndex by one sufficient? "Skipping" a LOD level seems unlikely...
for ( int i = currentIndex+1; i < numChildren(); i++ )
{
if ( ( dist <= maxDistances[ i ] ) && ( dist >= minDistances[ i ] ) )
{
setLODChild( i );
return;
}
}
// If none is found, set the Switch to CHILD_NONE
super.setWhichChild( Switch.CHILD_NONE );
}
}
public int addLODItem( Node item, float minDist, float maxDist )
{
if ( minDist > maxDist )
throw new IllegalArgumentException( "minDist (" + minDist + ") is greater than maxDist (" + maxDist + ")" );
if ( numChildren() == 0 )
{
minDistances[ 0 ] = minDist;
maxDistances[ 0 ] = maxDist;
super.addChild( item, 0 );
return ( 0 );
}
// try to find the index of the new item by comparing the minDistance
int index = -1;
for ( int i = 0; i < numChildren(); i++ )
{
if ( minDistances[ i ] > minDist )
{
index = i;
break;
}
}
// if no item with a greater minDistance is already stored, append the new item
if ( index < 0 )
{
index = numChildren();
}
if ( ( index > 0 ) && ( minDistances[ index - 1 ] > minDist ) )
throw new IllegalArgumentException( "minDist MUST NOT overlap two items." );
if ( ( index < numChildren() - 1 ) && ( maxDistances[ index + 1 ] < maxDist ) )
throw new IllegalArgumentException( "maxDist MUST NOT overlap two items." );
minDistances = ArrayUtils.ensureCapacity( minDistances, numChildren() + 1 );
maxDistances = ArrayUtils.ensureCapacity( maxDistances, numChildren() + 1 );
lastActive = ArrayUtils.ensureCapacity( lastActive, numChildren() + 1 );
if ( index < numChildren() )
{
System.arraycopy( minDistances, index, minDistances, index + 1, numChildren() - index );
System.arraycopy( maxDistances, index, maxDistances, index + 1, numChildren() - index );
System.arraycopy( lastActive, index, lastActive, index + 1, numChildren() - index );
}
minDistances[ index ] = minDist;
maxDistances[ index ] = maxDist;
//lastActive[ index ] = ( item instanceof LazyLoadable ) ? ETERNAL : INACTIVE;
lastActive[ index ] = INACTIVE;
containsLazyLoadables |= item instanceof LazyLoadable;
super.addChild( item, index );
return ( index );
}
private final boolean recheckContainsLazyLoadables()
{
containsLazyLoadables = false;
int n = numChildren();
for ( int i = 0; i < n; i++ )
{
if ( getChild( i ) instanceof LazyLoadable )
{
containsLazyLoadables = true;
break;
}
}
return ( containsLazyLoadables );
}
protected void setLODItem( int index, Node item, boolean itemChanged, float minDist, float maxDist )
{
if ( ( index < 0 ) || ( index >= numChildren() ) )
throw new ArrayIndexOutOfBoundsException( "There's no item with the index " + index );
if ( ( index > 0 ) && ( minDistances[ index - 1 ] > minDist ) )
throw new IllegalArgumentException( "minDist MUST NOT overlap two items." );
if ( ( index < numChildren() - 1 ) && ( maxDistances[ index + 1 ] < maxDist ) )
throw new IllegalArgumentException( "maxDist MUST NOT overlap two items." );
minDistances[ index ] = minDist;
maxDistances[ index ] = maxDist;
if ( itemChanged )
{
Node oldItem = setChild( item, index );
if ( oldItem instanceof LazyLoadable )
{
if ( !( item instanceof LazyLoadable ) )
{
lastActive[ index ] = INACTIVE;
recheckContainsLazyLoadables();
}
}
else
{
if ( item instanceof LazyLoadable )
{
lastActive[ index ] = INACTIVE;
// lastActive[ index ] = ETERNAL;
recheckContainsLazyLoadables();
}
}
}
}
public final void setLODItem( int index, Node item, float minDist, float maxDist )
{
boolean itemChanged = ( getChild( index ) != item );
setLODItem( index, item, itemChanged, minDist, maxDist );
}
public final void setLODItem( int index, float minDist, float maxDist )
{
setLODItem( index, null, false, minDist, maxDist );
}
public void removeLODItem( int index )
{
if ( ( index < 0 ) || ( index >= numChildren() ) )
throw new IllegalArgumentException( "There is no LODItem with that index." );
int n = numChildren();
System.arraycopy( minDistances, 0, minDistances, 0, index );
System.arraycopy( minDistances, index + 1, minDistances, index, n - index - 1 );
System.arraycopy( maxDistances, 0, maxDistances, 0, index );
System.arraycopy( maxDistances, index + 1, maxDistances, index, n - index - 1 );
System.arraycopy( lastActive, 0, lastActive, 0, index );
System.arraycopy( lastActive, index + 1, lastActive, index, n - index - 1 );
removeChild( index );
if ( containsLazyLoadables )
{
recheckContainsLazyLoadables();
}
}
public void removeLODItem( Node node )
{
int index = indexOf( node );
if ( index < 0 )
throw new IllegalArgumentException( "There is no such LODItem." );
removeLODItem( index );
}
/**
* {@inheritDoc}
*/
@Override
protected LODSwitch newInstance()
{
boolean gib = Node.globalIgnoreBounds;
Node.globalIgnoreBounds = this.isIgnoreBounds();
LODSwitch s = new LODSwitch();
Node.globalIgnoreBounds = gib;
return ( s );
}
/**
* {@inheritDoc}
*/
@Override
public LODSwitch sharedCopy( CopyListener listener )
{
LODSwitch copy = (LODSwitch)super.sharedCopy( listener );
copy.minDistances = new float[ this.minDistances.length ];
System.arraycopy( this.minDistances, 0, copy.minDistances, 0, this.minDistances.length );
copy.maxDistances = new float[ this.maxDistances.length ];
System.arraycopy( this.maxDistances, 0, copy.maxDistances, 0, this.maxDistances.length );
if ( listener != null )
{
listener.onNodeCopied( this, copy, true );
}
return ( copy );
}
/**
* {@inheritDoc}
*/
@Override
public LODSwitch sharedCopy()
{
return ( (LODSwitch)super.sharedCopy() );
}
public LODSwitch()
{
super( Switch.CHILD_NONE );
}
}