/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2007-2008, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.renderer3d.terrainblock;
import com.jme.image.Texture;
import com.jme.math.Vector3f;
import com.jme.scene.Node;
import com.jme.scene.Spatial;
import org.geotools.renderer3d.provider.texture.impl.TextureListener;
import org.geotools.renderer3d.provider.texture.impl.TextureProvider;
import org.geotools.renderer3d.utils.BoundingRectangle;
import org.geotools.renderer3d.utils.BoundingRectangleImpl;
import org.geotools.renderer3d.utils.ParameterChecker;
import org.geotools.renderer3d.utils.quadtree.NodeListener;
import org.geotools.renderer3d.utils.quadtree.QuadTreeNode;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.List;
/**
* TODO: Store the texture image related to the terrain block in this class, create it here (based on parent block one),
* apply it to the mesh, and pass a reference of it to the texture calculation method. That way texture memory management
* will be simplified.
*
* @author Hans H�ggstr�m
*/
public final class TerrainBlockImpl
implements TerrainBlock, NodeListener<TerrainBlock>
{
//======================================================================
// Private Fields
private final int myNumberOfGridsPerSide;
private final BufferedImage myTextureImage;
private final TextureProvider myTextureProvider;
private final TextureListener myTextureListener = new TextureListener()
{
public void onTextureReady( final BoundingRectangle area, final BufferedImage texture )
{
// The texture has been calculated to the image instance we hold in this terrain block,
// so just apply it to the terrain mesh if it has been created already.
if ( myTerrainMesh != null )
{
myTerrainMesh.setTextureImage( myTextureImage );
}
myHasCalculatedTextureImage = true;
}
};
private final int myTextureSize;
private Vector3f myCenter;
private QuadTreeNode<TerrainBlock> myQuadTreeNode;
private List<Spatial> myChildNodeSpatials = new ArrayList<Spatial>( 4 );
private TerrainMesh myTerrainMesh = null;
private Node myTerrain3DNode = null;
private boolean myHasCalculatedTextureImage = false;
private Texture myTempParentTextureToUse = null;
//======================================================================
// Private Constants
private static final BoundingRectangleImpl WHOLE_TEXTURE = new BoundingRectangleImpl( 0, 0, 1, 1 );
private static final BoundingRectangleImpl WHOLE_TEXTURE_FLIPPED_ALONG_Y = new BoundingRectangleImpl( 0, 1, 1, 0 );
//======================================================================
// Public Methods
//----------------------------------------------------------------------
// Constructors
/**
* @param quadTreeNode
* @param numberOfGridsPerSide number of grid cells along the side of the TerrainBlock.
* @param textureSize
* @param textureProvider
*/
public TerrainBlockImpl( final QuadTreeNode<TerrainBlock> quadTreeNode,
final int numberOfGridsPerSide,
final int textureSize,
final TextureProvider textureProvider )
{
ParameterChecker.checkNotNull( quadTreeNode, "quadTreeNode" );
ParameterChecker.checkPositiveNonZeroInteger( numberOfGridsPerSide, "numberOfGridsPerSide" );
ParameterChecker.checkPositiveNonZeroInteger( textureSize, "textureSize" );
ParameterChecker.checkNotNull( textureProvider, "textureProvider" );
myQuadTreeNode = quadTreeNode;
myNumberOfGridsPerSide = numberOfGridsPerSide;
myTextureProvider = textureProvider;
myTextureSize = textureSize;
myTextureImage = allocateTextureImage( textureSize );
updateDerivedData();
myQuadTreeNode.addNodeListener( this );
}
//----------------------------------------------------------------------
// NodeListener Implementation
public void onCollapsed( QuadTreeNode<TerrainBlock> quadTreeNode )
{
if ( myTerrain3DNode != null )
{
for ( Spatial childNodeSpatial : myChildNodeSpatials )
{
myTerrain3DNode.detachChild( childNodeSpatial );
}
myChildNodeSpatials.clear();
myTerrain3DNode.attachChild( getOrCreateTerrainMesh() );
}
}
public void onExpanded( QuadTreeNode<TerrainBlock> quadTreeNode )
{
if ( myTerrain3DNode != null )
{
if ( myTerrainMesh != null )
{
myTerrain3DNode.detachChild( myTerrainMesh );
}
attachChildNodeSpatials();
}
}
//----------------------------------------------------------------------
// TerrainBlock Implementation
public Spatial getSpatial()
{
if ( myTerrain3DNode == null )
{
myTerrain3DNode = new Node();
if ( myQuadTreeNode.isExpanded() )
{
attachChildNodeSpatials();
}
else
{
myTerrain3DNode.attachChild( getOrCreateTerrainMesh() );
}
}
return myTerrain3DNode;
}
public Vector3f getCenter()
{
return myCenter;
}
public void updateDerivedData()
{
myHasCalculatedTextureImage = false;
// Remove previous texture request, if found
myTextureProvider.cancelRequest( myTextureListener );
// Update center
final BoundingRectangle bounds = myQuadTreeNode.getBounds();
myCenter = new Vector3f( (float) bounds.getCenterX(),
(float) bounds.getCenterY(),
0 ); // TODO: Get ground height at center.
// Update terrain mesh if present
if ( myTerrainMesh != null )
{
///fillTextureImageWithLoadingGraphics();
myTerrainMesh.updateBounds( bounds.getX1(), bounds.getY1(), bounds.getX2(), bounds.getY2() );
// myTerrainMesh.setTextureImage( myTextureImage );
// Copy a chunk of a previously calculated parent block texture to the texture of this block
initializeTextureFromParentTexture();
}
// Make sure the node is collapsed
onCollapsed( myQuadTreeNode );
// Request texture for terrain block
myTextureProvider.requestTexture( bounds, myTextureImage, myTextureListener );
}
public BufferedImage getTextureImage()
{
return myTextureImage;
}
public boolean hasCalculatedTextureImage()
{
return myTerrainMesh != null &&
!myTerrainMesh.isPlaceholderTextureInUse() &&
myHasCalculatedTextureImage;
}
public void clearPicture()
{
fillTextureImageWithLoadingGraphics();
}
public Texture getTexture()
{
if ( myTerrainMesh != null )
{
return myTerrainMesh.getTexture();
}
else
{
return null;
}
}
//======================================================================
// Private Methods
private void initializeTextureFromParentTexture()
{
myTempParentTextureToUse = null;
final BoundingRectangle area = calculatePlaceholderTextureAndArea( myQuadTreeNode );
setPlaceholderTexture( myTempParentTextureToUse, area );
}
/**
* Recursive function to calculate the area and texture to use.
*
* @return the area to use of the myTempParentTextureToUse, or null if no parent texture is available to use.
*/
private BoundingRectangle calculatePlaceholderTextureAndArea( final QuadTreeNode<TerrainBlock> node )
{
if ( node == null )
{
// No texture even in root
myTempParentTextureToUse = null;
return null;
}
else
{
final TerrainBlock block = node.getNodeData();
final Texture texture = block.getTexture();
if ( block.hasCalculatedTextureImage() && texture != null )
{
// Found texture in current node, use it.
myTempParentTextureToUse = texture;
return WHOLE_TEXTURE;
}
else
{
// Get area and texture from parent
final QuadTreeNode<TerrainBlock> parentNode = node.getParent();
final BoundingRectangle area = calculatePlaceholderTextureAndArea( parentNode );
if ( area == null )
{
return null;
}
else
{
// Calculate location of this node in the parent area
int quadrant = parentNode.getIndexOfChild( node );
// Compensate for some texture flipping complications
quadrant = area.flipSubquadrantAcrossY( quadrant );
return area.createSubquadrantBoundingRectangle( quadrant );
}
}
}
}
private void setPlaceholderTexture( final Texture texture, final BoundingRectangle textureArea )
{
if ( myTerrainMesh != null )
{
myTerrainMesh.setPlaceholderTexture( texture, textureArea );
}
}
private void fillTextureImageWithLoadingGraphics()
{
final Graphics graphics = myTextureImage.getGraphics();
// Background color
graphics.setColor( Color.GRAY );
graphics.fillRect( 0, 0, myTextureImage.getWidth(), myTextureImage.getHeight() );
// Text
graphics.setColor( Color.BLACK );
graphics.drawString( "Rendering Texture", 0, myTextureImage.getHeight() / 2 );
}
private BufferedImage allocateTextureImage( final int textureSize )
{
// Try to allocate the texture space, if we are out of memory just use null (a placeholder texture will be used)
BufferedImage mapImage;
try
{
mapImage = new BufferedImage( textureSize, textureSize, BufferedImage.TYPE_4BYTE_ABGR );
}
catch ( OutOfMemoryError e )
{
mapImage = null;
}
return mapImage;
}
private void attachChildNodeSpatials()
{
myChildNodeSpatials.clear();
for ( int i = 0; i < myQuadTreeNode.getNumberOfChildren(); i++ )
{
final QuadTreeNode<TerrainBlock> child = myQuadTreeNode.getChild( i );
if ( child != null )
{
final TerrainBlock childBlock = child.getNodeData();
if ( childBlock != null )
{
final Spatial childSpatial = childBlock.getSpatial();
myChildNodeSpatials.add( childSpatial );
myTerrain3DNode.attachChild( childSpatial );
}
}
}
}
private TerrainMesh getOrCreateTerrainMesh()
{
if ( myTerrainMesh == null )
{
myTerrainMesh = createTerrainMesh();
}
return myTerrainMesh;
}
private TerrainMesh createTerrainMesh()
{
final float z = 0;
final TerrainMesh terrainMesh = new TerrainMesh( myNumberOfGridsPerSide,
myNumberOfGridsPerSide,
myQuadTreeNode.getBounds().getX1(),
myQuadTreeNode.getBounds().getY1(),
myQuadTreeNode.getBounds().getX2(),
myQuadTreeNode.getBounds().getY2(),
z );
fillTextureImageWithLoadingGraphics();
terrainMesh.setTextureImage( myTextureImage );
// Copy a chunk of a previously calculated parent block texture to the texture of this block
initializeTextureFromParentTexture();
return terrainMesh;
}
}