/*
* Copyright (c) 2016, Metron, Inc.
* All rights reserved.
*
* 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 Metron, Inc. 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 METRON, INC. 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) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.metsci.glimpse.worldwind.tile;
import static com.metsci.glimpse.util.logging.LoggerUtils.*;
import java.awt.Rectangle;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
import javax.media.opengl.GL2;
import javax.media.opengl.GLContext;
import com.metsci.glimpse.axis.Axis2D;
import com.metsci.glimpse.canvas.FBOGlimpseCanvas;
import com.metsci.glimpse.canvas.GlimpseCanvas;
import com.metsci.glimpse.context.GlimpseTargetStack;
import com.metsci.glimpse.context.TargetStackUtil;
import com.metsci.glimpse.layout.GlimpseLayout;
import com.metsci.glimpse.painter.decoration.BackgroundPainter;
import com.metsci.glimpse.util.geo.LatLonGeo;
import com.metsci.glimpse.util.geo.projection.GeoProjection;
import com.metsci.glimpse.util.units.Azimuth;
import com.metsci.glimpse.util.units.Length;
import com.metsci.glimpse.util.vector.Vector2d;
import gov.nasa.worldwind.View;
import gov.nasa.worldwind.geom.LatLon;
import gov.nasa.worldwind.geom.Position;
import gov.nasa.worldwind.geom.Vec4;
import gov.nasa.worldwind.layers.AbstractLayer;
import gov.nasa.worldwind.render.DrawContext;
import gov.nasa.worldwind.render.PreRenderable;
import gov.nasa.worldwind.render.Renderable;
import gov.nasa.worldwind.util.OGLStackHandler;
/**
* Displays the content of a GlimpseLayout onto the surface of the Worldwind globe
* and dynamically adjusts the surface area of the tile to just fill the screen (and no more)
* to ensure that the visible areas receive maximum texture resolution.
*
* @author ulman
*/
public class GlimpseDynamicSurfaceTile extends AbstractLayer implements GlimpseSurfaceTile, Renderable, PreRenderable
{
private static final Logger logger = Logger.getLogger( GlimpseDynamicSurfaceTile.class.getSimpleName( ) );
protected static final int HEURISTIC_ALTITUDE_CUTOFF = 800;
protected GlimpseLayout background;
protected GlimpseLayout mask;
protected GlimpseLayout layout;
protected Axis2D axes;
protected GeoProjection projection;
protected int width, height;
protected LatLonBounds maxBounds;
protected List<LatLon> maxCorners;
protected LatLonBounds bounds;
protected List<LatLon> corners;
protected boolean isMax = true;
protected FBOGlimpseCanvas offscreenCanvas;
protected TextureSurfaceTile tile;
public GlimpseDynamicSurfaceTile( GlimpseLayout layout, Axis2D axes, GeoProjection projection, int width, int height, double minLat, double maxLat, double minLon, double maxLon )
{
this( layout, axes, projection, width, height, getCorners( new LatLonBounds( minLat, maxLat, minLon, maxLon ) ) );
}
public GlimpseDynamicSurfaceTile( GlimpseLayout layout, Axis2D axes, GeoProjection projection, int width, int height, List<LatLon> corners )
{
this.axes = axes;
this.projection = projection;
this.layout = layout;
this.width = width;
this.height = height;
updateMaxCorners( corners );
this.mask = new GlimpseLayout( );
this.mask.setLayoutData( String.format( "pos 0 0 %d %d", width, height ) );
this.mask.addLayout( layout );
this.background = new GlimpseLayout( );
this.background.addPainter( new BackgroundPainter( ).setColor( 0f, 0f, 0f, 0f ) );
this.background.addLayout( mask );
}
@Override
public void setOpacity( double opacity )
{
super.setOpacity( opacity );
if ( this.tile != null ) this.tile.setOpacity( opacity );
}
/**
* @deprecated use {@link #setOpacity(double)} instead
*/
@Deprecated
public void setAlpha( float alpha )
{
this.setOpacity( alpha );
}
public void updateMaxCorners( List<LatLon> corners )
{
this.maxBounds = getCorners( corners );
this.maxCorners = getCorners( this.maxBounds );
}
@Override
public GlimpseLayout getGlimpseLayout( )
{
return this.layout;
}
@Override
public GlimpseCanvas getGlimpseCanvas( )
{
return this.offscreenCanvas;
}
@Override
public GlimpseTargetStack getTargetStack( )
{
if ( this.offscreenCanvas != null )
{
return TargetStackUtil.newTargetStack( this.offscreenCanvas, this.layout );
}
else
{
return null;
}
}
@Override
public void preRender( DrawContext dc )
{
if ( tile == null && offscreenCanvas == null )
{
offscreenCanvas = new FBOGlimpseCanvas( dc.getGLContext( ), width, height );
offscreenCanvas.addLayout( background );
}
if ( offscreenCanvas.getGLContext( ) != null )
{
updateGeometry( dc );
drawOffscreen( dc );
if ( tile == null && corners != null )
{
int textureHandle = getTextureHandle( );
tile = newTextureSurfaceTile( textureHandle, corners );
}
}
}
@Override
protected void doRender( DrawContext dc )
{
if ( tile != null )
{
tile.render( dc );
}
}
protected void drawOffscreen( DrawContext dc )
{
drawOffscreen( dc.getGLContext( ) );
}
protected void drawOffscreen( GLContext glContext )
{
OGLStackHandler stack = new OGLStackHandler( );
GL2 gl = glContext.getGL( ).getGL2( );
stack.pushAttrib( gl, GL2.GL_ALL_ATTRIB_BITS );
stack.pushClientAttrib( gl, ( int ) GL2.GL_ALL_CLIENT_ATTRIB_BITS );
stack.pushTexture( gl );
stack.pushModelview( gl );
stack.pushProjection( gl );
GLContext c = offscreenCanvas.getGLContext( );
if ( c != null )
{
c.makeCurrent( );
try
{
offscreenCanvas.paint( );
}
catch ( Exception e )
{
logWarning( logger, "Trouble drawing to offscreen buffer", e );
}
finally
{
glContext.makeCurrent( );
stack.pop( gl );
}
}
}
protected int getTextureHandle( )
{
return offscreenCanvas.getTextureName( );
}
protected TextureSurfaceTile newTextureSurfaceTile( int textureHandle, Iterable<? extends LatLon> corners )
{
TextureSurfaceTile tile = new TextureSurfaceTile( textureHandle, corners );
tile.setOpacity( getOpacity( ) );
return tile;
}
protected void updateGeometry( DrawContext dc )
{
List<LatLon> screenCorners1 = getCornersHeuristic1( dc );
List<LatLon> screenCorners2 = getCornersHeuristic2( dc );
if ( isValid( screenCorners1 ) && isValid( screenCorners2 ) )
{
updateGeometry( screenCorners2 );
}
else
{
updateGeometryDefault( );
}
}
protected void updateGeometryDefault( )
{
corners = maxCorners;
bounds = maxBounds;
isMax = true;
updateTile( );
}
protected void updateGeometry( List<LatLon> screenCorners )
{
LatLonBounds outerBounds = getBoundsFromCorners( screenCorners, 0.9 );
LatLonBounds innerBounds = getBoundsFromCorners( screenCorners, 0.4 );
// Update the geometry if:
//
// 1) we were previously showing the max tile
// 2) the view has been panned to near the edge of the current tile
// 3) the view has been zoomed in by 10% or more (in visible area)
//
// This is done (instead of updating every time the bounds change by any amount
// to make slight numerical differences which cause jitter in the on map position
// of elements in the glimpse generated image.
if ( isMax || !contains( bounds, innerBounds ) || getArea( bounds ) > getArea( outerBounds ) * 1.1 )
{
bounds = outerBounds;
corners = getCorners( bounds );
isMax = false;
updateTile( );
}
}
protected void updateTile( )
{
if ( tile != null )
{
setAxes( axes, bounds, projection );
tile.setCorners( corners );
}
}
protected LatLonBounds getBoundsFromCorners( List<LatLon> screenCorners, double bufferFactor )
{
LatLonBounds bounds = getCorners( screenCorners );
LatLonBounds bufferedBounds = bufferCorners( bounds, bufferFactor );
LatLonBounds intersectedBounds = getIntersectedCorners( maxBounds, bufferedBounds );
return intersectedBounds;
}
protected double getArea( LatLonBounds outerBounds )
{
return ( outerBounds.maxLat - outerBounds.minLat ) * ( outerBounds.maxLon - outerBounds.minLon );
}
protected boolean contains( LatLonBounds outerBounds, LatLonBounds innerBounds )
{
return outerBounds.maxLat > innerBounds.maxLat &&
outerBounds.minLat < innerBounds.minLat &&
outerBounds.maxLon > innerBounds.maxLon &&
outerBounds.minLon < innerBounds.minLon;
}
protected void setAxes( Axis2D axes, LatLonBounds bounds, GeoProjection projection )
{
Vector2d c1 = projection.project( LatLonGeo.fromDeg( bounds.minLat, bounds.minLon ) );
Vector2d c2 = projection.project( LatLonGeo.fromDeg( bounds.maxLat, bounds.minLon ) );
Vector2d c3 = projection.project( LatLonGeo.fromDeg( bounds.maxLat, bounds.maxLon ) );
Vector2d c4 = projection.project( LatLonGeo.fromDeg( bounds.minLat, bounds.maxLon ) );
double minX = minX( c1, c2, c3, c4 );
double maxX = maxX( c1, c2, c3, c4 );
double minY = minY( c1, c2, c3, c4 );
double maxY = maxY( c1, c2, c3, c4 );
axes.set( minX, maxX, minY, maxY );
axes.getAxisX( ).validate( );
axes.getAxisY( ).validate( );
}
public static double minX( Vector2d... corners )
{
double min = Double.POSITIVE_INFINITY;
for ( Vector2d corner : corners )
{
if ( corner.getX( ) < min ) min = corner.getX( );
}
return min;
}
public static double minY( Vector2d... corners )
{
double min = Double.POSITIVE_INFINITY;
for ( Vector2d corner : corners )
{
if ( corner.getY( ) < min ) min = corner.getY( );
}
return min;
}
public static double maxX( Vector2d... corners )
{
double max = Double.NEGATIVE_INFINITY;
for ( Vector2d corner : corners )
{
if ( corner.getX( ) > max ) max = corner.getX( );
}
return max;
}
public static double maxY( Vector2d... corners )
{
double max = Double.NEGATIVE_INFINITY;
for ( Vector2d corner : corners )
{
if ( corner.getY( ) > max ) max = corner.getY( );
}
return max;
}
public static boolean isValid( List<LatLon> screenCorners )
{
if ( screenCorners == null ) return false;
for ( LatLon latlon : screenCorners )
{
if ( latlon == null ) return false;
}
return true;
}
public static LatLonBounds bufferCorners( LatLonBounds corners, double bufferFraction )
{
double diffLat = corners.maxLat - corners.minLat;
double diffLon = corners.maxLon - corners.minLon;
double buffMinLat = corners.minLat - diffLat * bufferFraction;
double buffMaxLat = corners.maxLat + diffLat * bufferFraction;
double buffMinLon = corners.minLon - diffLon * bufferFraction;
double buffMaxLon = corners.maxLon + diffLon * bufferFraction;
return new LatLonBounds( buffMinLat, buffMaxLat, buffMinLon, buffMaxLon );
}
public static LatLonBounds getCorners( List<LatLon> screenCorners )
{
double minLat = Double.POSITIVE_INFINITY;
double minLon = Double.POSITIVE_INFINITY;
double maxLat = Double.NEGATIVE_INFINITY;
double maxLon = Double.NEGATIVE_INFINITY;
for ( LatLon latlon : screenCorners )
{
double lat = latlon.getLatitude( ).getDegrees( );
double lon = latlon.getLongitude( ).getDegrees( );
if ( lat < minLat ) minLat = lat;
if ( lat > maxLat ) maxLat = lat;
if ( lon < minLon ) minLon = lon;
if ( lon > maxLon ) maxLon = lon;
}
return new LatLonBounds( minLat, maxLat, minLon, maxLon );
}
public static LatLonBounds getUnionedCorners( LatLonBounds corners1, LatLonBounds corners2 )
{
double minLat = Math.min( corners1.minLat, corners2.minLat );
double minLon = Math.min( corners1.minLon, corners2.minLon );
double maxLat = Math.max( corners1.maxLat, corners2.maxLat );
double maxLon = Math.max( corners1.maxLon, corners2.maxLon );
return new LatLonBounds( minLat, maxLat, minLon, maxLon );
}
public static LatLonBounds getIntersectedCorners( LatLonBounds corners1, LatLonBounds corners2 )
{
double minLat = Math.max( corners1.minLat, corners2.minLat );
double minLon = Math.max( corners1.minLon, corners2.minLon );
double maxLat = Math.min( corners1.maxLat, corners2.maxLat );
double maxLon = Math.min( corners1.maxLon, corners2.maxLon );
return new LatLonBounds( minLat, maxLat, minLon, maxLon );
}
public static List<LatLon> getCorners( LatLonBounds bounds )
{
List<LatLon> corners = new ArrayList<LatLon>( );
corners.add( LatLon.fromDegrees( bounds.minLat, bounds.minLon ) );
corners.add( LatLon.fromDegrees( bounds.minLat, bounds.maxLon ) );
corners.add( LatLon.fromDegrees( bounds.maxLat, bounds.maxLon ) );
corners.add( LatLon.fromDegrees( bounds.maxLat, bounds.minLon ) );
return corners;
}
// a heuristic for calculating the corners of the visible region
public static List<LatLon> getCornersHeuristic1( DrawContext dc )
{
View view = dc.getView( );
Rectangle viewport = view.getViewport( );
List<LatLon> corners = new ArrayList<LatLon>( 4 );
corners.add( view.computePositionFromScreenPoint( viewport.getMinX( ), viewport.getMinY( ) ) );
corners.add( view.computePositionFromScreenPoint( viewport.getMinX( ), viewport.getMaxY( ) ) );
corners.add( view.computePositionFromScreenPoint( viewport.getMaxX( ), viewport.getMaxY( ) ) );
corners.add( view.computePositionFromScreenPoint( viewport.getMaxX( ), viewport.getMinY( ) ) );
return corners;
}
// another possible heuristic for calculating the corners of the visible region
// inspired by: gov.nasa.worldwind.layers.ScalebarLayer
public static List<LatLon> getCornersHeuristic2( DrawContext dc )
{
double x = dc.getView( ).getViewport( ).getWidth( ) / 2;
double y = dc.getView( ).getViewport( ).getHeight( ) / 2;
Position centerPosition = dc.getView( ).computePositionFromScreenPoint( x, y );
Vec4 center = dc.getGlobe( ).computePointFromPosition( centerPosition );
Vec4 eye = dc.getView( ).getEyePoint( );
Double distance = center.distanceTo3( eye );
double metersPerPixel = dc.getView( ).computePixelSizeAtDistance( distance );
// now assume this size roughly holds across the whole screen
// (which is an ok assumption when we're zoomed in)
View view = dc.getView( );
Rectangle viewport = view.getViewport( );
double viewportHeightMeters = viewport.getHeight( ) * metersPerPixel;
double viewportWidthMeters = viewport.getWidth( ) * metersPerPixel;
// in order to not worry about how the viewport is rotated
// (which direction is north) just take the largest dimension
double viewportSizeMeters = Math.max( viewportHeightMeters, viewportWidthMeters );
LatLonGeo centerLatLon = LatLonGeo.fromDeg( centerPosition.latitude.getDegrees( ), centerPosition.longitude.getDegrees( ) );
LatLonGeo swLatLon = centerLatLon.displacedBy( Length.fromMeters( viewportSizeMeters ), Azimuth.southwest );
LatLonGeo seLatLon = centerLatLon.displacedBy( Length.fromMeters( viewportSizeMeters ), Azimuth.southeast );
LatLonGeo nwLatLon = centerLatLon.displacedBy( Length.fromMeters( viewportSizeMeters ), Azimuth.northwest );
LatLonGeo neLatLon = centerLatLon.displacedBy( Length.fromMeters( viewportSizeMeters ), Azimuth.northeast );
Position swPos = Position.fromDegrees( swLatLon.getLatDeg( ), swLatLon.getLonDeg( ) );
Position sePos = Position.fromDegrees( seLatLon.getLatDeg( ), seLatLon.getLonDeg( ) );
Position nwPos = Position.fromDegrees( nwLatLon.getLatDeg( ), nwLatLon.getLonDeg( ) );
Position nePos = Position.fromDegrees( neLatLon.getLatDeg( ), neLatLon.getLonDeg( ) );
List<LatLon> corners = new ArrayList<LatLon>( 4 );
corners.add( swPos );
corners.add( sePos );
corners.add( nwPos );
corners.add( nePos );
return corners;
}
public static class LatLonBounds
{
public double minLat, maxLat, minLon, maxLon;
public LatLonBounds( double minLat, double maxLat, double minLon, double maxLon )
{
this.minLat = minLat;
this.maxLat = maxLat;
this.minLon = minLon;
this.maxLon = maxLon;
}
@Override
public String toString( )
{
return String.format( "%f %f %f %f", minLat, maxLat, minLon, maxLon );
}
}
}