/*
 
* 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 );
            
}
        
}
    
}


}