/* * Geotoolkit - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2012, 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.display2d.process.pyramid; import java.awt.Dimension; import java.awt.Image; import java.awt.Point; import java.awt.Rectangle; import java.awt.geom.NoninvertibleTransformException; import java.awt.geom.Point2D; import java.awt.image.*; import java.util.EventListener; import java.util.Map; import java.util.Vector; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import javax.media.jai.RasterFactory; import javax.swing.event.EventListenerList; import org.geotoolkit.display.PortrayalException; import org.geotoolkit.display2d.GO2Hints; import org.geotoolkit.display2d.canvas.J2DCanvasBuffered; import org.geotoolkit.display2d.service.CanvasDef; import org.geotoolkit.display2d.service.DefaultPortrayalService; import org.geotoolkit.display2d.service.SceneDef; import org.geotoolkit.display2d.service.ViewDef; import org.apache.sis.geometry.GeneralEnvelope; import org.opengis.geometry.Envelope; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.TransformException; import org.apache.sis.util.logging.Logging; /** * On the fly calculated image. multi-threaded. * * @author Johann Sorel (Geomatys) * @module */ public class ProgressiveImage implements RenderedImage{ // private static class TileSet{ // private final Map<String,Raster> tiles = new ConcurrentHashMap<String, Raster>(); // } /** store pregenerated tiles */ private final Map<String,Raster> tiles = new ConcurrentHashMap<>(); // private final ArrayBlockingQueue<TileSet> tiles; private final ColorModel colorModel; private final SampleModel sampleModel; private final Dimension gridSize; private final Dimension tileSize; private final double scale; private final Point2D upperleft; private final int nbtileonwidth; private final int nbtileonheight; private final CanvasDef cdef; private final SceneDef sdef; private final ViewDef vdef; /** listener support */ private final EventListenerList listeners = new EventListenerList(); /** painter threads */ // private final ExecutorService executor; /** * * @param canvasDef : canvas size will be ignored * @param sceneDef * @param viewDef * @param gridSize * @param tileSize */ public ProgressiveImage(final CanvasDef canvasDef, final SceneDef sceneDef, final ViewDef viewDef, final Dimension gridSize, final Dimension tileSize, final double scale, int nbPainter) throws PortrayalException{ this.gridSize = gridSize; this.tileSize = tileSize; this.scale = scale; this.colorModel = ColorModel.getRGBdefault(); this.sampleModel = colorModel.createCompatibleSampleModel(1, 1); final Envelope envelope = viewDef.getEnvelope(); final CoordinateReferenceSystem crs = envelope.getCoordinateReferenceSystem(); this.upperleft = new Point2D.Double( envelope.getMinimum(0), envelope.getMaximum(1)); //prepare a J2DCanvas to render several tiles in the same tile //we consider a 2000*2000 size to be the maximum, which is 16Mb in memory //we expect the user to access tile lines by lines. final int maxNbTile = (2000*2000) / (tileSize.width*tileSize.height); if(maxNbTile < gridSize.width){ //we can not generate a full line nbtileonwidth = maxNbTile; nbtileonheight = 1; }else{ //we can generate more than one line nbtileonwidth = gridSize.width; nbtileonheight = maxNbTile / gridSize.width; } this.cdef = canvasDef; this.sdef = sceneDef; this.vdef = viewDef; // executor = Executors.newFixedThreadPool(nbPainter, new ThreadFactory() { // private volatile int inc = 0; // @Override // public Thread newThread(Runnable r) { // final Thread t = new Thread(r); // t.setName("TilePainter " + inc++); // return t; // } // }); // // tiles = new ArrayBlockingQueue<TileSet>(nbPainter); // // for(int y=0;y<gridSize.height;y+=nbtileonheight){ // for(int x=0;x<gridSize.width;x+=nbtileonwidth){ // executor.execute(new TileGenerator(new Point(x, y))); // } // } } /** * Tiles are generated on the fly, so we have informations on their generation * process but we don't have the tiles themselves. * * @return empty vector */ @Override public Vector<RenderedImage> getSources() { return new Vector<RenderedImage>(); } /** * A PortrayalRenderedImage does not have any properties * * @param name * @return always Image.UndefinedProperty */ @Override public Object getProperty(String name) { return Image.UndefinedProperty; } /** * A PortrayalRenderedImage does not have any properties * * @return always null */ @Override public String[] getPropertyNames() { return null; } /** * Fallback on the mosaic definition. * * @return mosaic grid size width * mosaic tile size width. */ @Override public int getWidth() { return gridSize.width * tileSize.height; } /** * Fallback on the mosaic definition. * * @return mosaic grid size width * mosaic tile size width. */ @Override public int getHeight() { return gridSize.height * tileSize.width; } /** * Generated tiles start at zero. * * @return 0 */ @Override public int getMinX() { return 0; } /** * Generated tiles start at zero. * * @return 0 */ @Override public int getMinY() { return 0; } /** * Fallback on the mosaic definition. * * @return mosaic grid size width. */ @Override public int getNumXTiles() { return gridSize.width; } /** * Fallback on the mosaic definition. * * @return mosaic grid size height. */ @Override public int getNumYTiles() { return gridSize.height; } /** * Generated tiles start at zero. * * @return 0 */ @Override public int getMinTileX() { return 0; } /** * Generated tiles start at zero. * * @return 0 */ @Override public int getMinTileY() { return 0; } /** * Fallback on the mosaic definition. * * @return mosaic tile size width. */ @Override public int getTileWidth() { return tileSize.width; } /** * Fallback on the mosaic definition. * * @return mosaic tile size height. */ @Override public int getTileHeight() { return tileSize.height; } /** * Generated tiles start at zero. * * @return 0 */ @Override public int getTileGridXOffset() { return 0; } /** * Generated tiles start at zero. * * @return 0 */ @Override public int getTileGridYOffset() { return 0; } /** * Returns the image's bounds as a <code>Rectangle</code>. * * <p> The image's bounds are defined by the values returned by * <code>getMinX()</code>, <code>getMinY()</code>, * <code>getWidth()</code>, and <code>getHeight()</code>. * A <code>Rectangle</code> is created based on these four methods. * * @return Rectangle */ public Rectangle getBounds() { return new Rectangle(getMinX(), getMinY(), getWidth(), getHeight()); } @Override public ColorModel getColorModel() { return colorModel; } @Override public SampleModel getSampleModel() { return sampleModel; } @Override public Raster getTile(int col, int row) { final Raster raster = tiles.remove(getTileIndex(col, row)); if(raster!=null) return raster; tiles.clear(); renderTiles(col, row); return tiles.remove(getTileIndex(col, row)); } // @Override // public Raster getTile(int col, int row) { // final String index = getTileIndex(col, row); // // Raster raster = null; // do{ // final TileSet[] generates = tiles.toArray(new TileSet[0]); // for(TileSet ts : generates){ // raster = ts.tiles.remove(index); // if(raster != null){ // if(ts.tiles.isEmpty()){ // tiles.remove(ts); // } // break; // } // } // // if(raster == null){ // try { // Thread.sleep(100); // } catch (InterruptedException ex) { // Logging.getLogger("org.geotoolkit.display2d.process.pyramid").log(Level.SEVERE, null, ex); // } // } // // }while(raster == null); // // return raster; // } @Override public Raster getData() { return getData(null); } @Override public WritableRaster getData(Rectangle region) { return copyData(region, null); } @Override public WritableRaster copyData(WritableRaster raster) { final Rectangle bounds = (raster!=null) ? raster.getBounds() : null; return copyData(bounds, raster); } public WritableRaster copyData(Rectangle region, WritableRaster dstRaster) { final Rectangle bounds = getBounds(); // image's bounds if (region == null) { region = bounds; } else if (!region.intersects(bounds)) { throw new IllegalArgumentException("Rectangle does not intersect datas."); } // Get the intersection of the region and the image bounds. final Rectangle xsect = (region == bounds) ? region : region.intersection(bounds); //create a raster of this size if(dstRaster == null){ SampleModel sampleModel = getSampleModel(); sampleModel = sampleModel.createCompatibleSampleModel(xsect.width, xsect.height); dstRaster = RasterFactory.createWritableRaster(sampleModel, new Point(0, 0)); } //calculate the first and last tiles index we will need final int startTileX = xsect.x / getTileWidth(); final int startTileY = xsect.y / getTileHeight(); final int endTileX = (xsect.x+xsect.width) / getTileWidth(); final int endTileY = (xsect.y+xsect.height) / getTileHeight(); //loop on each tile for (int j = startTileY; j <= endTileY; j++) { for (int i = startTileX; i <= endTileX; i++) { final Raster tile = getTile(i, j); dstRaster.setRect( i*getTileWidth(), j*getTileHeight(), tile); } } return dstRaster; } /** * @return unique index for this tile coordinate */ private String getTileIndex(int col, int row){ return row+" "+ col; } protected void fireTileCreated(int x, int y){ for(ProgressListener l : listeners.getListeners(ProgressListener.class)){ l.tileCreated(x, y); } } public void addProgressListener(ProgressListener listener){ listeners.add(ProgressListener.class, listener); } public void removeProgressListener(ProgressListener listener){ listeners.remove(ProgressListener.class, listener); } // @Override // protected void finalize() throws Throwable { // if(!executor.isShutdown()){ // executor.shutdownNow(); // } // super.finalize(); // } public static interface ProgressListener extends EventListener{ void tileCreated(int x, int y); } private void renderTiles(int col, int row) { final Dimension canvasSize = new Dimension( nbtileonwidth*tileSize.width, nbtileonheight*tileSize.height); final J2DCanvasBuffered canvas = new J2DCanvasBuffered(vdef.getEnvelope().getCoordinateReferenceSystem(), canvasSize); canvas.setRenderingHint(GO2Hints.KEY_COLOR_MODEL, colorModel); try { DefaultPortrayalService.prepareCanvas(canvas, cdef, sdef, vdef); } catch (PortrayalException ex) { ex.printStackTrace(); } final double tilespanX = scale * tileSize.width; final double tilespanY = scale * tileSize.height; final GeneralEnvelope canvasEnv = new GeneralEnvelope(canvas.getObjectiveCRS()); canvasEnv.setRange(0, upperleft.getX() + (col) * tilespanX, upperleft.getX() + (col + nbtileonwidth) * tilespanX); canvasEnv.setRange(1, upperleft.getY() - (row + nbtileonheight) * tilespanY, upperleft.getY() - (row) * tilespanY); try { canvas.setVisibleArea(canvasEnv); } catch (NoninvertibleTransformException | TransformException ex) { Logging.getLogger("org.geotoolkit.display2d.process.pyramid").log(Level.SEVERE, null, ex); } //cut the canvas buffer in pieces canvas.repaint(); final BufferedImage canvasBuffer = canvas.getSnapShot(); for(int x=0; x<nbtileonwidth && col+x<gridSize.width; x++){ for(int y=0; y<nbtileonheight && row+y<gridSize.height; y++){ final String idx = getTileIndex(col+x, row+y); final BufferedImage tile = canvasBuffer.getSubimage( x*tileSize.width, y*tileSize.height, tileSize.width, tileSize.height); tiles.put(idx, tile.getRaster()); fireTileCreated(col+x,row+y); } } } // private class TileGenerator implements Runnable{ // // private final Point topleft; // // public TileGenerator(final Point start) { // topleft = start; // // } // // @Override // public void run() { // // final TileSet ts = new TileSet(); // try { // tiles.put(ts); // } catch (InterruptedException ex) { // Logging.getLogger("org.geotoolkit.display2d.process.pyramid").log(Level.SEVERE, null, ex); // } // // final Dimension canvasSize = new Dimension( // nbtileonwidth*tileSize.width, // nbtileonheight*tileSize.height); // // final J2DCanvasBuffered canvas = new J2DCanvasBuffered(vdef.getEnvelope().getCoordinateReferenceSystem(), canvasSize); // canvas.setRenderingHint(GO2Hints.KEY_COLOR_MODEL, colorModel); // try { // DefaultPortrayalService.prepareCanvas(canvas, cdef, sdef, vdef); // } catch (PortrayalException ex) { // ex.printStackTrace(); // } // // // final int col = topleft.x; // final int row = topleft.y; // // final double tilespanX = scale*tileSize.width; // final double tilespanY = scale*tileSize.height; // // final GeneralEnvelope canvasEnv = new GeneralEnvelope(canvas.getObjectiveCRS()); // canvasEnv.setRange(0, // upperleft.getX() + (col)*tilespanX, // upperleft.getX() + (col+nbtileonwidth)*tilespanX // ); // canvasEnv.setRange(1, // upperleft.getY() - (row+nbtileonheight)*tilespanY, // upperleft.getY() - (row)*tilespanY // ); // // try { // canvas.getController().setVisibleArea(canvasEnv); // } catch (NoninvertibleTransformException ex) { // Logging.getLogger("org.geotoolkit.display2d.process.pyramid").log(Level.SEVERE, null, ex); // } catch (TransformException ex) { // Logging.getLogger("org.geotoolkit.display2d.process.pyramid").log(Level.SEVERE, null, ex); // } // // //cut the canvas buffer in pieces // canvas.repaint(); // // // // final BufferedImage canvasBuffer = canvas.getSnapShot(); // for(int x=0; x<nbtileonwidth && col+x<gridSize.width; x++){ // for(int y=0; y<nbtileonheight && row+y<gridSize.height; y++){ // final String idx = getTileIndex(col+x, row+y); // final BufferedImage tile = canvasBuffer.getSubimage( // x*tileSize.width, // y*tileSize.height, // tileSize.width, // tileSize.height); // ts.tiles.put(idx, tile.getRaster()); // fireTileCreated(col+x,row+y); // } // } // // } // // } }