/*
* 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.charts.slippy;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.metsci.glimpse.axis.Axis2D;
import com.metsci.glimpse.context.GlimpseContext;
import com.metsci.glimpse.gl.texture.DrawableTexture;
import com.metsci.glimpse.painter.base.GlimpsePainterBase;
import com.metsci.glimpse.painter.texture.ShadedTexturePainter;
import com.metsci.glimpse.painter.texture.TextureUnit;
import com.metsci.glimpse.support.projection.LatLonProjection;
import com.metsci.glimpse.support.projection.Projection;
import com.metsci.glimpse.support.shader.triangle.ColorTexture2DProgram;
import com.metsci.glimpse.support.texture.RGBTextureProjected2D;
import com.metsci.glimpse.support.texture.TextureProjected2D;
import com.metsci.glimpse.util.geo.LatLonGeo;
import com.metsci.glimpse.util.geo.projection.GeoProjection;
import com.metsci.glimpse.util.vector.Vector2d;
/**
* Paints the slippy tiles. New tiles are fetched dynamically as the user zooms/pans and stale textures are removed.
* @author oren
*/
public class SlippyMapTilePainter extends ShadedTexturePainter
{
private static final Logger logger = Logger.getLogger( SlippyCache.class.getName( ) );
private static final double LOG2 = Math.log( 2 );
private final GeoProjection geoProj;
private final int maxZoom;
private final SlippyProjection[] slippyProj;
private final ExecutorService exec;
private final SlippyCache cache;
/**
* These are checked during the asynchronous texture fetch to see if we still need to fetch it,
* and after we fetch it if we still need to display it.
*/
private final AtomicReference<double[]> lastBounds = new AtomicReference<>( );
private final AtomicInteger lastZoom = new AtomicInteger( );
/**
* We need to know what zoom level each texture lives at so we can remove stale textures
* when the zoom level changes.
*/
private final ConcurrentHashMap<RGBTextureProjected2D, Integer> texZoomMap = new ConcurrentHashMap<>( );
public SlippyMapTilePainter( GeoProjection geoProj, List<String> prefixes, ExecutorService exec, Path cacheDir, int maxZoom )
{
this.geoProj = geoProj;
this.cache = new SlippyCache( geoProj, prefixes, cacheDir );
this.maxZoom = maxZoom;
this.slippyProj = new SlippyProjection[maxZoom + 1];
for ( int zoom = 0; zoom <= maxZoom; zoom++ )
{
this.slippyProj[zoom] = new SlippyProjection( zoom );
}
this.exec = exec;
this.setProgram( new ColorTexture2DProgram( ) );
}
@Override
public void doPaintTo( GlimpseContext context )
{
updateTiles( GlimpsePainterBase.getAxis2D( context ) );
super.doPaintTo( context );
}
protected void updateTiles( Axis2D axis )
{
int xPix = axis.getAxisX( ).getSizePixels( );
if ( !isVisible( ) || xPix <= 0 )
{
return;
}
double minx = axis.getMinX( );
double maxx = axis.getMaxX( );
double maxy = axis.getMaxY( );
double miny = axis.getMinY( );
double[] bounds = new double[] { minx, maxx, miny, maxy };
if ( Arrays.equals( bounds, lastBounds.get( ) ) )
{
return;
}
lastBounds.set( bounds );
double xTileDim = xPix / 256.;
LatLonGeo ne = geoProj.unproject( maxx, maxy );
LatLonGeo sw = geoProj.unproject( minx, miny );
double east = ne.getLonDeg( );
double west = sw.getLonDeg( );
double lonSizeDeg = ( east - west ) / xTileDim;
double zoomApprox = Math.log( 360 / lonSizeDeg ) / LOG2;
final int zoom = ( int ) Math.min( Math.round( zoomApprox ), maxZoom );
lastZoom.set( zoom );
Vector2d tileNE = slippyProj[zoom].project( ne );
Vector2d tileSW = slippyProj[zoom].project( sw );
final int tileYmin = ( int ) Math.floor( tileNE.getY( ) );
final int tileYmax = ( int ) Math.ceil( tileSW.getY( ) );
final int tileXmin = ( int ) Math.floor( tileSW.getX( ) );
final int tileXmax = ( int ) Math.ceil( tileNE.getX( ) );
painterLock.lock( );
try
{
for ( int y = tileYmin; y < tileYmax; y++ )
{
for ( int x = tileXmin; x < tileXmax; x++ )
{
RGBTextureProjected2D tex = cache.getTextureIfPresent( zoom, x, y );
if ( tex != null && drawableTextures.containsKey( new TextureUnit<DrawableTexture>( tex ) ) )
{
continue;
}
exec.submit( new FetchTexture( zoom, x, y ) );
}
}
Iterator<TextureUnit<DrawableTexture>> itr = drawableTextures.keySet( ).iterator( );
while ( itr.hasNext( ) )
{
TextureUnit<DrawableTexture> texUnit = itr.next( );
TextureProjected2D tex = ( TextureProjected2D ) texUnit.getTexture( );
double[] texBounds = getBounds( tex.getProjection( ) );
int texZoom = texZoomMap.get( tex );
if ( texZoom != zoom || !intersect( bounds, texBounds ) )
{
itr.remove( );
texZoomMap.remove( tex );
}
}
}
finally
{
painterLock.unlock( );
}
}
private double[] getBounds( final Projection proj )
{
float[] min = new float[2];
float[] max = new float[2];
proj.getVertexXY( 0, 0, min );
proj.getVertexXY( 1, 1, max );
return new double[] { min[0], max[0], min[1], max[1] };
}
private double[] getBounds( int zoom, int tileX, int tileY )
{
return getBounds( getProjection( zoom, tileX, tileY ) );
}
private Projection getProjection( int zoom, int x, int y )
{
LatLonGeo nw = slippyProj[zoom].unproject( x, y );
LatLonGeo se = slippyProj[zoom].unproject( x + 1, y + 1 );
double minLat = se.getLatDeg( );
double maxLat = nw.getLatDeg( );
double minLon = nw.getLonDeg( );
double maxLon = se.getLonDeg( );
return new LatLonProjection( geoProj, minLat, maxLat, minLon, maxLon, false );
}
private static boolean intersect( final double[] outerBounds, final double[] bounds )
{
return contains( outerBounds, bounds[0], bounds[2] )
|| contains( outerBounds, bounds[0], bounds[3] )
|| contains( outerBounds, bounds[1], bounds[2] )
|| contains( outerBounds, bounds[1], bounds[3] )
|| contains( bounds, outerBounds[0], outerBounds[2] )
|| contains( bounds, outerBounds[0], outerBounds[3] )
|| contains( bounds, outerBounds[1], outerBounds[2] )
|| contains( bounds, outerBounds[1], outerBounds[3] );
}
private static boolean contains( final double[] bounds, final double x, final double y )
{
return ! ( x < bounds[0] || bounds[1] < x || y < bounds[2] || bounds[3] < y );
}
private final class FetchTexture implements Runnable
{
private final int zoom;
private final int x;
private final int y;
private FetchTexture( int zoom, int x, int y )
{
this.zoom = zoom;
this.x = x;
this.y = y;
}
@Override
public void run( )
{
final double[] bounds = getBounds( zoom, x, y );
try
{
RGBTextureProjected2D tex = null;
int zoomCheck = lastZoom.get( );
double[] boundsCheck = lastBounds.get( );
if ( zoom == zoomCheck && intersect( boundsCheck, bounds ) )
{
tex = cache.getTexture( zoom, x, y );
}
if ( tex == null )
{
return;
}
zoomCheck = lastZoom.get( );
boundsCheck = lastBounds.get( );
if ( zoom == zoomCheck && intersect( boundsCheck, bounds ) )
{
painterLock.lock( );
try
{
addDrawableTexture( tex );
texZoomMap.put( tex, zoom );
}
finally
{
painterLock.unlock( );
}
}
}
catch ( Exception e )
{
logger.log( Level.WARNING, "Exception in tile fetching thread", e );
}
finally
{
//nothing?
}
}
}
}