Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
gov.nasa.worldwind.render.SurfaceObjectTileBuilder Maven / Gradle / Ivy
/*
* Copyright (C) 2012 United States Government as represented by the Administrator of the
* National Aeronautics and Space Administration.
* All Rights Reserved.
*/
package gov.nasa.worldwind.render;
import com.jogamp.opengl.util.texture.*;
import gov.nasa.worldwind.avlist.*;
import gov.nasa.worldwind.cache.Cacheable;
import gov.nasa.worldwind.geom.*;
import gov.nasa.worldwind.globes.Globe2D;
import gov.nasa.worldwind.layers.TextureTile;
import gov.nasa.worldwind.pick.PickedObject;
import gov.nasa.worldwind.util.*;
import com.jogamp.opengl.GL;
import java.awt.*;
import java.util.*;
import java.util.List;
/**
* Builds a list of {@link gov.nasa.worldwind.render.SurfaceTile} instances who's content is defined by a specified set
* of {@link gov.nasa.worldwind.render.SurfaceRenderable} instances. It's typically not necessary to use
* SurfaceObjectTileBuilder directly. World Wind's default scene controller automatically batches instances of
* SurfaceRenderable in a single SurfaceObjectTileBuilder. Applications that need to draw basic surface shapes should
* use or extend {@link gov.nasa.worldwind.render.SurfaceShape} instead of using SurfaceObjectTileBuilder directly.
*
* Surface tiles are built by calling {@link #buildTiles(DrawContext, Iterable)} with an iterable of surface
* renderables. This assembles a set of surface tiles that meet the resolution requirements for the specified draw
* context, then draws the surface renderables into those offscreen surface tiles by calling render on each instance.
* This process may temporarily use the framebuffer to perform offscreen rendering, and therefore should be called
* during the preRender method of a World Wind layer. See the {@link gov.nasa.worldwind.render.PreRenderable} interface
* for details. Once built, the surface tiles can be rendered by a {@link gov.nasa.worldwind.render.SurfaceTileRenderer}.
*
* By default, SurfaceObjectTileBuilder creates texture tiles with a width and height of 512 pixels, and with internal
* format GL_RGBA
. These parameters are configurable by calling {@link
* #setTileDimension(java.awt.Dimension)} or {@link #setTileTextureFormat(int)}.
*
* The most common usage pattern for SurfaceObjectTileBuilder is to build the surface tiles from a set of surface
* renderables during the preRender phase, then draw those surface tiles during the render phase. For example, a
* renderable can use SurfaceObjectTileBuilder to draw a set of surface renderables as follows:
*
*
*
* class MyRenderable implements Renderable, PreRenderable
* {
* protected SurfaceObjectTileBuilder tileBuilder = new SurfaceObjectTileBuilder();
*
* public void preRender(DrawContext dc)
* {
* List> surfaceRenderables = Arrays.asList(
* new SurfaceCircle(LatLon.fromDegrees(0, 100), 10000),
* new SurfaceSquare(LatLon.fromDegrees(0, 101), 10000));
* this.tileBuilder.buildSurfaceTiles(dc, surfaceRenderables);
* }
*
* public void render(DrawContext dc)
* {
* dc.getGeographicSurfaceTileRenderer().renderTiles(dc, this.tileBuilder.getTiles(dc));
* }
* }
*
*
*
* @author dcollins
* @version $Id: SurfaceObjectTileBuilder.java 3108 2015-05-26 19:07:06Z dcollins $
*/
public class SurfaceObjectTileBuilder
{
/** The default surface tile texture dimension, in pixels. */
protected static final int DEFAULT_TEXTURE_DIMENSION = 512;
/** The default OpenGL internal format used to create surface tile textures. */
protected static final int DEFAULT_TEXTURE_INTERNAL_FORMAT = GL.GL_RGBA8;
/** The default OpenGL pixel format used to create surface tile textures. */
protected static final int DEFAULT_TEXTURE_PIXEL_FORMAT = GL.GL_RGBA;
/**
* The default split scale. The split scale 2.9 has been empirically determined to render sharp lines and edges with
* the SurfaceShapes such as SurfacePolyline and SurfacePolygon.
*/
protected static final double DEFAULT_SPLIT_SCALE = 2.9;
/** The default level zero tile delta used to construct a LevelSet. */
protected static final LatLon DEFAULT_LEVEL_ZERO_TILE_DELTA = LatLon.fromDegrees(36, 36);
/**
* The default number of levels used to construct a LevelSet. Approximately 0.1 meters per pixel at the Earth's
* equator.
*/
protected static final int DEFAULT_NUM_LEVELS = 17;
/** The next unique ID. This property is shared by all instances of SurfaceObjectTileBuilder. */
protected static long nextUniqueId = 1;
/**
* Map associating a tile texture dimension to its corresponding LevelSet. This map is a class property in order to
* share LevelSets across all instances of SurfaceObjectTileBuilder.
*/
protected static Map levelSetMap = new HashMap();
/**
* Indicates the desired tile texture width and height, in pixels. Initially set to
* DEFAULT_TEXTURE_DIMENSION
.
*/
protected Dimension tileDimension = new Dimension(DEFAULT_TEXTURE_DIMENSION, DEFAULT_TEXTURE_DIMENSION);
/** The surface tile OpenGL texture format. 0 indicates the default format is used. */
protected int tileTextureFormat;
/** Controls if surface tiles are rendered using a linear filter or a nearest-neighbor filter. */
protected boolean useLinearFilter = true;
/** Controls if mip-maps are generated for surface tile textures. */
protected boolean useMipmaps = true;
/** Controls if tiles are forced to update during {@link #buildTiles(DrawContext, Iterable)}. */
protected boolean forceTileUpdates;
/** Controls the tile resolution as distance changes between the globe's surface and the eye point. */
protected double splitScale = DEFAULT_SPLIT_SCALE;
/**
* List of currently assembled surface renderables. Valid only during the execution of {@link
* #buildTiles(DrawContext, Iterable)}.
*/
protected List currentSurfaceObjects = new ArrayList();
/** List of currently assembled surface tiles. */
protected Map tileInfoMap = new HashMap();
/** The currently active TileInfo. Valid only during the execution of {@link #buildTiles(DrawContext, Iterable)}. */
protected TileInfo currentInfo;
/** Support class used to render to an offscreen surface tile. */
protected OGLRenderToTextureSupport rttSupport = new OGLRenderToTextureSupport();
/**
* Constructs a new SurfaceObjectTileBuilder with a tile width and height of 512
, with the default tile
* texture format, with linear filtering enabled, and with mip-mapping disabled.
*/
public SurfaceObjectTileBuilder()
{
}
/**
* Constructs a new SurfaceObjectTileBuilder width the specified tile dimension, tile texture format, and flags
* specifying if linear filtering and mip-mapping are enabled.
*
* @param tileTextureDimension the surface tile texture dimension, in pixels.
* @param tileTextureFormat the surface tile OpenGL texture format, or 0 to use the default format.
* @param useLinearFilter true to use linear filtering while rendering surface tiles; false to use
* nearest-neighbor filtering.
* @param useMipmaps true to generate mip-maps for surface tile textures; false otherwise.
*
* @throws IllegalArgumentException if the tile dimension is null.
*/
public SurfaceObjectTileBuilder(Dimension tileTextureDimension, int tileTextureFormat, boolean useLinearFilter,
boolean useMipmaps)
{
if (tileTextureDimension == null)
{
String message = Logging.getMessage("nullValue.DimensionIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
this.setTileDimension(tileTextureDimension);
this.setTileTextureFormat(tileTextureFormat);
this.setUseLinearFilter(useLinearFilter);
this.setUseMipmaps(useMipmaps);
}
/**
* Returns the surface tile dimension.
*
* @return the surface tile dimension, in pixels.
*/
public Dimension getTileDimension()
{
return this.tileDimension;
}
/**
* Specifies the preferred surface tile texture dimension. If the dimension is larger than the viewport dimension,
* this uses a dimension with width and height set to the largest power of two that is less than or equal to the
* specified dimension and the viewport dimension.
*
* @param dimension the surface tile dimension, in pixels.
*
* @throws IllegalArgumentException if the dimension is null.
*/
public void setTileDimension(Dimension dimension)
{
if (dimension == null)
{
String message = Logging.getMessage("nullValue.DimensionIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
this.tileDimension = dimension;
}
/**
* Returns the surface tile's OpenGL texture format, or 0 to indicate that the default format is used.
*
* @return the OpenGL texture format, or 0 if the default format is used.
*
* @see #setTileTextureFormat(int)
*/
public int getTileTextureFormat()
{
return tileTextureFormat;
}
/**
* Specifies the surface tile's OpenGL texture format. A value of 0 indicates that the default format should be
* used. Otherwise, the texture format may be one of the following: GL_ALPHA GL_ALPHA4
* GL_ALPHA8 GL_ALPHA12 GL_ALPHA16 GL_COMPRESSED_ALPHA
* GL_COMPRESSED_LUMINANCE GL_COMPRESSED_LUMINANCE_ALPHA GL_COMPRESSED_INTENSITY
* GL_COMPRESSED_RGB GL_COMPRESSED_RGBA GL_DEPTH_COMPONENT GL_DEPTH_COMPONENT16
* GL_DEPTH_COMPONENT24 GL_DEPTH_COMPONENT32 GL_LUMINANCE GL_LUMINANCE4
* GL_LUMINANCE8 GL_LUMINANCE12 GL_LUMINANCE16 GL_LUMINANCE_ALPHA
* GL_LUMINANCE4_ALPHA4 GL_LUMINANCE6_ALPHA2 GL_LUMINANCE8_ALPHA8
* GL_LUMINANCE12_ALPHA4 GL_LUMINANCE12_ALPHA12 GL_LUMINANCE16_ALPHA16
* GL_INTENSITY GL_INTENSITY4 GL_INTENSITY8 GL_INTENSITY12
* GL_INTENSITY16 GL_R3_G3_B2 GL_RGB GL_RGB4 GL_RGB5 GL_RGB8
* GL_RGB10 GL_RGB12 GL_RGB16 GL_RGBA GL_RGBA2 GL_RGBA4
* GL_RGB5_A1 GL_RGBA8 GL_RGB10_A2 GL_RGBA12 GL_RGBA16
* GL_SLUMINANCE GL_SLUMINANCE8 GL_SLUMINANCE_ALPHA GL_SLUMINANCE8_ALPHA8
* GL_SRGB GL_SRGB8 GL_SRGB_ALPHA GL_SRGB8_ALPHA8
*
* If the texture format is any of GL_RGB, GL_RGB8, GL_RGBA, or GL_RGBA8
, the tile builder attempts to
* use OpenGL framebuffer objects to render shapes to the texture tiles. Otherwise, this renders shapes to the
* framebuffer and copies the framebuffer contents to the texture tiles.
*
* @param textureFormat the OpenGL texture format, or 0 to use the default format.
*/
public void setTileTextureFormat(int textureFormat)
{
this.tileTextureFormat = textureFormat;
}
/**
* Returns if linear filtering is used when rendering surface tiles.
*
* @return true if linear filtering is used; false if nearest-neighbor filtering is used.
*/
public boolean isUseLinearFilter()
{
return useLinearFilter;
}
/**
* Specifies if linear filtering should be used when rendering surface tiles.
*
* @param useLinearFilter true to use linear filtering; false to use nearest-neighbor filtering.
*/
public void setUseLinearFilter(boolean useLinearFilter)
{
this.useLinearFilter = useLinearFilter;
}
/**
* Returns if mip-maps are generated for surface tile textures.
*
* @return true if mip-maps are generated; false otherwise.
*/
public boolean isUseMipmaps()
{
return this.useMipmaps;
}
/**
* Specifies if mip-maps should be generated for surface tile textures.
*
* @param useMipmaps true to generate mip-maps; false otherwise.
*/
public void setUseMipmaps(boolean useMipmaps)
{
this.useMipmaps = useMipmaps;
}
/**
* Indicates whether or not tiles textures are forced to update during {@link #buildTiles(DrawContext, Iterable)}.
* When true, tile textures always update their contents with the current surface renderables. When false, tile
* textures only update their contents when the surface renderables change. Initially false.
*
* @return true if tile textures always update their contents, false if tile textures only update when the surface
* renderables change.
*/
public boolean isForceTileUpdates()
{
return this.forceTileUpdates;
}
/**
* Specifies whether or not tiles textures are forced to update during {@link #buildTiles(DrawContext, Iterable)}.
* When true, tile textures always update their contents with the current surface renderables. When false, tile
* textures only update their contents when the surface renderables change.
*
* @param forceTileUpdates true if tile textures should always update their contents, false if tile textures should
* only update when the surface renderables change.
*/
public void setForceTileUpdates(boolean forceTileUpdates)
{
this.forceTileUpdates = forceTileUpdates;
}
/**
* Sets the parameter controlling the tile resolution as distance changes between the globe's surface and the eye
* point. Higher resolution is displayed as the split scale increases from 1.0. Lower resolution is displayed as the
* split scale decreases from 1.0. The default value is 2.9.
*
* @param splitScale a value near 1.0 that controls the tile's surface texel resolution as the distance between the
* globe's surface and the eye point change. Increasing values select higher resolution,
* decreasing values select lower resolution. The default value is 2.9.
*/
public void setSplitScale(double splitScale)
{
this.splitScale = splitScale;
}
/**
* Returns the split scale value controlling the tile's surface texel resolution relative to the distance between
* the globe's surface at the image position and the eye point.
*
* @return the current split scale.
*
* @see #setSplitScale(double)
*/
public double getSplitScale()
{
return this.splitScale;
}
/**
* Returns the number of SurfaceTiles assembled during the last call to {@link #buildTiles(DrawContext, Iterable)}.
*
* @param dc the draw context used to build tiles.
*
* @return a count of SurfaceTiles containing a composite representation of the specified surface renderables.
*
* @throws IllegalArgumentException if the draw context is null.
*/
public int getTileCount(DrawContext dc)
{
if (dc == null)
{
String message = Logging.getMessage("nullValue.DrawContextIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
Object tileInfoKey = this.createTileInfoKey(dc);
TileInfo tileInfo = this.tileInfoMap.get(tileInfoKey);
return tileInfo != null ? tileInfo.tiles.size() : 0;
}
/**
* Returns the list of SurfaceTiles assembled during the last call to {@link #buildTiles(DrawContext, Iterable)}.
*
* @param dc the draw context used to build tiles.
*
* @return a List of SurfaceTiles containing a composite representation of the specified surface renderables.
*
* @throws IllegalArgumentException if the draw context is null.
*/
public Collection extends SurfaceTile> getTiles(DrawContext dc)
{
if (dc == null)
{
String message = Logging.getMessage("nullValue.DrawContextIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
Object tileInfoKey = this.createTileInfoKey(dc);
TileInfo tileInfo = this.tileInfoMap.get(tileInfoKey);
return tileInfo != null ? tileInfo.tiles : Collections.emptyList();
}
/**
* Assembles the surface tiles and draws any surface renderables in the iterable into those offscreen tiles. The
* surface tiles are assembled to meet the necessary resolution of to the draw context's {@link
* gov.nasa.worldwind.View}. This may temporarily use the framebuffer to perform offscreen rendering, and therefore
* should be called during the preRender method of a World Wind {@link gov.nasa.worldwind.layers.Layer}.
*
* This does nothing if the specified iterable is null, is empty or contains no surface renderables.
*
* @param dc the draw context to build tiles for.
* @param iterable the iterable to gather surface renderables from.
*
* @throws IllegalArgumentException if the draw context is null.
*/
public void buildTiles(DrawContext dc, Iterable> iterable)
{
if (dc == null)
{
String message = Logging.getMessage("nullValue.DrawContextIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
TileInfoKey tileInfoKey = this.createTileInfoKey(dc);
this.currentInfo = this.tileInfoMap.get(tileInfoKey);
if (this.currentInfo == null)
{
this.currentInfo = this.createTileInfo(dc);
this.tileInfoMap.put(tileInfoKey, this.currentInfo);
}
this.currentSurfaceObjects.clear();
this.currentInfo.tiles.clear();
if (iterable == null)
return;
// Assemble the list of current surface renderables from the specified iterable.
this.assembleSurfaceObjects(iterable);
// We've cleared any tile assembly state from the last rendering pass. Determine if we can assemble and update
// the tiles. If not, we're done.
if (this.currentSurfaceObjects.isEmpty() || !this.canAssembleTiles(dc))
return;
// Assemble the current visible tiles and update their associated textures if necessary.
this.assembleTiles(dc);
this.updateTiles(dc);
// Clear references to surface renderables to avoid dangling references. The surface renderable list is no
// longer needed, no are the lists held by each tile.
this.currentSurfaceObjects.clear();
for (SurfaceObjectTile tile : this.currentInfo.tiles)
{
tile.clearObjectList();
}
}
/**
* Removes all entries from the list of SurfaceTiles assembled during the last call to {@link
* #buildTiles(DrawContext, Iterable)}.
*
* @param dc the draw context used to build tiles.
*
* @throws IllegalArgumentException if the draw context is null.
*/
public void clearTiles(DrawContext dc)
{
if (dc == null)
{
String message = Logging.getMessage("nullValue.DrawContextIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
Object tileInfoKey = this.createTileInfoKey(dc);
TileInfo tileInfo = this.tileInfoMap.get(tileInfoKey);
if (tileInfo != null)
{
tileInfo.tiles.clear();
}
}
/**
* Returns the list of pickable object candidates associated with the SurfaceTiles assembled during the last call to
* {@link #buildTiles(DrawContext, Iterable)}.
*
* @param dc the draw context used to build tiles.
*
* @return the pick candidates associated with the list of SurfaceTiles.
*
* @throws IllegalArgumentException if the draw context is null.
*/
public Collection getPickCandidates(DrawContext dc)
{
if (dc == null)
{
String message = Logging.getMessage("nullValue.DrawContextIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
Object tileInfoKey = this.createTileInfoKey(dc);
TileInfo tileInfo = this.tileInfoMap.get(tileInfoKey);
return tileInfo != null ? tileInfo.pickCandidates : Collections.emptyList();
}
/**
* Removes all entries from the list of pickable object candidates assembled during the last call to {@link
* #buildTiles(DrawContext, Iterable)}.
*
* @param dc the draw context used to build tiles.
*
* @throws IllegalArgumentException if the draw context is null.
*/
public void clearPickCandidates(DrawContext dc)
{
if (dc == null)
{
String message = Logging.getMessage("nullValue.DrawContextIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
Object tileInfoKey = this.createTileInfoKey(dc);
TileInfo tileInfo = this.tileInfoMap.get(tileInfoKey);
if (tileInfo != null)
{
tileInfo.pickCandidates.clear();
}
}
//**************************************************************//
//******************** Tile Updating *************************//
//**************************************************************//
/**
* Updates each {@link SurfaceObjectTileBuilder.SurfaceObjectTile} in the {@link #currentInfo}. This is typically
* called after {@link #assembleTiles(DrawContext)} to update the assembled tiles.
*
* This method does nothing if currentTiles
is empty.
*
* @param dc the draw context the tiles relate to.
*/
protected void updateTiles(DrawContext dc)
{
if (this.currentInfo.tiles.isEmpty())
return;
// The tile drawing rectangle has the same dimension as the current tile viewport, but it's lower left corner
// is placed at the origin. This is because the orthographic projection setup by OGLRenderToTextureSupport
// maps (0, 0) to the lower left corner of the drawing region, therefore we can drop the (x, y) offset when
// drawing pixels to the texture, as (0, 0) is automatically mapped to (x, y). Since we've created the tiles
// from a LevelSet where each level has equivalent dimension, we assume that tiles in the current tile list
// have equivalent dimension.
// The OpenGL framebuffer object extension used by RenderToTextureSupport works only for texture formats
// GL_RGB and GL_RGBA. Disable framebuffer objects if the tile builder has been configured with a different
// format.
this.rttSupport.setEnableFramebufferObject(
this.tileTextureFormat == 0 || // Default format is GL_RGB8.
this.tileTextureFormat == GL.GL_RGB ||
this.tileTextureFormat == GL.GL_RGB8 ||
this.tileTextureFormat == GL.GL_RGBA ||
this.tileTextureFormat == GL.GL_RGBA8);
this.rttSupport.beginRendering(dc, 0, 0, this.currentInfo.tileWidth, this.currentInfo.tileHeight);
try
{
for (SurfaceObjectTile tile : this.currentInfo.tiles)
{
this.updateTile(dc, tile);
}
}
finally
{
this.rttSupport.endRendering(dc);
}
}
/**
* Draws the current list of surface renderables into the specified surface tile. The surface tiles is updated only
* when necessary. The tile keeps track of the list of surface renderables rendered into it, and the state keys
* those objects. The tile is updated if the list changes, if any of the state keys change, or if the tile has no
* texture. Otherwise the tile is left unchanged and the update is skipped.
*
* @param dc the draw context the tile relates to.
* @param tile the tile to update.
*/
protected void updateTile(DrawContext dc, SurfaceObjectTile tile)
{
// Get the tile's texture from the draw context's texture cache. If null we create a new texture and update the
// texture cache below.
Texture texture = tile.getTexture(dc.getTextureCache());
// If force tile updates is off, compare the previous tile state against the currently computed state to
// determine if the tile needs to be updated. The tile needs to be updated if any the following conditions are
// true:
// * The tile has no texture.
// * The tile has no state.
// * The list of intersecting objects has changed.
// * An intersecting object's state key is different than one stored in the tile's previous state key.
if (!this.isForceTileUpdates())
{
Object tileStateKey = tile.getStateKey(dc);
if (texture != null && tileStateKey.equals(tile.lastUpdateStateKey))
return;
// If the tile needs to be updated, then assign its lastUpdateStateKey before its texture is created. This
// ensures that the lastUpdateStateKey is current when the tile is added to the cache.
tile.lastUpdateStateKey = tileStateKey;
}
if (texture == null) // Create the tile's texture if it doesn't already have one.
{
texture = this.createTileTexture(dc, tile.getWidth(), tile.getHeight());
tile.setTexture(dc.getTextureCache(), texture);
}
if (texture == null) // This should never happen, but we check anyway.
{
Logging.logger().warning(Logging.getMessage("nullValue.TextureIsNull"));
return;
}
try
{
// Surface renderables expect the SurfaceTileDrawContext to be attached to the draw context's AVList. Create
// a SurfaceTileDrawContext with the tile's Sector and viewport. The Sector defines the context's geographic
// extent, and the viewport defines the context's corresponding viewport in pixels.
dc.setValue(AVKey.SURFACE_TILE_DRAW_CONTEXT, this.createSurfaceTileDrawContext(tile));
this.rttSupport.setColorTarget(dc, texture);
this.rttSupport.clear(dc, new Color(0, 0, 0, 0)); // Set all texture pixels to transparent black.
if (tile.hasObjects())
{
for (SurfaceRenderable so : tile.getObjectList())
{
so.render(dc);
}
}
}
finally
{
this.rttSupport.setColorTarget(dc, null);
dc.removeKey(AVKey.SURFACE_TILE_DRAW_CONTEXT);
}
}
/**
* Returns a new surface tile texture for use on the specified draw context with the specified width and height.
*
* The returned texture's internal format is specified by tilePixelFormat
. If
* tilePixelFormat
is zero, this returns a texture with internal format GL_RGBA8
.
*
* The returned texture's parameters are configured as follows: Parameter
* Name Value GL.GL_TEXTURE_MIN_FILTER
GL_LINEAR_MIPMAP_LINEAR
* if useLinearFilter
and useMipmaps
are both true, GL_LINEAR
if
* useLinearFilter
is true and useMipmaps
is false, and GL_NEAREST
if
* useLinearFilter
is false. GL.GL_TEXTURE_MAG_FILTER
GL_LINEAR
* if useLinearFilter
is true, GL_NEAREST
if useLinearFilter
is
* false. GL.GL_TEXTURE_WRAP_S
GL_CLAMP_TO_EDGE
* GL.GL_TEXTURE_WRAP_T
GL_CLAMP_TO_EDGE
*
* @param dc the draw context to create a texture for.
* @param width the texture's width, in pixels.
* @param height the texture's height, in pixels.
*
* @return a new texture with the specified width and height.
*/
protected Texture createTileTexture(DrawContext dc, int width, int height)
{
int internalFormat = this.tileTextureFormat;
if (internalFormat == 0)
internalFormat = DEFAULT_TEXTURE_INTERNAL_FORMAT;
int pixelFormat = OGLUtil.computeTexturePixelFormat(internalFormat);
if (pixelFormat == 0)
pixelFormat = DEFAULT_TEXTURE_PIXEL_FORMAT;
Texture t;
GL gl = dc.getGL();
TextureData td = new TextureData(
gl.getGLProfile(), // GL profile
internalFormat, // internal format
width, height, // dimension
0, // border
pixelFormat, // pixel format
GL.GL_UNSIGNED_BYTE, // pixel type
this.isUseMipmaps(), // mipmap
false, false, // dataIsCompressed, mustFlipVertically
null, null) // buffer, flusher
{
/**
* Overridden to return a non-zero size. TextureData does not compute an estimated memory size if the buffer
* is null. Therefore we override getEstimatedMemorySize() to return the appropriate size in bytes of a
* texture with the common pixel formats.
*/
@Override
public int getEstimatedMemorySize()
{
int sizeInBytes = OGLUtil.estimateTextureMemorySize(this.getInternalFormat(), this.getWidth(),
this.getHeight(), this.getMipmap());
if (sizeInBytes > 0)
return sizeInBytes;
return super.getEstimatedMemorySize();
}
};
t = TextureIO.newTexture(td);
t.bind(gl);
gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, this.isUseLinearFilter() ?
(this.isUseMipmaps() ? GL.GL_LINEAR_MIPMAP_LINEAR : GL.GL_LINEAR) : GL.GL_NEAREST);
gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, this.isUseLinearFilter() ?
GL.GL_LINEAR : GL.GL_NEAREST);
gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, GL.GL_CLAMP_TO_EDGE);
gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_CLAMP_TO_EDGE);
if (this.isUseMipmaps())
{
double maxAnisotropy = dc.getGLRuntimeCapabilities().getMaxTextureAnisotropy();
if (dc.getGLRuntimeCapabilities().isUseAnisotropicTextureFilter() && maxAnisotropy >= 2.0)
{
gl.glTexParameterf(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAX_ANISOTROPY_EXT, (float) maxAnisotropy);
}
}
return t;
}
/**
* Returns a new Object representing the drawing context for the specified tile. The returned object should
* represent the tile's sector and it's corresponding viewport in pixels.
*
* @param tile The tile to create a context for.
*
* @return a new drawing context for the specified tile.
*/
protected Object createSurfaceTileDrawContext(SurfaceObjectTile tile)
{
return new SurfaceTileDrawContext(tile, this.currentInfo.pickCandidates);
}
//**************************************************************//
//******************** Surface Renderable Assembly ***********//
//**************************************************************//
/**
* Adds any SurfaceRenderables in the specified Iterable to the tile builder's {@link #currentSurfaceObjects} list.
*
* @param iterable the Iterable to gather SurfaceRenderables from.
*/
protected void assembleSurfaceObjects(Iterable> iterable)
{
// Gather up all the SurfaceRenderables, ignoring null references and non SurfaceRenderables.
for (Object o : iterable)
{
if (o instanceof SurfaceRenderable)
this.currentSurfaceObjects.add((SurfaceRenderable) o);
}
}
//**************************************************************//
//******************** LevelSet Assembly *********************//
//**************************************************************//
/**
* Returns a shared LevelSet
for the specified tileDimension
. All instances of
* SurfaceObjectTileBuilder
share common LevelSets to determine which tiles are visible, but create
* unique tile instances and uses a unique tile cache name. Since all instances use the same tile structure to
* determine visible tiles, this saves memory while ensuring that each instance stores its own tiles in the cache.
*
* The returned LevelSet's cache name and dataset name are dummy values, and should not be used. Use this tile
* builder's cache name for the specified tileDimension
instead.
*
* In practice, there are at most 10 dimensions we use: 512, 256, 128, 64, 32, 16, 8, 4, 2, 1. Therefore keeping the
* LevelSet
s in a map requires little memory overhead, and ensures each LevelSet
is
* retained once constructed. Retaining references to the LevelSet
s means we're able to re-use the
* texture resources associated with each LevelSet
in the DrawContext
's texture cache.
*
* Subsequent calls are guaranteed to return the same LevelSet
for the same
* tileDimension
.
*
* @param tileWidth the tile width, in pixels.
* @param tileHeight the tile height, in pixels.
*
* @return a LevelSet with the specified tile dimensions.
*/
protected LevelSet getLevelSet(int tileWidth, int tileHeight)
{
// If we already have a LevelSet for the dimension, just return it. Otherwise create it and put it in a map for
// use during subsequent calls.
Dimension key = new Dimension(tileWidth, tileHeight);
LevelSet levelSet = levelSetMap.get(key);
if (levelSet == null)
{
levelSet = createLevelSet(tileWidth, tileHeight);
levelSetMap.put(key, levelSet);
}
return levelSet;
}
/**
* Returns a new LevelSet with the specified tile width and height. The LevelSet overs the full sphere, has a level
* zero tile delta of {@link #DEFAULT_LEVEL_ZERO_TILE_DELTA}, has number of levels equal to {@link
* #DEFAULT_NUM_LEVELS} (with no empty levels). The LevelSets' cache name and dataset name dummy values, and should
* not be used.
*
* @param tileWidth the LevelSet's tile width, in pixels.
* @param tileHeight the LevelSet's tile height, in pixels.
*
* @return a new LevelSet configured to with
*/
protected static LevelSet createLevelSet(int tileWidth, int tileHeight)
{
AVList params = new AVListImpl();
params.setValue(AVKey.LEVEL_ZERO_TILE_DELTA, DEFAULT_LEVEL_ZERO_TILE_DELTA);
params.setValue(AVKey.SECTOR, Sector.FULL_SPHERE);
params.setValue(AVKey.NUM_LEVELS, DEFAULT_NUM_LEVELS);
params.setValue(AVKey.NUM_EMPTY_LEVELS, 0);
params.setValue(AVKey.TILE_WIDTH, tileWidth);
params.setValue(AVKey.TILE_HEIGHT, tileHeight);
// This is a shared LevelSet, so just supply a dummy cache name and dataset name.
params.setValue(AVKey.DATA_CACHE_NAME, SurfaceObjectTileBuilder.class.getName());
params.setValue(AVKey.DATASET_NAME, SurfaceObjectTileBuilder.class.getName());
// We won't use any tile resource paths, so just supply a dummy format suffix.
params.setValue(AVKey.FORMAT_SUFFIX, SurfaceObjectTileBuilder.class.getName());
return new LevelSet(params);
}
//**************************************************************//
//******************** Tile Assembly *************************//
//**************************************************************//
/**
* Returns true if the draw context's viewport width and height are greater than zero.
*
* @param dc the DrawContext to test.
*
* @return true if the DrawContext's has a non-zero viewport; false otherwise.
*/
protected boolean canAssembleTiles(DrawContext dc)
{
Rectangle viewport = dc.getView().getViewport();
return viewport.getWidth() > 0 && viewport.getHeight() > 0;
}
/**
* Assembles a set of surface tiles that are visible in the specified DrawContext and meet the tile builder's
* resolution criteria. Tiles are culled against the current surface renderable list, against the DrawContext's view
* frustum during rendering mode, and against the DrawContext's pick frustums during picking mode. If a tile does
* not meet the tile builder's resolution criteria, it's split into four sub-tiles and the process recursively
* repeated on the sub-tiles. Visible leaf tiles are added to the {@link #currentInfo}.
*
* During assembly each surface renderable in {@link #currentSurfaceObjects} is sorted into the tiles they
* intersect. The top level tiles are used as an index to quickly determine which tiles each renderable intersects.
* Surface renderables are sorted into sub-tiles by simple intersection tests, and are added to each tile's surface
* renderable list at most once. See {@link SurfaceObjectTileBuilder.SurfaceObjectTile#addSurfaceObject(SurfaceRenderable,
* gov.nasa.worldwind.geom.Sector)}. Tiles that don't intersect any surface renderables are discarded.
*
* @param dc the DrawContext to assemble tiles for.
*/
protected void assembleTiles(DrawContext dc)
{
LevelSet levelSet = this.currentInfo.levelSet;
String tileCacheName = this.currentInfo.cacheName;
Level level = levelSet.getFirstLevel();
Angle dLat = level.getTileDelta().getLatitude();
Angle dLon = level.getTileDelta().getLongitude();
Angle latOrigin = levelSet.getTileOrigin().getLatitude();
Angle lonOrigin = levelSet.getTileOrigin().getLongitude();
// Store the top level tiles in a set to ensure that each top level tile is added only once. Store the tiles
// that intersect each surface renderable in a set to ensure that each object is added to a tile at most once.
Set topLevelTiles = new HashSet();
Set intersectingTileKeys = new HashSet();
// Iterate over the current surface renderables, adding each surface renderable to the top level tiles that it
// intersects. This produces a set of top level tiles containing the surface renderables that intersect each
// tile. We use the tile structure as an index to quickly determine the tiles a surface renderable intersects,
// and add object to those tiles. This has the effect of quickly sorting the objects into the top level tiles.
// We collect the top level tiles in a HashSet to ensure there are no duplicates when multiple objects intersect
// the same top level tiles.
for (SurfaceRenderable so : this.currentSurfaceObjects)
{
List sectors = so.getSectors(dc);
if (sectors == null)
continue;
for (Sector s : sectors)
{
// Use the LevelSets tiling scheme to index the surface renderable's sector into the top level tiles.
// This index operation is faster than computing an intersection test between each tile and the list of
// surface renderables.
int firstRow = Tile.computeRow(dLat, s.getMinLatitude(), latOrigin);
int firstCol = Tile.computeColumn(dLon, s.getMinLongitude(), lonOrigin);
int lastRow = Tile.computeRow(dLat, s.getMaxLatitude(), latOrigin);
int lastCol = Tile.computeColumn(dLon, s.getMaxLongitude(), lonOrigin);
Angle p1 = Tile.computeRowLatitude(firstRow, dLat, latOrigin);
for (int row = firstRow; row <= lastRow; row++)
{
Angle p2;
p2 = p1.add(dLat);
Angle t1 = Tile.computeColumnLongitude(firstCol, dLon, lonOrigin);
for (int col = firstCol; col <= lastCol; col++)
{
Angle t2;
t2 = t1.add(dLon);
Object tileKey = this.createTileKey(level, row, col, tileCacheName);
// Ignore this tile if the surface renderable has already been added to it. This handles
// dateline spanning surface renderables which have two sectors that share a common boundary.
if (intersectingTileKeys.contains(tileKey))
continue;
SurfaceObjectTile tile = (SurfaceObjectTile) TextureTile.getMemoryCache().getObject(tileKey);
if (tile == null)
{
tile = this.createTile(new Sector(p1, p2, t1, t2), level, row, col, tileCacheName);
TextureTile.getMemoryCache().add(tileKey, tile);
}
intersectingTileKeys.add(tileKey); // Set of intersecting tile keys ensure no duplicate objects.
topLevelTiles.add(tile); // Set of top level tiles ensures no duplicates tiles.
tile.addSurfaceObject(so, s);
t1 = t2;
}
p1 = p2;
}
}
intersectingTileKeys.clear(); // Clear the intersecting tile keys for the next surface renderable.
}
// Add each top level tile or its descendants to the current tile list.
for (SurfaceObjectTile tile : topLevelTiles)
{
this.addTileOrDescendants(dc, levelSet, null, tile);
}
}
/**
* Potentially adds the specified tile or its descendants to the tile builder's {@link #currentInfo}. The tile and
* its descendants are discarded if the tile is not visible or does not intersect any surface renderables in the
* parent's surface renderable list. See {@link SurfaceObjectTileBuilder.SurfaceObjectTile#getObjectList()}.
*
* If the tile meet the tile builder's resolution criteria it's added to the tile builder's
* currentTiles
list. Otherwise, it's split into four sub-tiles and each tile is recursively processed.
* See {@link #meetsRenderCriteria(DrawContext, gov.nasa.worldwind.util.LevelSet, gov.nasa.worldwind.util.Tile)}.
*
* @param dc the current DrawContext.
* @param levelSet the tile's LevelSet.
* @param parent the tile's parent, or null if the tile is a top level tile.
* @param tile the tile to add.
*/
protected void addTileOrDescendants(DrawContext dc, LevelSet levelSet, SurfaceObjectTile parent,
SurfaceObjectTile tile)
{
// Ignore this tile if it falls completely outside the DrawContext's visible sector.
if (!this.intersectsVisibleSector(dc, tile))
{
// This tile is not added to the current tile list, so we clear it's object list to prepare it for use
// during the next frame.
tile.clearObjectList();
return;
}
// Ignore this tile if it falls completely outside the frustum. This may be the viewing frustum or the pick
// frustum, depending on the implementation.
if (!this.intersectsFrustum(dc, tile))
{
// This tile is not added to the current tile list, so we clear it's object list to prepare it for use
// during the next frame.
tile.clearObjectList();
return;
}
// If the parent tile is not null, add any parent surface renderables that intersect this tile.
if (parent != null)
this.addIntersectingObjects(dc, parent, tile);
// Ignore tiles that do not intersect any surface renderables.
if (!tile.hasObjects())
return;
// If this tile meets the current rendering criteria, add it to the current tile list. This tile's object list
// is cleared after the tile update operation.
if (this.meetsRenderCriteria(dc, levelSet, tile))
{
this.addTile(tile);
return;
}
Level nextLevel = levelSet.getLevel(tile.getLevelNumber() + 1);
for (TextureTile subTile : tile.createSubTiles(nextLevel))
{
this.addTileOrDescendants(dc, levelSet, tile, (SurfaceObjectTile) subTile);
}
// This tile is not added to the current tile list, so we clear it's object list to prepare it for use during
// the next frame.
tile.clearObjectList();
}
/**
* Adds surface renderables from the parent's object list to the specified tile's object list. If the tile's sector
* does not intersect the sector bounding the parent's object list, this does nothing. Otherwise, this adds any of
* the parent's surface renderables that intersect the tile's sector to the tile's object list.
*
* @param dc the current DrawContext.
* @param parent the tile's parent.
* @param tile the tile to add intersecting surface renderables to.
*/
protected void addIntersectingObjects(DrawContext dc, SurfaceObjectTile parent, SurfaceObjectTile tile)
{
// If the parent has no objects, then there's nothing to add to this tile and we exit immediately.
if (!parent.hasObjects())
return;
// If this tile does not intersect the parent's object bounding sector, then none of the parent's objects
// intersect this tile. Therefore we exit immediately, and do not add any objects to this tile.
if (!tile.getSector().intersects(parent.getObjectSector()))
return;
// If this tile contains the parent's object bounding sector, then all of the parent's objects intersect this
// tile. Therefore we just add all of the parent's objects to this tile. Additionally, the parent's object
// bounding sector becomes this tile's object bounding sector.
if (tile.getSector().contains(parent.getObjectSector()))
{
tile.addAllSurfaceObjects(parent.getObjectList(), parent.getObjectSector());
}
// Otherwise, the tile may intersect some of the parent's object list. Compute which objects intersect this
// tile, and compute this tile's bounding sector as the union of those object's sectors.
else
{
for (SurfaceRenderable so : parent.getObjectList())
{
List sectors = so.getSectors(dc);
if (sectors == null)
continue;
// Test intersection against each of the surface renderable's sectors. We break after finding an
// intersection to avoid adding the same object to the tile more than once.
for (Sector s : sectors)
{
if (tile.getSector().intersects(s))
{
tile.addSurfaceObject(so, s);
break;
}
}
}
}
}
/**
* Adds the specified tile to this tile builder's {@link #currentInfo} and the TextureTile memory cache.
*
* @param tile the tile to add.
*/
protected void addTile(SurfaceObjectTile tile)
{
this.currentInfo.tiles.add(tile);
TextureTile.getMemoryCache().add(tile.getTileKey(), tile);
}
/**
* Test if the tile intersects the specified draw context's frustum. During picking mode, this tests intersection
* against all of the draw context's pick frustums. During rendering mode, this tests intersection against the draw
* context's viewing frustum.
*
* @param dc the draw context the surface renderable is related to.
* @param tile the tile to test for intersection.
*
* @return true if the tile intersects the draw context's frustum; false otherwise.
*/
protected boolean intersectsFrustum(DrawContext dc, TextureTile tile)
{
Extent extent = tile.getExtent(dc);
if (extent == null)
return false;
if (dc.isPickingMode())
return dc.getPickFrustums().intersectsAny(extent);
return dc.getView().getFrustumInModelCoordinates().intersects(extent);
}
/**
* Test if the specified tile intersects the draw context's visible sector. This returns false if the draw context's
* visible sector is null.
*
* @param dc the current draw context.
* @param tile the tile to test for intersection.
*
* @return true if the tile intersects the draw context's visible sector; false otherwise.
*/
protected boolean intersectsVisibleSector(DrawContext dc, TextureTile tile)
{
return dc.getVisibleSector() != null && dc.getVisibleSector().intersects(tile.getSector());
}
/**
* Tests if the specified tile meets the rendering criteria on the specified draw context. This returns true if the
* tile is from the level set's final level, or if the tile achieves the desired resolution on the draw context.
*
* @param dc the current draw context.
* @param levelSet the level set the tile belongs to.
* @param tile the tile to test.
*
* @return true if the tile meets the rendering criteria; false otherwise.
*/
protected boolean meetsRenderCriteria(DrawContext dc, LevelSet levelSet, Tile tile)
{
return levelSet.isFinalLevel(tile.getLevel().getLevelNumber()) || !this.needToSplit(dc, tile);
}
/**
* Tests if the specified tile must be split to meets the desired resolution on the specified draw context. This
* compares the distance form the eye point to the tile to determine if the tile meets the desired resolution for
* the {@link gov.nasa.worldwind.View} attached to the draw context.
*
* @param dc the current draw context.
* @param tile the tile to test.
*
* @return true if the tile must be split; false otherwise.
*/
protected boolean needToSplit(DrawContext dc, Tile tile)
{
// Compute the height in meters of a texel from the specified tile. Take care to convert from the radians to
// meters by multiplying by the globe's radius, not the length of a Cartesian point. Using the length of a
// Cartesian point is incorrect when the globe is flat.
double texelSizeRadians = tile.getLevel().getTexelSize();
double texelSizeMeters = dc.getGlobe().getRadius() * texelSizeRadians;
// Compute the level of detail scale and the field of view scale. These scales are multiplied by the eye
// distance to derive a scaled distance that is then compared to the texel size. The level of detail scale is
// specified as a power of 10. For example, a detail factor of 3 means split when the cell size becomes more
// than one thousandth of the eye distance. The field of view scale is specified as a ratio between the current
// field of view and a the default field of view. In a perspective projection, decreasing the field of view by
// 50% has the same effect on object size as decreasing the distance between the eye and the object by 50%.
// The detail hint is reduced for tiles above 75 degrees north and below 75 degrees south.
double s = this.getSplitScale();
if (tile.getSector().getMinLatitude().degrees >= 75 || tile.getSector().getMaxLatitude().degrees <= -75)
s *= 0.85;
double detailScale = Math.pow(10, -s);
double fieldOfViewScale = dc.getView().getFieldOfView().tanHalfAngle() / Angle.fromDegrees(45).tanHalfAngle();
fieldOfViewScale = WWMath.clamp(fieldOfViewScale, 0, 1);
// Compute the distance between the eye point and the sector in meters, and compute a fraction of that distance
// by multiplying the actual distance by the level of detail scale and the field of view scale.
double eyeDistanceMeters = tile.getSector().distanceTo(dc, dc.getView().getEyePoint());
double scaledEyeDistanceMeters = eyeDistanceMeters * detailScale * fieldOfViewScale;
// Split when the texel size in meters becomes greater than the specified fraction of the eye distance, also in
// meters. Another way to say it is, use the current tile if its texel size is less than the specified fraction
// of the eye distance.
//
// NOTE: It's tempting to instead compare a screen pixel size to the texel size, but that calculation is
// window-size dependent and results in selecting an excessive number of tiles when the window is large.
return texelSizeMeters > scaledEyeDistanceMeters;
}
//**************************************************************//
//******************** Tile Info *****************************//
//**************************************************************//
/**
* Creates a key to address the tile information associated with the specified draw context. Each key is unique to
* this instance, the tile dimensions that fit in the draw context's viewport, and the globe offset when a 2D globe
* is in use. Using a unique set of tile information ensures that
*
* In practices, there are at most 10 dimensions we'll use (512, 256, 128, 64, 32, 16, 8, 4, 2, 1) and 3 globe
* offsets (-1, 0, 1). Therefore there are at most 30 sets of tile information for each instance of
* SurfaceObjectTileBuilder.
*
* @param dc the draw context to create the tile info key for.
*
* @return the tile info key for the specified draw context.
*/
protected TileInfoKey createTileInfoKey(DrawContext dc)
{
Dimension tileDimension = this.computeTextureTileDimension(dc);
return new TileInfoKey(dc, tileDimension.width, tileDimension.height);
}
/**
* Creates a tile info associated with the specified draw context.
*
* @param dc the draw context to create the tile info for.
*
* @return the tile info for the specified draw context.
*/
protected TileInfo createTileInfo(DrawContext dc)
{
// Use a LevelSet shared by all instances of this class to save memory and prevent a conflict in the tile and
// texture caches. Use a cache name unique to this tile info instance.
Dimension tileDimension = this.computeTextureTileDimension(dc);
LevelSet levelSet = this.getLevelSet(tileDimension.width, tileDimension.height);
String cacheName = this.uniqueCacheName();
return new TileInfo(levelSet, cacheName, tileDimension.width, tileDimension.height);
}
/**
* Returns the tile dimension used to create the tile textures for the specified DrawContext
. This
* attempts to use this tile builder's {@link #tileDimension}, but always returns a dimension that is is a power of
* two, is square, and fits in the DrawContext
's viewport.
*
* @param dc the DrawContext
to compute a texture tile dimension for.
*
* @return a texture tile dimension appropriate for the specified DrawContext
.
*/
protected Dimension computeTextureTileDimension(DrawContext dc)
{
// Force a square dimension by using the maximum of the tile builder's tileWidth and tileHeight.
int maxSize = Math.max(this.tileDimension.width, this.tileDimension.height);
// The viewport may be smaller than the desired dimension. For that reason, we constrain the desired tile
// dimension by the viewport width and height.
Rectangle viewport = dc.getView().getViewport();
if (maxSize > viewport.width)
maxSize = viewport.width;
if (maxSize > viewport.height)
maxSize = viewport.height;
// The final dimension used to render all surface tiles will be the power of two which is less than or equal to
// the preferred dimension, and which fits into the viewport.
int potSize = WWMath.powerOfTwoFloor(maxSize);
return new Dimension(potSize, potSize);
}
/**
* Returns a unique name appropriate for use as part of a cache name.
*
* @return a unique cache name.
*/
protected String uniqueCacheName()
{
StringBuilder sb = new StringBuilder();
sb.append(this.getClass().getName());
sb.append("/");
sb.append(nextUniqueId++);
return sb.toString();
}
protected static class TileInfoKey
{
public final int globeOffset;
public final int tileWidth;
public final int tileHeight;
public TileInfoKey(DrawContext dc, int tileWidth, int tileHeight)
{
this.globeOffset = (dc.getGlobe() instanceof Globe2D) ? ((Globe2D) dc.getGlobe()).getOffset() : 0;
this.tileWidth = tileWidth;
this.tileHeight = tileHeight;
}
@Override
public boolean equals(Object o)
{
if (this == o)
return true;
if (o == null || this.getClass() != o.getClass())
return false;
TileInfoKey that = (TileInfoKey) o;
return this.globeOffset == that.globeOffset
&& this.tileWidth == that.tileWidth
&& this.tileHeight == that.tileHeight;
}
@Override
public int hashCode()
{
int result = this.globeOffset;
result = 31 * result + this.tileWidth;
result = 31 * result + this.tileHeight;
return result;
}
}
protected static class TileInfo
{
public ArrayList tiles = new ArrayList();
public ArrayList pickCandidates = new ArrayList();
public LevelSet levelSet;
public String cacheName;
public int tileWidth;
public int tileHeight;
public TileInfo(LevelSet levelSet, String cacheName, int tileWidth, int tileHeight)
{
this.levelSet = levelSet;
this.cacheName = cacheName;
this.tileWidth = tileWidth;
this.tileHeight = tileHeight;
}
}
//**************************************************************//
//******************** Surface Object Tile *******************//
//**************************************************************//
/**
* Returns a new SurfaceObjectTile corresponding to the specified {@code sector}, {@code level}, {@code row}, {@code
* column}, and {@code cacheName}.
*
* @param sector The tile's Sector.
* @param level The tile's Level in a {@link LevelSet}.
* @param row The tile's row in the Level, starting from 0 and increasing to the right.
* @param column The tile's column in the Level, starting from 0 and increasing upward.
* @param cacheName Tile tile's cache name.
*
* @return a new SurfaceObjectTile.
*/
protected SurfaceObjectTile createTile(Sector sector, Level level, int row, int column, String cacheName)
{
return new SurfaceObjectTile(sector, level, row, column, cacheName);
}
/**
* Returns a new tile key corresponding to the tile with the specified {@code level}, {@code row}, {@code column},
* and {@code cacheName}.
*
* @param level The tile's Level in a {@link LevelSet}.
* @param row The tile's row in the Level, starting from 0 and increasing to the right.
* @param column The tile's column in the Level, starting from 0 and increasing upward.
* @param cacheName Tile tile's cache name.
*
* @return a tile key.
*/
protected Object createTileKey(Level level, int row, int column, String cacheName)
{
return new TileKey(level.getLevelNumber(), row, column, cacheName);
}
/**
* Represents a {@link gov.nasa.worldwind.layers.TextureTile} who's contents is constructed by a set of surface
* objects. The tile maintains a collection of surface renderables that intersect the tile, and provides methods for
* to modify and retrieve that collection. Additionally, the method {@link #getStateKey(DrawContext)} provides a
* mechanism to uniquely identify the tile's current state, including the state of each intersecting surface
* object.
*/
protected static class SurfaceObjectTile extends TextureTile
{
/** The sector that bounds the surface renderables intersecting the tile. */
protected Sector objectSector;
/** List of surface renderables intersecting the tile. */
protected List intersectingObjects;
/** The state key that was valid when the tile was last updated. */
protected Object lastUpdateStateKey;
/**
* Constructs a tile for a given sector, level, row and column of the tile's containing tile set.
*
* @param sector The sector corresponding with the tile.
* @param level The tile's level within a containing level set.
* @param row The row index (0 origin) of the tile within the indicated level.
* @param column The column index (0 origin) of the tile within the indicated level.
* @param cacheName The tile's cache name. Overrides the Level's cache name to associates the tile with it's
* tile builder in a global cache.
*
* @throws IllegalArgumentException if any of the {@code sector}, {@code level}, or {@code cacheName } are
* {@code null}.
*/
public SurfaceObjectTile(Sector sector, Level level, int row, int column, String cacheName)
{
super(sector, level, row, column, cacheName);
}
/**
* Returns the tile's size in bytes. Overridden to append the size of the {@link #cacheName} and the {@link
* #lastUpdateStateKey} to the superclass' computed size.
*
* @return The tile's size in bytes.
*/
@Override
public long getSizeInBytes()
{
long size = super.getSizeInBytes();
if (this.lastUpdateStateKey instanceof Cacheable)
size += ((Cacheable) this.lastUpdateStateKey).getSizeInBytes();
else if (this.lastUpdateStateKey != null)
size += 4; // If the object doesn't implement Cacheable, just account for the reference to it.
return size;
}
/**
* Returns an object that uniquely identifies the tile's state on the specified draw context. This object is
* guaranteed to be globally unique; an equality test with a state key from another always returns false.
*
* @param dc the draw context the state key relates to.
*
* @return an object representing surface renderable's current state.
*/
public Object getStateKey(DrawContext dc)
{
return new SurfaceObjectTileStateKey(dc, this);
}
/**
* Returns a sector that bounds the surface renderables intersecting the tile. This returns null if no surface
* objects intersect the tile.
*
* @return a sector bounding the tile's intersecting objects.
*/
public Sector getObjectSector()
{
return this.objectSector;
}
/**
* Returns whether list of surface renderables intersecting this tile has elements.
*
* @return {@code true} if the list of surface renderables intersecting this tile has elements, and {@code
* false} otherwise.
*/
public boolean hasObjects()
{
return this.intersectingObjects != null && !this.intersectingObjects.isEmpty();
}
/**
* Returns a list of surface renderables intersecting the tile.
*
* @return a tile's intersecting objects.
*/
public List getObjectList()
{
return this.intersectingObjects;
}
/**
* Clears the tile's list of intersecting objects. {@link #getObjectSector()} returns null after calling this
* method.
*/
public void clearObjectList()
{
this.intersectingObjects = null;
this.objectSector = null;
}
/**
* Adds the specified surface renderable to the tile's list of intersecting objects.
*
* @param so the surface renderable to add.
* @param sector the sector bounding the specified surface renderable.
*/
public void addSurfaceObject(SurfaceRenderable so, Sector sector)
{
if (this.intersectingObjects == null)
this.intersectingObjects = new ArrayList();
this.intersectingObjects.add(so);
this.objectSector = (this.objectSector != null) ? this.objectSector.union(sector) : sector;
}
/**
* Adds the specified collection of surface renderables to the tile's list of intersecting objects.
*
* @param c the collection of surface renderables to add.
* @param sector the sector bounding the specified surface renderable collection.
*/
public void addAllSurfaceObjects(List c, Sector sector)
{
if (this.intersectingObjects == null)
this.intersectingObjects = new ArrayList();
this.intersectingObjects.addAll(c);
this.objectSector = (this.objectSector != null) ? this.objectSector.union(sector) : sector;
}
/**
* {@inheritDoc}
*
* Overridden to return a new SurfaceObjectTile. The returned tile is created with the same cache name as this
* tile.
*/
@Override
protected TextureTile createSubTile(Sector sector, Level level, int row, int col)
{
return new SurfaceObjectTile(sector, level, row, col, this.getCacheName());
}
/**
* {@inheritDoc}
*
* Overridden to return a TileKey with the same cache name as this tile.
*/
@Override
protected TileKey createSubTileKey(Level level, int row, int col)
{
return new TileKey(level.getLevelNumber(), row, col, this.getCacheName());
}
}
/**
* Represents a surface renderable tile's current state. TileStateKey distinguishes the tile's state by comparing
* the individual state keys of the surface renderables intersecting the tile. This does not retain any references
* to the surface renderables themselves. Should the tile state key live longer than the surface renderables, the
* state key does not prevent those objects from being reclaimed by the garbage collector.
*/
protected static class SurfaceObjectTileStateKey implements Cacheable
{
protected final TileKey tileKey;
protected final Object[] intersectingObjectKeys;
/**
* Construsts a tile state key for the specified surface renderable tile.
*
* @param dc the draw context the state key is related to.
* @param tile the tile to construct a state key for.
*/
public SurfaceObjectTileStateKey(DrawContext dc, SurfaceObjectTile tile)
{
if (tile != null && tile.hasObjects())
{
this.tileKey = tile.getTileKey();
this.intersectingObjectKeys = new Object[tile.getObjectList().size()];
int index = 0;
for (SurfaceRenderable so : tile.getObjectList())
{
this.intersectingObjectKeys[index++] = so.getStateKey(dc);
}
}
else
{
this.tileKey = null;
this.intersectingObjectKeys = null;
}
}
@Override
public boolean equals(Object o)
{
if (this == o)
return true;
if (o == null || this.getClass() != o.getClass())
return false;
// Compare the tile keys and each state key in the array. The state keys are equal if the tile keys are
// equal, the arrays equivalent length, and each array element is equivalent. Arrays.equals() correctly
// handles null references.
SurfaceObjectTileStateKey that = (SurfaceObjectTileStateKey) o;
return (this.tileKey != null ? this.tileKey.equals(that.tileKey) : that.tileKey == null)
&& Arrays.equals(this.intersectingObjectKeys, that.intersectingObjectKeys);
}
@Override
public int hashCode()
{
int result = this.tileKey != null ? this.tileKey.hashCode() : 0;
result = 31 * result + Arrays.hashCode(this.intersectingObjectKeys); // Correctly handles a null reference.
return result;
}
/**
* Returns the tile state key's size in bytes. The total size of the intersecting object keys, plus the size of
* the array itself. The tileKey is owned by the SurfaceObjectTile, so we don't include it in the state key's
* size.
*
* @return The state key's size in bytes.
*/
public long getSizeInBytes()
{
if (this.intersectingObjectKeys == null)
return 0;
long size = 4 * this.intersectingObjectKeys.length; // For the array references.
for (Object o : this.intersectingObjectKeys)
{
if (o instanceof Cacheable)
size += ((Cacheable) o).getSizeInBytes();
else if (o != null)
size += 4; // If the object doesn't implement Cacheable, just account for the reference to it.
}
return size;
}
}
}