/* * Geotoolkit - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2013, Geomatys * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotoolkit.display3d.scene; import com.jogamp.opengl.GLAutoDrawable; import com.jogamp.opengl.GLContext; import com.jogamp.opengl.GLDrawableFactory; import com.jogamp.opengl.GLException; import com.jogamp.opengl.GLProfile; import com.jogamp.opengl.GLRunnable; import com.jogamp.opengl.util.texture.TextureData; import com.jogamp.opengl.util.texture.awt.AWTTextureData; import java.awt.Dimension; import java.awt.image.BufferedImage; import java.awt.image.Raster; import org.apache.sis.geometry.GeneralEnvelope; import org.apache.sis.storage.DataStoreException; import javax.measure.IncommensurableException; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.*; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import javax.vecmath.Vector2d; import javax.vecmath.Vector3d; import org.geotoolkit.display.PortrayalException; import org.geotoolkit.display.primitive.SceneNode; import org.geotoolkit.display3d.Map3D; import org.geotoolkit.display3d.scene.camera.Camera; import org.geotoolkit.display3d.scene.camera.TrackBallCamera; import org.geotoolkit.display3d.scene.component.Tile3D; import org.geotoolkit.display3d.scene.loader.ElevationLoader; import org.geotoolkit.display3d.scene.loader.ImageLoader; import org.geotoolkit.display3d.scene.quadtree.JQuadView; import org.geotoolkit.display3d.scene.quadtree.QuadTree; import org.geotoolkit.display3d.scene.quadtree.QuadTreeNode; import org.geotoolkit.display3d.scene.quadtree.QuadTreeUtils; import org.geotoolkit.math.XMath; import org.opengis.geometry.Envelope; import org.opengis.referencing.operation.TransformException; /** * @author Thomas Rouby (Geomatys) */ public class TerrainUpdater implements PropertyChangeListener, Updater { private final Comparator<QuadTreeNode> DISTANCE_COMPARATOR = new Comparator<QuadTreeNode>(){ public int compare(QuadTreeNode o1, QuadTreeNode o2) { final int depth1 = o1.getTreeDepth(); final int depth2 = o2.getTreeDepth(); final int dz = Integer.compare(depth2, depth1); if(dz!=0) return dz; final Envelope env1 = o1.getEnvelope(); final Vector2d center1 = new Vector2d(env1.getMedian(0), env1.getMedian(1)); final Envelope env2 = o2.getEnvelope(); final Vector2d center2 = new Vector2d(env2.getMedian(0), env2.getMedian(1)); final double d1 = dists(center1); final double d2 = dists(center2); if(d1<d2){ return -1; }else if(d1>d2){ return +1; }else{ final int dx = Double.compare(center1.x, center2.x); if(dx!=0) return dx; final int dy = Double.compare(center1.y, center2.y); return dy; } } private double dists(Vector2d candidate){ double dx = (lastCameraPos.x - candidate.x); dx *= dx; double dy = (lastCameraPos.y - candidate.y); dy *= dy; return dx + dy; } }; private final BlockingQueue queue = new ArrayBlockingQueue(1024); private final ThreadPoolExecutor executor; private final boolean debug = false; private JQuadView debugQuad; private final Map3D map3d; private final Terrain terrain; private volatile boolean needUpdate = false; //variables used for update private Vector3d lastCameraPos; private final Set<QuadTreeNode> nodes = new TreeSet<>(DISTANCE_COMPARATOR); private final ConcurrentLinkedDeque<QuadTreeNode> garbage = new ConcurrentLinkedDeque<>(); private final AtomicBoolean updating = new AtomicBoolean(); private final Loader loader = new Loader(); private GLContext externalContext; private List<GLRunnable> textureLoad = new ArrayList<>(); private GLProfile glProfile; //private GL loaderGL; public TerrainUpdater(Terrain terrain) { this.terrain = terrain; this.map3d = terrain.getCanvas(); //listen to camera changes to update map3d.getCamera().addPropertyChangeListener(this); //create all core threads now int nbThread = Runtime.getRuntime().availableProcessors(); if(nbThread >= 4) nbThread -= 1; //keep a free thread for rendering executor = new ThreadPoolExecutor( nbThread, nbThread, 1, TimeUnit.MINUTES, queue); executor.prestartAllCoreThreads(); if(debug){ debugQuad = new JQuadView(); } loader.start(); } @Override public void forceUpdate() { needUpdate = true; } @Override public void propertyChange(PropertyChangeEvent evt) { //camera has changed final String propname = evt.getPropertyName(); if(propname.equals(Camera.PROP_CENTER) || propname.equals(Camera.PROP_EYE)){ needUpdate = true; } } public void updateScene() throws DataStoreException, TransformException, IncommensurableException { //remove all tasks not done yet. queue.clear(); nodes.clear(); final TrackBallCamera camera = this.map3d.getCamera(); lastCameraPos = new Vector3d(camera.getCenter()); final double cameraLength = camera.getLength(); final double viewScale = camera.getViewScale(cameraLength); final double viewDist = camera.getProjectionLength(cameraLength)/2.0; final int indexScale = terrain.getNearestScaleIndex(viewScale); final QuadTree quadTree = terrain.getQuadTree(); //load the remaining tiles final Dimension gridSize = QuadTreeUtils.getGridSize(indexScale); final Dimension tileSize = quadTree.getTileSize(); //calculate the list of tiles we need to render final List<QuadTreeNode> viewPts = quadTree.findView(indexScale, lastCameraPos.x, lastCameraPos.y, viewDist); nodes.addAll(viewPts); final int viewSize = viewPts.size(); //loop on terrain node and remove tiles which are not in the list final List<SceneNode> tiles = terrain.getChildren(); final List<QuadTreeNode> removeNodes = new ArrayList<>(); for(int i=tiles.size()-1;i>=0;i--){ final SceneNode node = tiles.get(i); if (node instanceof QuadTreeNode) { final QuadTreeNode quadTreeNode = (QuadTreeNode)node; if(nodes.contains(quadTreeNode)){ //node already here, check if it needs some update if(quadTreeNode.isData() && quadTreeNode.isDataImageLoaded() && quadTreeNode.isDataMNTLoaded()){ //no update needed nodes.remove(quadTreeNode); } }else{ removeNodes.add(quadTreeNode); } } } //nothing to load if(!nodes.isEmpty()){ for(final QuadTreeNode node : nodes){ node.getOrCreateData(); tiles.add(node); if(!node.isDataMNTLoaded()){ final Runnable loader = new Runnable() { @Override public void run() { //retest, maybe another thread did the job if(!node.isDataMNTLoaded()){ try { updateMntOn(node); } catch (Exception ex) { terrain.getCanvas().getMonitor().exceptionOccured(ex, Level.WARNING); } } } }; executor.execute(loader); } if(!node.isDataImageLoaded()) { final Runnable loader = new Runnable() { @Override public void run() { //retest, maybe another thread did the job if(!node.isDataImageLoaded()){ try { updateImageOn(node); } catch (Exception ex) { terrain.getCanvas().getMonitor().exceptionOccured(ex, Level.WARNING); } } } }; executor.execute(loader); } } } if (debug) { System.out.println("3D terrain update queue size : "+viewSize+" "+ queue.size()); } //remove obsolete tiles for (QuadTreeNode rmNode : removeNodes) { tiles.remove(rmNode); garbage.add(rmNode); } if(debug){ List<QuadTreeNode> quadNodes = new ArrayList<>(); for (SceneNode tile : tiles){ if (tile instanceof QuadTreeNode) { quadNodes.add((QuadTreeNode)tile); } } debugQuad.setNodes(quadNodes); } } public void updateMntOn(final QuadTreeNode node) throws PortrayalException { if (node.isData()) { final SceneNode3D sceneNode3d = node.getData(); if (sceneNode3d instanceof Tile3D) { final Tile3D tile3d = (Tile3D) sceneNode3d; final GeneralEnvelope tileEnv = (node.getEnvelope() instanceof GeneralEnvelope)?((GeneralEnvelope)node.getEnvelope()):(new GeneralEnvelope(node.getEnvelope())); final Dimension textureDimension = node.getTileSize(); final ElevationLoader loaderMNT = terrain.getElevationLoader(); final BufferedImage targetImage = loaderMNT.getBufferedImageOf(tileEnv, textureDimension); final Raster rasterMNT = targetImage.getTile(0, 0); float[] vertices = tile3d.getVerticesAsArray(); final Dimension ptsSize = tile3d.getPtsNumber(); final Dimension axisSize = tile3d.getAxisNumber(); for (int x=0; x<ptsSize.width; x++) { for (int y=0; y<ptsSize.height; y++) { // (i+j*axis0Pts)*3 final int col = XMath.clamp(x-1, 0, axisSize.width - 1); final int row = XMath.clamp(y-1, 0, axisSize.height - 1); final int pixel0 = XMath.clamp((int)(((double)col/((double)axisSize.width-1.0))*textureDimension.width), 0, textureDimension.width-1); final int pixel1 = XMath.clamp((int)(((double)row/((double)axisSize.height-1.0))*textureDimension.height), 0, textureDimension.height-1); final int coord = x + y * ptsSize.width; final int coordZ = coord *3 + 2; vertices[coordZ] = rasterMNT.getSampleFloat(pixel0,pixel1,0); if(Float.isNaN(vertices[coordZ])){ vertices[coordZ] = (float)loaderMNT.getMinimumElevation(); } if (x == 0 || y == 0 || x == ptsSize.width-1 || y == ptsSize.height-1) { vertices[coordZ] += Tile3D.borderZTranslate; } } } node.setDataMNT(vertices); } } else { System.out.println("Try to update MNT on " + node.getPosition() + " but has no data"); } } public void updateImageOn(final QuadTreeNode node) throws PortrayalException { if (node.isData()) { final Envelope tileEnv = node.getEnvelope(); final Dimension textureDimension = node.getTileSize(); final ImageLoader loaderImg = terrain.getImageLoader(); final BufferedImage targetImage = loaderImg.getBufferedImageOf(tileEnv, textureDimension); final TextureData data = new AWTTextureData(glProfile, 0, 0, false, targetImage); node.setDataImage(data); } else { System.out.println("Try to update image on " + node.getPosition() + " but has no data"); } } public synchronized void initialize(GLAutoDrawable glDrawable) { this.glProfile = glDrawable.getGLProfile(); final GLDrawableFactory factory = glDrawable.getFactory(); externalContext = factory.createExternalGLContext(); } @Override public synchronized void update(GLAutoDrawable glDrawable){ //clear garbage for(QuadTreeNode candidate = garbage.pollFirst();candidate!=null;candidate=garbage.pollFirst()){ candidate.dispose(glDrawable); } if(!needUpdate) return; needUpdate = false; queue.clear(); updating.set(true); synchronized(LOCK){ LOCK.notifyAll(); } } public void stopUpdate(boolean awaitTermination) throws InterruptedException { executor.shutdown(); } // /** // * Not working right yet. // * Waiting for sgothel answer. // * // * @param gl // * @return // */ // private GL createLoaderGL(GL gl) { // final GLContext baseContext = gl.getContext(); // final boolean isCurrent = baseContext.isCurrent(); // final GLProfile profile = gl.getGLProfile(); // final GLDrawableFactory factory = GLDrawableFactory.getFactory(profile); // final AbstractGraphicsDevice device = baseContext.getGLDrawable().getNativeSurface().getGraphicsConfiguration().getScreen().getDevice(); // final GLDrawable loaderDrawable = factory.createDummyDrawable(device, true, profile); // loaderDrawable.setRealized(true); // final GLContext loaderContext = loaderDrawable.createContext(baseContext); // // makeCurrent(loaderContext); // if(isCurrent) { // makeCurrent(baseContext); // }else{ // loaderContext.release(); // } // return loaderContext.getGL(); // } private void makeCurrent(GLContext ctx) { if( GLContext.CONTEXT_NOT_CURRENT >= ctx.makeCurrent() ) { throw new GLException("Couldn't make ctx current: "+ctx); } } private final Object LOCK = new Object(); private class Loader extends Thread{ public void doWait(){ synchronized(LOCK){ if(!updating.get()){ try { LOCK.wait(); } catch (InterruptedException ex) { Map3D.LOGGER.log(Level.INFO, ex.getMessage(),ex); } } } } @Override public void run() { while(true){ while(updating.getAndSet(false)){ try { updateScene(); } catch (DataStoreException | TransformException | IncommensurableException ex) { Map3D.LOGGER.log(Level.INFO, ex.getMessage(),ex); } } doWait(); } } } }