/* * 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.dnc; import static com.google.common.base.Objects.equal; import static com.jogamp.common.nio.Buffers.SIZEOF_FLOAT; import static com.metsci.glimpse.dnc.DncChunks.createHostChunk; import static com.metsci.glimpse.dnc.DncChunks.xferChunkToDevice; import static com.metsci.glimpse.dnc.DncIconAtlases.createHostIconAtlas; import static com.metsci.glimpse.dnc.DncIconAtlases.xferIconAtlasToDevice; import static com.metsci.glimpse.dnc.DncLabelAtlases.coordsPerLabelAtlasAlign; import static com.metsci.glimpse.dnc.DncLabelAtlases.coordsPerLabelAtlasBounds; import static com.metsci.glimpse.dnc.DncLabelAtlases.createHostLabelAtlas; import static com.metsci.glimpse.dnc.DncLabelAtlases.xferLabelAtlasToDevice; import static com.metsci.glimpse.dnc.DncPainterUtils.coverageSignificanceComparator; import static com.metsci.glimpse.dnc.DncPainterUtils.groupRenderingOrder; import static com.metsci.glimpse.dnc.DncShaderUtils.setUniformAxisRect; import static com.metsci.glimpse.dnc.DncShaderUtils.setUniformViewport; import static com.metsci.glimpse.dnc.convert.Render.coordsPerRenderIconVertex; import static com.metsci.glimpse.dnc.convert.Render.coordsPerRenderLabelVertex; import static com.metsci.glimpse.dnc.convert.Render.coordsPerRenderLineVertex; import static com.metsci.glimpse.dnc.convert.Render.coordsPerRenderTriangleVertex; import static com.metsci.glimpse.dnc.geosym.DncGeosymIo.readGeosymColors; import static com.metsci.glimpse.dnc.geosym.DncGeosymIo.readGeosymLineAreaStyles; import static com.metsci.glimpse.dnc.geosym.DncGeosymThemes.DNC_THEME_STANDARD; import static com.metsci.glimpse.dnc.util.DncMiscUtils.newWorkerDaemon; import static com.metsci.glimpse.dnc.util.DncMiscUtils.sorted; import static com.metsci.glimpse.dnc.util.DncMiscUtils.timeSince_MILLIS; import static com.metsci.glimpse.painter.base.GlimpsePainterBase.getBounds; import static com.metsci.glimpse.painter.base.GlimpsePainterBase.requireAxis2D; import static com.metsci.glimpse.util.logging.LoggerUtils.getLogger; import static java.lang.Math.PI; import static java.lang.Math.cos; import static java.lang.System.currentTimeMillis; import static java.util.Collections.emptyMap; import static java.util.Collections.singleton; import static java.util.Collections.sort; import static javax.media.opengl.GL.GL_ARRAY_BUFFER; import static javax.media.opengl.GL.GL_BLEND; import static javax.media.opengl.GL.GL_FLOAT; import static javax.media.opengl.GL.GL_LINE_STRIP; import static javax.media.opengl.GL.GL_MAX_TEXTURE_SIZE; import static javax.media.opengl.GL.GL_ONE; import static javax.media.opengl.GL.GL_ONE_MINUS_SRC_ALPHA; import static javax.media.opengl.GL.GL_POINTS; import static javax.media.opengl.GL.GL_TEXTURE0; import static javax.media.opengl.GL.GL_TEXTURE_2D; import static javax.media.opengl.GL.GL_TRIANGLES; import java.awt.Color; import java.nio.CharBuffer; import java.nio.FloatBuffer; import java.nio.IntBuffer; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; import java.util.function.Consumer; import java.util.function.Function; import java.util.logging.Logger; import javax.media.opengl.GL2ES2; import com.metsci.glimpse.axis.Axis1D; import com.metsci.glimpse.axis.Axis2D; import com.metsci.glimpse.axis.listener.AxisListener1D; import com.metsci.glimpse.context.GlimpseBounds; import com.metsci.glimpse.context.GlimpseContext; import com.metsci.glimpse.dnc.DncAreaProgram.DncAreaProgramHandles; import com.metsci.glimpse.dnc.DncAtlases.DncAtlasEntry; import com.metsci.glimpse.dnc.DncChunks.DncChunkKey; import com.metsci.glimpse.dnc.DncChunks.DncDeviceChunk; import com.metsci.glimpse.dnc.DncChunks.DncGroup; import com.metsci.glimpse.dnc.DncChunks.DncHostChunk; import com.metsci.glimpse.dnc.DncIconAtlases.DncDeviceIconAtlas; import com.metsci.glimpse.dnc.DncIconAtlases.DncHostIconAtlas; import com.metsci.glimpse.dnc.DncIconProgram.DncIconProgramHandles; import com.metsci.glimpse.dnc.DncLabelAtlases.DncDeviceLabelAtlas; import com.metsci.glimpse.dnc.DncLabelAtlases.DncHostLabelAtlas; import com.metsci.glimpse.dnc.DncLabelProgram.DncLabelProgramHandles; import com.metsci.glimpse.dnc.DncLineProgram.DncLineProgramHandles; import com.metsci.glimpse.dnc.convert.Flat2Render.DncChunkPriority; import com.metsci.glimpse.dnc.convert.Flat2Render.RenderCache; import com.metsci.glimpse.dnc.convert.Render.RenderChunk; import com.metsci.glimpse.dnc.geosym.DncGeosymAssignment; import com.metsci.glimpse.dnc.geosym.DncGeosymLineAreaStyle; import com.metsci.glimpse.dnc.geosym.DncGeosymTheme; import com.metsci.glimpse.dnc.util.DncMiscUtils.ThrowingRunnable; import com.metsci.glimpse.dnc.util.RateLimitedAxisLimitsListener1D; import com.metsci.glimpse.gl.util.GLUtils; import com.metsci.glimpse.painter.base.GlimpsePainter; import com.metsci.glimpse.support.settings.LookAndFeel; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.ints.IntCollection; public class DncPainter implements GlimpsePainter { protected static final Logger logger = getLogger( DncPainter.class ); // XXX: Maybe move these to settings protected static final long chunkDisposeTimeLimit_MILLIS = 3; protected static final int guaranteedChunkDisposalsPerFrame = 1; protected static final long highlightSetDisposeTimeLimit_MILLIS = 3; protected static final int guaranteedHighlightSetDisposalsPerFrame = 1; protected static final long iconAtlasDisposeTimeLimit_MILLIS = 3; protected static final int guaranteedIconAtlasDisposalsPerFrame = 1; protected static final long labelAtlasDisposeTimeLimit_MILLIS = 3; protected static final int guaranteedLabelAtlasDisposalsPerFrame = 1; protected static final long chunkXferTimeLimit_MILLIS = 5; protected static final int guaranteedChunkXfersPerFrame = 1; protected static final long iconAtlasXferTimeLimit_MILLIS = 3; protected static final int guaranteedIconAtlasXfersPerFrame = 1; protected static final long labelAtlasXferTimeLimit_MILLIS = 1; protected static final int guaranteedLabelAtlasXfersPerFrame = 1; protected static class RasterizeArgs { public final int maxTextureDim; public final double screenDpi; public RasterizeArgs( int maxTextureDim, double screenDpi ) { this.maxTextureDim = maxTextureDim; this.screenDpi = screenDpi; } } protected final Object mutex; protected final ExecutorService asyncExec; protected final ExecutorService iconsExec; protected final ExecutorService labelsExec; protected final RenderCache cache; protected final DncPainterSettings settings; protected final Collection<DncLibrary> allLibraries; protected final Map<DncChunkKey,DncHostChunk> hChunks; protected final Map<DncChunkKey,DncDeviceChunk> dChunks; protected final List<DncDeviceChunk> dChunksToDispose; protected final Map<DncChunkKey,DncHostIconAtlas> hIconAtlases; protected final Map<DncChunkKey,DncDeviceIconAtlas> dIconAtlases; protected final List<DncDeviceIconAtlas> dIconAtlasesToDispose; protected final Map<DncChunkKey,DncHostLabelAtlas> hLabelAtlases; protected final Map<DncChunkKey,DncDeviceLabelAtlas> dLabelAtlases; protected final List<DncDeviceLabelAtlas> dLabelAtlasesToDispose; protected final Map<DncChunkKey,IndexSetTexture> highlightSets; protected final List<IndexSetTexture> highlightSetsToDispose; protected final DncAreaProgram areaProgram; protected final DncLineProgram lineProgram; protected final DncIconProgram iconProgram; protected final DncLabelProgram labelProgram; protected final Set<Axis2D> axes; protected final AxisListener1D axisListener; protected final Set<DncLibrary> activeLibraries; protected final Set<DncCoverage> activeCoverages; protected final CopyOnWriteArrayList<Runnable> activeChunksListeners; public final Function<DncChunkKey,DncChunkPriority> chunkPriorityFunc; protected boolean visible; protected DncGeosymTheme theme; protected Map<String,DncGeosymLineAreaStyle> lineAreaStyles; protected RasterizeArgs rasterizeArgs; // Accessed only on the labels thread protected Int2ObjectMap<Color> labelColors; protected String labelColorsFile; public DncPainter( RenderCache cache, DncPainterSettings settings ) { this( cache, settings, DNC_THEME_STANDARD ); } public DncPainter( RenderCache cache, DncPainterSettings settings, DncGeosymTheme theme ) { this.mutex = new Object( ); this.asyncExec = newWorkerDaemon( "DncPainter.Async." ); this.iconsExec = newWorkerDaemon( "DncPainter.Icons." ); this.labelsExec = newWorkerDaemon( "DncPainter.Labels." ); this.cache = cache; this.settings = settings; this.allLibraries = cache.libraries; this.hChunks = new HashMap<>( ); this.dChunks = new HashMap<>( ); this.dChunksToDispose = new ArrayList<>( ); this.hIconAtlases = new HashMap<>( ); this.dIconAtlases = new HashMap<>( ); this.dIconAtlasesToDispose = new ArrayList<>( ); this.hLabelAtlases = new HashMap<>( ); this.dLabelAtlases = new HashMap<>( ); this.dLabelAtlasesToDispose = new ArrayList<>( ); this.highlightSets = new HashMap<>( ); this.highlightSetsToDispose = new ArrayList<>( ); this.areaProgram = new DncAreaProgram( ); this.lineProgram = new DncLineProgram( ); this.iconProgram = new DncIconProgram( ); this.labelProgram = new DncLabelProgram( ); this.axes = new HashSet<>( ); this.axisListener = new RateLimitedAxisLimitsListener1D( ) { public void axisLimitsUpdatedRateLimited( Axis1D axis ) { updateActiveLibraries( allLibraries ); } }; this.activeLibraries = new HashSet<>( ); this.activeCoverages = new HashSet<>( ); this.activeChunksListeners = new CopyOnWriteArrayList<>( ); this.chunkPriorityFunc = new Function<DncChunkKey,DncChunkPriority>( ) { public DncChunkPriority apply( DncChunkKey chunkKey ) { synchronized ( mutex ) { boolean isActive = ( activeLibraries.contains( chunkKey.library ) && activeCoverages.contains( chunkKey.coverage ) ); return ( isActive ? DncChunkPriority.IMMEDIATE : DncChunkPriority.SOON ); } } }; this.visible = true; this.theme = null; this.lineAreaStyles = emptyMap( ); this.rasterizeArgs = null; this.labelColors = new Int2ObjectOpenHashMap<>( ); this.labelColorsFile = null; setTheme( theme ); } public void addAxis( Axis2D axis ) { synchronized ( mutex ) { // Don't allow axes to be re-enabled after disposal if ( asyncExec.isShutdown( ) ) return; if ( axes.add( axis ) ) { axis.getAxisX( ).addAxisListener( axisListener ); axis.getAxisY( ).addAxisListener( axisListener ); updateActiveLibraries( allLibraries ); } } } public void removeAxis( Axis2D axis ) { synchronized ( mutex ) { // Not necessary, since axes would already be empty //if ( asyncExec.isShutdown( ) ) return; if ( axes.remove( axis ) ) { axis.getAxisX( ).removeAxisListener( axisListener ); axis.getAxisY( ).removeAxisListener( axisListener ); updateActiveLibraries( allLibraries ); } } } public void addActiveChunksListener( Runnable listener ) { // Thread-safe because listeners list is a CopyOnWriteArrayList activeChunksListeners.add( listener ); } public void removeActiveChunksListener( Runnable listener ) { // Thread-safe because listeners list is a CopyOnWriteArrayList activeChunksListeners.remove( listener ); } protected void notifyActiveChunksListeners( ) { // Thread-safe because listeners list is a CopyOnWriteArrayList for ( Runnable listener : activeChunksListeners ) { listener.run( ); } } public boolean isChunkActive( DncChunkKey chunkKey ) { synchronized ( mutex ) { return ( activeLibraries.contains( chunkKey.library ) && activeCoverages.contains( chunkKey.coverage ) ); } } public Collection<DncChunkKey> activeChunkKeys( ) { synchronized ( mutex ) { Collection<DncChunkKey> chunkKeys = new ArrayList<>( ); for ( DncLibrary library : activeLibraries ) { for ( DncCoverage coverage : activeCoverages ) { DncChunkKey chunkKey = new DncChunkKey( library, coverage ); chunkKeys.add( chunkKey ); } } return chunkKeys; } } public void setTheme( DncGeosymTheme newTheme ) { // If asyncExec is currently in a call to activateCoverages, it's possible for // this block to run AFTER asyncExec reads this.theme, but BEFORE it populates // maps with newly loaded chunk data. // // That feels wrong, and makes the whole thing tricky to reason about. However, // it turns out okay, because after writing this.theme we re-activate everything, // which gives eventual consistency. // synchronized ( mutex ) { if ( !equal( newTheme, theme ) ) { // Drop everything that was created using the old theme deactivateChunks( activeLibraries, activeCoverages ); this.lineAreaStyles = emptyMap( ); // Store the theme this.theme = newTheme; // Reload everything using the new theme activateChunks( activeLibraries, activeCoverages ); final String newLineAreaStylesFile = newTheme.lineAreaStylesFile; asyncExec.execute( new ThrowingRunnable( ) { public void runThrows( ) throws Exception { Map<String,DncGeosymLineAreaStyle> newLineAreaStyles = readGeosymLineAreaStyles( newLineAreaStylesFile ); synchronized ( mutex ) { lineAreaStyles = newLineAreaStyles; } } } ); } } } public void highlightFeatures( DncChunkKey chunkKey, IntCollection featureNums ) { synchronized ( mutex ) { if ( !highlightSets.containsKey( chunkKey ) ) { highlightSets.put( chunkKey, new IndexSetTexture( ) ); } highlightSets.get( chunkKey ).set( featureNums ); } } public void setCoverageActive( DncCoverage coverage, boolean active ) { setCoveragesActive( singleton( coverage ), active ); } public void setCoveragesActive( Collection<DncCoverage> coverages, boolean active ) { if ( active ) { activateCoverages( coverages ); } else { deactivateCoverages( coverages ); } } public void activateCoverages( String... coverageNames ) { Collection<DncCoverage> coverages = new ArrayList<>( ); for ( String coverageName : coverageNames ) { coverages.add( new DncCoverage( coverageName ) ); } activateCoverages( coverages ); } public void activateCoverage( DncCoverage coverage ) { activateCoverages( singleton( coverage ) ); } public void activateCoverages( Collection<DncCoverage> coverages ) { boolean activeChunksChanged = false; synchronized ( mutex ) { // Don't allow coverages to be re-activated after disposal if ( asyncExec.isShutdown( ) ) return; if ( activeCoverages.addAll( coverages ) ) { activateChunks( activeLibraries, coverages ); activeChunksChanged = true; } } if ( activeChunksChanged ) { notifyActiveChunksListeners( ); } } public void deactivateCoverage( DncCoverage coverage ) { deactivateCoverages( singleton( coverage ) ); } public void deactivateCoverages( Collection<DncCoverage> coverages ) { boolean activeChunksChanged = false; synchronized ( mutex ) { // Not necessary, since activeCoverages would already be empty //if ( asyncExec.isShutdown( ) ) return; if ( activeCoverages.removeAll( coverages ) ) { deactivateChunks( activeLibraries, coverages ); activeChunksChanged = true; } } if ( activeChunksChanged ) { notifyActiveChunksListeners( ); } } protected void updateActiveLibraries( Collection<DncLibrary> librariesToUpdate ) { boolean activeChunksChanged = false; synchronized ( mutex ) { // Not usually necessary, since axes would be empty, but guards against strange impls of isLibraryActive if ( asyncExec.isShutdown( ) ) return; Set<DncLibrary> librariesToActivate = new HashSet<>( ); Set<DncLibrary> librariesToDeactivate = new HashSet<>( ); for ( DncLibrary library : librariesToUpdate ) { if ( settings.isLibraryActive( library, axes ) ) { if ( activeLibraries.add( library ) ) { librariesToActivate.add( library ); activeChunksChanged = true; } } else { if ( activeLibraries.remove( library ) ) { librariesToDeactivate.add( library ); activeChunksChanged = true; } } } deactivateChunks( librariesToDeactivate, activeCoverages ); activateChunks( librariesToActivate, activeCoverages ); } if ( activeChunksChanged ) { notifyActiveChunksListeners( ); } } protected void activateChunks( Collection<DncLibrary> libraries, Collection<DncCoverage> coverages ) { coverages = sorted( coverages, coverageSignificanceComparator ); synchronized ( mutex ) { // Not strictly necessary, but avoids submitting useless requests to the cache if ( asyncExec.isShutdown( ) ) return; for ( DncLibrary library : libraries ) { for ( DncCoverage coverage : coverages ) { final DncChunkKey chunkKey = new DncChunkKey( library, coverage ); if ( !dChunks.containsKey( chunkKey ) && !hChunks.containsKey( chunkKey ) ) { cache.getChunk( chunkKey, chunkPriorityFunc, new Consumer<RenderChunk>( ) { public void accept( final RenderChunk renderChunk ) { // On the async thread ... asyncExec.execute( new ThrowingRunnable( ) { public void runThrows( ) throws Exception { // Wait until we have certain display info from our first paint final RasterizeArgs rasterizeArgs = waitForRasterizeArgs( ); if ( rasterizeArgs == null ) { return; } // Bail out if chunk is no longer active synchronized ( mutex ) { if ( !( activeLibraries.contains( chunkKey.library ) && activeCoverages.contains( chunkKey.coverage ) ) ) { return; } } // Load and put chunk vertices int featureCount = renderChunk.featureCount; IntBuffer groupsBuf = cache.sliceChunkGroups( renderChunk ); FloatBuffer verticesBuf = cache.memmapChunkVertices( renderChunk ); final DncHostChunk hChunk = createHostChunk( chunkKey, featureCount, groupsBuf, verticesBuf, cache.geosymAssignments ); synchronized ( mutex ) { if ( activeLibraries.contains( chunkKey.library ) && activeCoverages.contains( chunkKey.coverage ) ) { hChunks.put( chunkKey, hChunk ); if ( !highlightSets.containsKey( chunkKey ) ) { highlightSets.put( chunkKey, new IndexSetTexture( ) ); } } } // On the icons thread ... iconsExec.execute( new ThrowingRunnable( ) { public void runThrows( ) throws Exception { // Bail out if chunk is no longer active synchronized ( mutex ) { if ( !( activeLibraries.contains( chunkKey.library ) && activeCoverages.contains( chunkKey.coverage ) ) ) { return; } } // Get up-to-date cgmDir and svgDir String cgmDir; String svgDir; synchronized ( mutex ) { if ( theme == null ) { return; } cgmDir = theme.cgmDir; svgDir = theme.svgDir; } // Load, rasterize, and put chunk icons DncHostIconAtlas hIconAtlas = createHostIconAtlas( hChunk, cgmDir, svgDir, rasterizeArgs.maxTextureDim, rasterizeArgs.screenDpi ); if ( hIconAtlas != null ) { synchronized ( mutex ) { if ( equal( cgmDir, theme.cgmDir ) && equal( svgDir, theme.svgDir ) && activeLibraries.contains( chunkKey.library ) && activeCoverages.contains( chunkKey.coverage ) ) { hIconAtlases.put( chunkKey, hIconAtlas ); } } } } } ); // On the labels thread ... labelsExec.execute( new ThrowingRunnable( ) { public void runThrows( ) throws Exception { // Bail out if chunk is no longer active synchronized ( mutex ) { if ( !( activeLibraries.contains( chunkKey.library ) && activeCoverages.contains( chunkKey.coverage ) ) ) { return; } } // Get up-to-date colors map String colorsFile; synchronized ( mutex ) { if ( theme == null ) { return; } colorsFile = theme.colorsFile; } if ( !equal( colorsFile, labelColorsFile ) ) { labelColors = readGeosymColors( colorsFile ); labelColorsFile = colorsFile; } // Load, rasterize, and put chunk labels CharBuffer labelCharsBuf = cache.sliceChunkLabelChars( renderChunk ); IntBuffer labelLengthsBuf = cache.sliceChunkLabelLengths( renderChunk ); DncHostLabelAtlas hLabelAtlas = createHostLabelAtlas( hChunk, labelCharsBuf, labelLengthsBuf, labelColors, rasterizeArgs.maxTextureDim, rasterizeArgs.screenDpi ); if ( hLabelAtlas != null ) { synchronized ( mutex ) { if ( equal( colorsFile, theme.colorsFile ) && activeLibraries.contains( chunkKey.library ) && activeCoverages.contains( chunkKey.coverage ) ) { hLabelAtlases.put( chunkKey, hLabelAtlas ); } } } } } ); } } ); } } ); } } } } } /** * Wait until either the painter is disposed (in which case return null) * or this.rasterizeArgs is set (in which case return this.rasterizeArgs). */ protected RasterizeArgs waitForRasterizeArgs( ) { synchronized ( mutex ) { while ( true ) { // Stop waiting if painter has been disposed if ( asyncExec.isShutdown( ) ) { return null; } if ( rasterizeArgs != null ) { return rasterizeArgs; } try { mutex.wait( ); } catch ( InterruptedException e ) { } } } } protected void deactivateChunks( Collection<DncLibrary> libraries, Collection<DncCoverage> coverages ) { synchronized ( mutex ) { // Not necessary, since everything would already be empty //if ( asyncExec.isShutdown( ) ) return; for ( DncLibrary library : libraries ) { for ( DncCoverage coverage : coverages ) { DncChunkKey chunkKey = new DncChunkKey( library, coverage ); hChunks.remove( chunkKey ); hIconAtlases.remove( chunkKey ); hLabelAtlases.remove( chunkKey ); DncDeviceChunk dChunk = dChunks.remove( chunkKey ); if ( dChunk != null ) dChunksToDispose.add( dChunk ); DncDeviceIconAtlas dIconAtlas = dIconAtlases.remove( chunkKey ); if ( dIconAtlas != null ) dIconAtlasesToDispose.add( dIconAtlas ); DncDeviceLabelAtlas dLabelAtlas = dLabelAtlases.remove( chunkKey ); if ( dLabelAtlas != null ) dLabelAtlasesToDispose.add( dLabelAtlas ); // Keep higlight-set objects in the map, but dispose their device resources IndexSetTexture highlightSet = highlightSets.get( chunkKey ); if ( highlightSet != null ) highlightSetsToDispose.add( highlightSet ); } } } } @Override public void dispose( GlimpseContext context ) { GL2ES2 gl = context.getGL( ).getGL2ES2( ); synchronized ( mutex ) { // Don't try to dispose again if already disposed if ( asyncExec.isShutdown( ) ) return; // Chunks for ( DncDeviceChunk dChunk : dChunksToDispose ) dChunk.dispose( gl ); for ( DncDeviceChunk dChunk : dChunks.values( ) ) dChunk.dispose( gl ); dChunksToDispose.clear( ); dChunks.clear( ); hChunks.clear( ); // Icon-atlases for ( DncDeviceIconAtlas dIconAtlas : dIconAtlasesToDispose ) dIconAtlas.dispose( gl ); for ( DncDeviceIconAtlas dIconAtlas : dIconAtlases.values( ) ) dIconAtlas.dispose( gl ); dIconAtlasesToDispose.clear( ); dIconAtlases.clear( ); hIconAtlases.clear( ); // Label-atlases for ( DncDeviceLabelAtlas dLabelAtlas : dLabelAtlasesToDispose ) dLabelAtlas.dispose( gl ); for ( DncDeviceLabelAtlas dLabelAtlas : dLabelAtlases.values( ) ) dLabelAtlas.dispose( gl ); dLabelAtlasesToDispose.clear( ); dLabelAtlases.clear( ); hLabelAtlases.clear( ); // Highlight-sets for ( IndexSetTexture highlightSet : highlightSets.values( ) ) highlightSet.freeDeviceResources( gl ); highlightSetsToDispose.clear( ); highlightSets.clear( ); // Shader programs areaProgram.dispose( gl ); lineProgram.dispose( gl ); iconProgram.dispose( gl ); labelProgram.dispose( gl ); // Axis listeners for ( Axis2D axis : axes ) { axis.getAxisX( ).removeAxisListener( axisListener ); axis.getAxisY( ).removeAxisListener( axisListener ); } axes.clear( ); // Mark all chunks as deactivated activeLibraries.clear( ); activeCoverages.clear( ); // Executors // // Jobs already submitted will run, but will find that no chunks are active, and so // will simply discard whatever they have generated. // // Jobs submitted later will be silently discarded. // asyncExec.shutdown( ); iconsExec.shutdown( ); labelsExec.shutdown( ); // Wake up any threads sleeping in waitForRasterizeArgs() mutex.notifyAll( ); } } @Override public boolean isDisposed( ) { synchronized ( mutex ) { return asyncExec.isShutdown( ); } } @Override public void setLookAndFeel( LookAndFeel laf ) { } @Override public boolean isVisible( ) { synchronized ( mutex ) { return visible; } } @Override public void setVisible( boolean visible ) { synchronized ( mutex ) { this.visible = visible; } } @Override public void paintTo( GlimpseContext context ) { GlimpseBounds bounds = getBounds( context ); Axis2D axis = requireAxis2D( context ); GL2ES2 gl = context.getGL( ).getGL2ES2( ); gl.glEnable( GL_BLEND ); // Premultiplied alpha gl.glBlendFunc( GL_ONE, GL_ONE_MINUS_SRC_ALPHA ); gl.getGL3( ).glBindVertexArray( GLUtils.defaultVertexAttributeArray( gl ) ); synchronized ( mutex ) { if ( !visible ) return; // Don't try to paint after disposal if ( asyncExec.isShutdown( ) ) return; // Store values used in rasterizing icons and labels if ( rasterizeArgs == null ) { int[] maxTextureDim = new int[ 1 ]; gl.glGetIntegerv( GL_MAX_TEXTURE_SIZE, maxTextureDim, 0 ); this.rasterizeArgs = new RasterizeArgs( maxTextureDim[ 0 ], context.getDPI( ) ); logger.fine( "Rasterization args: max-texture-dim = " + rasterizeArgs.maxTextureDim + ", screen-dpi = " + rasterizeArgs.screenDpi ); mutex.notifyAll( ); } // Dispose of deactivated chunks int chunkDisposeCount = 0; long chunkDisposeStart_PMILLIS = System.currentTimeMillis( ); while ( !dChunksToDispose.isEmpty( ) ) { boolean allowDispose = ( chunkDisposeCount < guaranteedChunkDisposalsPerFrame || timeSince_MILLIS( chunkDisposeStart_PMILLIS ) <= chunkDisposeTimeLimit_MILLIS ); if ( !allowDispose ) break; DncDeviceChunk dChunk = dChunksToDispose.remove( 0 ); dChunk.dispose( gl ); chunkDisposeCount++; } // Dispose of deactivated icon-atlases int iconAtlasDisposeCount = 0; long iconAtlasDisposeStart_PMILLIS = System.currentTimeMillis( ); while ( !dIconAtlasesToDispose.isEmpty( ) ) { boolean allowDispose = ( iconAtlasDisposeCount < guaranteedIconAtlasDisposalsPerFrame || timeSince_MILLIS( iconAtlasDisposeStart_PMILLIS ) <= iconAtlasDisposeTimeLimit_MILLIS ); if ( !allowDispose ) break; DncDeviceIconAtlas dIconAtlas = dIconAtlasesToDispose.remove( 0 ); dIconAtlas.dispose( gl ); iconAtlasDisposeCount++; } // Dispose of deactivated label-atlases int labelAtlasDisposeCount = 0; long labelAtlasDisposeStart_PMILLIS = System.currentTimeMillis( ); while ( !dLabelAtlasesToDispose.isEmpty( ) ) { boolean allowDispose = ( labelAtlasDisposeCount < guaranteedLabelAtlasDisposalsPerFrame || timeSince_MILLIS( labelAtlasDisposeStart_PMILLIS ) <= labelAtlasDisposeTimeLimit_MILLIS ); if ( !allowDispose ) break; DncDeviceLabelAtlas dLabelAtlas = dLabelAtlasesToDispose.remove( 0 ); dLabelAtlas.dispose( gl ); labelAtlasDisposeCount++; } // Dispose of deactivated highlight-sets int highlightSetDisposeCount = 0; long highlightSetDisposeStart_PMILLIS = System.currentTimeMillis( ); while ( !highlightSetsToDispose.isEmpty( ) ) { boolean allowDispose = ( highlightSetDisposeCount < guaranteedHighlightSetDisposalsPerFrame || timeSince_MILLIS( highlightSetDisposeStart_PMILLIS ) <= highlightSetDisposeTimeLimit_MILLIS ); if ( !allowDispose ) break; IndexSetTexture highlightSet = highlightSetsToDispose.remove( 0 ); highlightSet.freeDeviceResources( gl ); highlightSetDisposeCount++; } // Identify chunks to draw Collection<DncChunkKey> chunksToDraw = new ArrayList<>( ); for ( DncLibrary library : activeLibraries ) { // If this axis is a control axis (i.e. it controls library activation), // then paint only libraries that are active for it specifically. // // Otherwise, paint all libraries, regardless of which control axis they // were loaded for. // if ( !axes.contains( axis ) || settings.isLibraryActive( library, axis ) ) { for ( DncCoverage coverage : activeCoverages ) { chunksToDraw.add( new DncChunkKey( library, coverage ) ); } } } // Transfer chunks to the graphics device int chunkXferCount = 0; long chunkXferStart_PMILLIS = System.currentTimeMillis( ); for ( DncChunkKey chunkKey : chunksToDraw ) { if ( hChunks.containsKey( chunkKey ) ) { boolean allowXfer = ( chunkXferCount < guaranteedChunkXfersPerFrame || timeSince_MILLIS( chunkXferStart_PMILLIS ) <= chunkXferTimeLimit_MILLIS ); if ( allowXfer ) { DncHostChunk hChunk = hChunks.remove( chunkKey ); DncDeviceChunk dChunk = xferChunkToDevice( hChunk, gl ); dChunks.put( chunkKey, dChunk ); chunkXferCount++; } } } // Transfer icon-atlases to the graphics device int iconAtlasXferCount = 0; long iconAtlasXferStart_PMILLIS = System.currentTimeMillis( ); for ( DncChunkKey chunkKey : chunksToDraw ) { if ( hIconAtlases.containsKey( chunkKey ) ) { boolean allowXfer = ( iconAtlasXferCount < guaranteedIconAtlasXfersPerFrame || timeSince_MILLIS( iconAtlasXferStart_PMILLIS ) <= iconAtlasXferTimeLimit_MILLIS ); if ( allowXfer ) { DncHostIconAtlas hIconAtlas = hIconAtlases.remove( chunkKey ); DncDeviceIconAtlas dIconAtlas = xferIconAtlasToDevice( hIconAtlas, gl ); dIconAtlases.put( chunkKey, dIconAtlas ); iconAtlasXferCount++; } } } // Transfer label-atlases to the graphics device int labelAtlasXferCount = 0; long labelAtlasXferStart_PMILLIS = System.currentTimeMillis( ); for ( DncChunkKey chunkKey : chunksToDraw ) { if ( hLabelAtlases.containsKey( chunkKey ) ) { boolean allowXfer = ( labelAtlasXferCount < guaranteedLabelAtlasXfersPerFrame || timeSince_MILLIS( labelAtlasXferStart_PMILLIS ) <= labelAtlasXferTimeLimit_MILLIS ); if ( allowXfer ) { DncHostLabelAtlas hLabelAtlas = hLabelAtlases.remove( chunkKey ); DncDeviceLabelAtlas dLabelAtlas = xferLabelAtlasToDevice( hLabelAtlas, gl ); dLabelAtlases.put( chunkKey, dLabelAtlas ); labelAtlasXferCount++; } } } // Do the actual drawing boolean areasVisible = settings.areAreasVisible( axis ); boolean linesVisible = settings.areLinesVisible( axis ); boolean iconsVisible = settings.areIconsVisible( axis ); boolean labelsVisible = settings.areLabelsVisible( axis ); if ( areasVisible || linesVisible || iconsVisible || labelsVisible ) { List<DncGroup> groupsToDraw = new ArrayList<>( ); for ( DncChunkKey chunkKey : chunksToDraw ) { DncDeviceChunk dChunk = dChunks.get( chunkKey ); if ( dChunk != null ) groupsToDraw.addAll( dChunk.groups ); } sort( groupsToDraw, groupRenderingOrder ); float iconScale = settings.iconsGlobalScale( axis ); long pulsatePeriod_MILLIS = 1100; long currentTime_PMILLIS = currentTimeMillis( ); double pulsatePeriodFrac = ( ( double ) ( currentTime_PMILLIS % pulsatePeriod_MILLIS ) ) / ( ( double ) pulsatePeriod_MILLIS ); double pulsateScale = 0.5*( 1.0 + cos( 2.0 * PI * pulsatePeriodFrac ) ); float lineHighlightExtraThickness_PX = ( float ) ( 2.0*pulsateScale ); float iconHighlightScale = ( float ) ( 1.0 + 0.75*pulsateScale ); float labelHighlightScale = ( float ) ( 1.0 + 0.75*pulsateScale ); for ( DncGroup group : groupsToDraw ) { DncDeviceChunk dChunk = dChunks.get( group.chunkKey ); DncDeviceIconAtlas dIconAtlas = dIconAtlases.get( group.chunkKey ); DncDeviceLabelAtlas dLabelAtlas = dLabelAtlases.get( group.chunkKey ); IndexSetTexture highlightSet = highlightSets.get( group.chunkKey ); DncGeosymAssignment geosymAssignment = group.geosymAssignment; boolean drawGroupAreas = ( areasVisible && geosymAssignment.hasAreaSymbol( ) ); boolean drawGroupLines = ( linesVisible && geosymAssignment.hasLineSymbol( ) ); boolean drawGroupIcons = ( dIconAtlas != null && iconsVisible && geosymAssignment.hasPointSymbol( ) ); boolean drawGroupLabels = ( dLabelAtlas != null && labelsVisible && !geosymAssignment.labelMakers.isEmpty( ) ); if ( dChunk != null && highlightSet != null && ( drawGroupAreas || drawGroupLines || drawGroupIcons || drawGroupLabels ) ) { int highlightSetTextureUnit = 1; gl.glActiveTexture( GL_TEXTURE0 + highlightSetTextureUnit ); highlightSet.bind( gl, dChunk.featureCount, rasterizeArgs.maxTextureDim ); if ( drawGroupAreas ) { DncGeosymLineAreaStyle style = lineAreaStyles.get( geosymAssignment.areaSymbolId ); if ( style != null && style.symbolType.equals( "AreaPlain" ) ) { DncAreaProgramHandles handles = areaProgram.handles( gl ); gl.glUseProgram( handles.program ); setUniformAxisRect( gl, handles.AXIS_RECT, axis ); gl.glUniform4fv( handles.RGBA, 1, style.fillRgba, 0 ); gl.glUniform1i( handles.HIGHLIGHT_SET, highlightSetTextureUnit ); gl.glEnableVertexAttribArray( handles.inAreaVertex ); gl.glBindBuffer( GL_ARRAY_BUFFER, dChunk.verticesHandle ); gl.glVertexAttribPointer( handles.inAreaVertex, coordsPerRenderTriangleVertex, GL_FLOAT, false, 0, group.trianglesCoordFirst * SIZEOF_FLOAT ); gl.glDrawArrays( GL_TRIANGLES, 0, group.trianglesCoordCount / coordsPerRenderTriangleVertex ); gl.glDisableVertexAttribArray( handles.inAreaVertex ); } } if ( drawGroupLines ) { DncGeosymLineAreaStyle style = lineAreaStyles.get( geosymAssignment.lineSymbolId ); if ( style != null ) { DncLineProgramHandles handles = lineProgram.handles( gl ); gl.glUseProgram( handles.program ); setUniformAxisRect( gl, handles.AXIS_RECT, axis ); setUniformViewport( gl, handles.VIEWPORT_SIZE_PX, bounds ); gl.glUniform4fv( handles.RGBA, 1, style.lineRgba, 0 ); gl.glUniform1i( handles.STIPPLE_ENABLE, style.hasLineStipple ? 1 : 0 ); gl.glUniform1f( handles.STIPPLE_FACTOR, style.lineStippleFactor ); gl.glUniform1i( handles.STIPPLE_PATTERN, style.lineStipplePattern ); gl.glUniform1f( handles.LINE_THICKNESS_PX, style.lineWidth ); gl.glUniform1f( handles.FEATHER_THICKNESS_PX, 1f ); gl.glUniform1i( handles.HIGHLIGHT_SET, highlightSetTextureUnit ); gl.glUniform1f( handles.HIGHLIGHT_EXTRA_THICKNESS_PX, lineHighlightExtraThickness_PX ); gl.glEnableVertexAttribArray( handles.inLineVertex ); gl.glBindBuffer( GL_ARRAY_BUFFER, dChunk.verticesHandle ); gl.glVertexAttribPointer( handles.inLineVertex, coordsPerRenderLineVertex, GL_FLOAT, false, 0, group.linesCoordFirst * SIZEOF_FLOAT ); gl.glDrawArrays( GL_LINE_STRIP, 0, group.linesCoordCount / coordsPerRenderLineVertex ); gl.glDisableVertexAttribArray( handles.inLineVertex ); } } if ( drawGroupIcons ) { DncAtlasEntry atlasEntry = dIconAtlas.entries.get( geosymAssignment.pointSymbolId ); if ( atlasEntry != null ) { DncIconProgramHandles handles = iconProgram.handles( gl ); gl.glUseProgram( handles.program ); setUniformAxisRect( gl, handles.AXIS_RECT, axis ); setUniformViewport( gl, handles.VIEWPORT_SIZE_PX, bounds ); int iconAtlasTextureUnit = 0; gl.glActiveTexture( GL_TEXTURE0 + iconAtlasTextureUnit ); gl.glBindTexture( GL_TEXTURE_2D, dIconAtlas.textureHandle ); gl.glUniform1i( handles.ATLAS, iconAtlasTextureUnit ); gl.glUniform4f( handles.IMAGE_BOUNDS, atlasEntry.sMin, atlasEntry.tMin, atlasEntry.sMax, atlasEntry.tMax ); gl.glUniform2f( handles.IMAGE_SIZE_PX, iconScale * atlasEntry.w, iconScale * atlasEntry.h ); gl.glUniform2f( handles.IMAGE_ALIGN, atlasEntry.xAlign, atlasEntry.yAlign ); gl.glUniform1i( handles.HIGHLIGHT_SET, highlightSetTextureUnit ); gl.glUniform1f( handles.HIGHLIGHT_SCALE, iconHighlightScale ); gl.glEnableVertexAttribArray( handles.inIconVertex ); gl.glBindBuffer( GL_ARRAY_BUFFER, dChunk.verticesHandle ); gl.glVertexAttribPointer( handles.inIconVertex, coordsPerRenderIconVertex, GL_FLOAT, false, 0, group.iconsCoordFirst * SIZEOF_FLOAT ); gl.glDrawArrays( GL_POINTS, 0, group.iconsCoordCount / coordsPerRenderIconVertex ); gl.glDisableVertexAttribArray( handles.inIconVertex ); } } if ( drawGroupLabels ) { DncLabelProgramHandles handles = labelProgram.handles( gl ); gl.glUseProgram( handles.program ); setUniformAxisRect( gl, handles.AXIS_RECT, axis ); setUniformViewport( gl, handles.VIEWPORT_SIZE_PX, bounds ); int labelAtlasTextureUnit = 0; gl.glActiveTexture( GL_TEXTURE0 + labelAtlasTextureUnit ); gl.glBindTexture( GL_TEXTURE_2D, dLabelAtlas.textureHandle ); gl.glUniform1i( handles.ATLAS, labelAtlasTextureUnit ); gl.glUniform2f( handles.ATLAS_SIZE_PX, dLabelAtlas.textureWidth, dLabelAtlas.textureHeight ); gl.glUniform1i( handles.HIGHLIGHT_SET, highlightSetTextureUnit ); gl.glUniform1f( handles.HIGHLIGHT_SCALE, labelHighlightScale ); gl.glEnableVertexAttribArray( handles.inLabelVertex ); gl.glBindBuffer( GL_ARRAY_BUFFER, dChunk.verticesHandle ); gl.glVertexAttribPointer( handles.inLabelVertex, coordsPerRenderLabelVertex, GL_FLOAT, false, 0, group.labelsCoordFirst * SIZEOF_FLOAT ); gl.glEnableVertexAttribArray( handles.inImageAlign ); gl.glBindBuffer( GL_ARRAY_BUFFER, dLabelAtlas.entriesAlignHandle ); gl.glVertexAttribPointer( handles.inImageAlign, coordsPerLabelAtlasAlign, GL_FLOAT, false, 0, group.labelFirst * coordsPerLabelAtlasAlign * SIZEOF_FLOAT ); gl.glEnableVertexAttribArray( handles.inImageBounds ); gl.glBindBuffer( GL_ARRAY_BUFFER, dLabelAtlas.entriesBoundsHandle ); gl.glVertexAttribPointer( handles.inImageBounds, coordsPerLabelAtlasBounds, GL_FLOAT, false, 0, group.labelFirst * coordsPerLabelAtlasBounds * SIZEOF_FLOAT ); gl.glDrawArrays( GL_POINTS, 0, group.labelsCoordCount / coordsPerRenderLabelVertex ); gl.glDisableVertexAttribArray( handles.inLabelVertex ); gl.glDisableVertexAttribArray( handles.inImageAlign ); gl.glDisableVertexAttribArray( handles.inImageBounds ); } } } gl.glUseProgram( 0 ); gl.getGL3( ).glBindVertexArray( 0 ); } } } }