/* * Geotoolkit - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2012-2014, 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.coverage.xmlstore; import java.awt.*; import java.awt.image.BufferedImage; import java.awt.image.ColorModel; import java.awt.image.IndexColorModel; import java.awt.image.Raster; import java.awt.image.RenderedImage; import java.awt.image.SampleModel; import java.awt.image.WritableRaster; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.text.DecimalFormatSymbols; import java.text.NumberFormat; import java.util.*; import java.util.List; import java.util.concurrent.*; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.logging.Level; import java.util.logging.Logger; import javax.imageio.IIOImage; import javax.imageio.ImageIO; import javax.imageio.ImageWriter; import javax.imageio.stream.ImageOutputStream; import javax.swing.ProgressMonitor; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; import net.iharder.Base64; import org.apache.sis.geometry.GeneralDirectPosition; import org.apache.sis.geometry.GeneralEnvelope; import org.apache.sis.storage.DataStoreException; import org.apache.sis.util.ArgumentChecks; import org.apache.sis.util.ArraysExt; import org.apache.sis.util.Classes; import org.apache.sis.util.collection.Cache; import org.apache.sis.util.logging.Logging; import org.geotoolkit.image.internal.ImageUtils; import org.geotoolkit.image.internal.SampleType; import org.geotoolkit.image.io.XImageIO; import org.geotoolkit.image.BufferedImages; import org.geotoolkit.image.iterator.PixelIterator; import org.geotoolkit.image.iterator.PixelIteratorFactory; import org.geotoolkit.internal.referencing.CRSUtilities; import org.geotoolkit.storage.coverage.AbstractGridMosaic; import org.geotoolkit.storage.coverage.DefaultTileReference; import org.geotoolkit.storage.coverage.GridMosaic; import org.geotoolkit.storage.coverage.TileReference; import org.opengis.coverage.PointOutsideCoverageException; import org.opengis.geometry.DirectPosition; import org.opengis.geometry.Envelope; /** * * @author Johann Sorel (Geomatys) * @author Alexis Manin (Geomatys) * @module */ @XmlAccessorType(XmlAccessType.NONE) public class XMLMosaic implements GridMosaic { private static final Logger LOGGER = Logging.getLogger("org.geotoolkit.coverage.xmlstore"); private static final NumberFormat DECIMAL_FORMAT = NumberFormat.getInstance(Locale.ENGLISH); /** Executor used to write images */ private static final RejectedExecutionHandler LOCAL_REJECT_EXECUTION_HANDLER = new ThreadPoolExecutor.CallerRunsPolicy(); private static final BlockingQueue IMAGEQUEUE = new ArrayBlockingQueue(getMaxExecutors()*2); private static final ThreadPoolExecutor TILEWRITEREXECUTOR = new ThreadPoolExecutor( 0, getMaxExecutors(), 1, TimeUnit.MINUTES, IMAGEQUEUE, LOCAL_REJECT_EXECUTION_HANDLER); /* * Used only if we use the tile state cache mechanism, which means we don't use XML document to read / write tile states. */ private volatile Cache<Point, Boolean> isMissingCache = null; //empty tile information private byte[] emptyTileEncoded = null; //written values @XmlElement double scale; @XmlElement double[] upperLeft; @XmlElement int gridWidth; @XmlElement int gridHeight; @XmlElement int tileWidth; @XmlElement int tileHeight; // Use getter /setter to bind those two, because we must perform special operation at flush. String existMask; String emptyMask; XMLPyramid pyramid = null; BitSet tileExist; BitSet tileEmpty; @XmlElement Boolean cacheTileState; Path folder; final ReentrantReadWriteLock bitsetLock = new ReentrantReadWriteLock(); /** * Read the maximum number of threads in {@link #TILEWRITEREXECUTOR} from * {@code geotk.pyramid.xml.max.painters} system property. * * @return value of {@code geotk.pyramid.xml.max.painters} system property} or * number of available processors - 1. */ private static int getMaxExecutors() { final int availableProcessors = Runtime.getRuntime().availableProcessors(); final String property = System.getProperty("geotk.pyramid.xml.max.painters"); final int nbPainters; if (property != null) { nbPainters = Integer.valueOf(property); } else { nbPainters = availableProcessors > 1 ? availableProcessors - 1 : 1; } LOGGER.log(Level.FINE, "Initialize XML tile writer executor pool with a size of "+nbPainters); return nbPainters; } /** * Mosaic initialization. Should ALWAYS be called at mosaic instantiation, before doing anything else. * @param pyramid The owner pyramid of this mosaic. Cannot be null. */ void initialize(XMLPyramid pyramid) { ArgumentChecks.ensureNonNull("Owner pyramid", pyramid); this.pyramid = pyramid; // If we create a new mosaic, behavior for tile state management has not been determined yet, we try to get it from store parameters. if (cacheTileState == null) { try { cacheTileState = ((XMLCoverageStore) pyramid.getPyramidSet().getRef().getStore()).cacheTileState; } catch (Exception e) { // If we've got a problem retrieving cache state parameter, we use default behavior (flushing tile states). cacheTileState = false; } } bitsetLock.writeLock().lock(); try { if (existMask != null && !existMask.isEmpty()) { try { tileExist = BitSet.valueOf(Base64.decode(existMask)); /* * Caching tile state can only be determined at pyramid creation, because a switch of behavior after * that seems a little bit tricky. */ cacheTileState = false; } catch (IOException ex) { LOGGER.log(Level.WARNING, ex.getMessage(), ex); tileExist = new BitSet(gridWidth * gridHeight); } } else { tileExist = cacheTileState ? null : new BitSet(gridWidth * gridHeight); } if (emptyMask != null && !emptyMask.isEmpty()) { try { tileEmpty = BitSet.valueOf(Base64.decode(emptyMask)); } catch (IOException ex) { LOGGER.log(Level.WARNING, ex.getMessage(), ex); tileEmpty = new BitSet(gridWidth * gridHeight); } } else { tileEmpty = cacheTileState ? null : new BitSet(gridWidth * gridHeight); } } finally { bitsetLock.writeLock().unlock(); } /* Here is an handy check, mainly for retro-compatibility purpose. We should only get an empty bit set if the * mosaic has just been created. So, if the mosaic directory exists and contains at least one file, it means * that we've got an old version of pyramid descriptor, or it is corrupted. In such cases, we must cache tile * state in order to retrieve existing ones. */ if (tileExist != null && tileExist.isEmpty() && Files.isDirectory(getFolder())) { try (DirectoryStream dStream = Files.newDirectoryStream(folder)) { if (dStream.iterator().hasNext()) { cacheTileState = true; } } catch (Exception e) { LOGGER.log(Level.WARNING, "Mosaic folder cannot be scanned.", e); } } } private Cache<Point, Boolean> getIsMissingCache() { if (isMissingCache == null) { synchronized (this) { //double check if (isMissingCache == null) { long maxTile = (long)gridWidth * (long)gridHeight; int cacheSize = maxTile > 1000 ? 1000 : (int) maxTile; isMissingCache = new Cache<>(cacheSize, cacheSize, false); } } } return isMissingCache; } private synchronized byte[] createEmptyTile() throws DataStoreException { if (emptyTileEncoded == null) { XMLCoverageReference ref = pyramid.getPyramidSet().getRef(); //create an empty tile final List<XMLSampleDimension> dims = ref.getXMLSampleDimensions(); final BufferedImage emptyTile; if (dims != null && !dims.isEmpty()) { final int dimsSize = dims.size(); emptyTile = BufferedImages.createImage(tileWidth, tileHeight, dimsSize, dims.get(0).getDataType()); //-- fill image by noData if it is possible if (dims.get(0).buildSampleDimension().getNoDataValues() != null) { final boolean[] nodataExists = new boolean[dimsSize]; Arrays.fill(nodataExists, false); final double[] nodatas = new double[dimsSize]; for (int i = 0; i < dimsSize; i++) { final double[] nodat = dims.get(i).buildSampleDimension().getNoDataValues(); if (nodat != null) { nodataExists[i] = true; nodatas[i] = nodat[0];//-- only one value by band is supported } } final PixelIterator pix = PixelIteratorFactory.createDefaultWriteableIterator(emptyTile, emptyTile); int d = 0; while (pix.next()) { if (nodataExists[d]) pix.setSampleDouble(nodatas[d++]); if (d == dimsSize) d = 0; } } } else { ColorModel colorModel = ref.getColorModel(); SampleModel sampleModel = ref.getSampleModel(); if (colorModel != null && sampleModel != null) { int[] java2DColorMap = null; if (colorModel instanceof IndexColorModel) { final IndexColorModel indexColorMod = (IndexColorModel) colorModel; final int mapSize = indexColorMod.getMapSize(); java2DColorMap = new int[mapSize]; indexColorMod.getRGBs(java2DColorMap); // colorMap = new long[mapSize]; // for (int p = 0; p < mapSize; p++) colorMap[p] = rgbs[p]; } emptyTile = ImageUtils.createImage(tileWidth, tileHeight, SampleType.valueOf(sampleModel.getDataType()), sampleModel.getNumBands(), ImageUtils.getEnumPhotometricInterpretation(colorModel), ImageUtils.getEnumPlanarConfiguration(sampleModel), java2DColorMap); } else { emptyTile = new BufferedImage(tileWidth, tileHeight, BufferedImage.TYPE_INT_ARGB); } } final ByteArrayOutputStream out = new ByteArrayOutputStream(); try { ImageIO.write(emptyTile, pyramid.getPyramidSet().getFormatName(), out); out.flush(); } catch (IOException ex) { LOGGER.log(Level.SEVERE, null, ex); } emptyTileEncoded = out.toByteArray(); } return emptyTileEncoded; } private static String updateCompletionString(BitSet input) throws IOException { return Base64.encodeBytes(input.toByteArray(), Base64.GZIP); } /** * Id equals scale string value */ @Override public String getId() { final StringBuilder sb = new StringBuilder(); sb.append(scale); final XMLCoverageReference ref = pyramid.getPyramidSet().getRef(); final String version = ref.getVersion(); if("1.0".equals(version)){ //backward compatibility for older pyramid files for(int i=0;i<upperLeft.length;i++){ sb.append('x'); sb.append(upperLeft[i]); } return sb.toString().replace(DecimalFormatSymbols.getInstance().getDecimalSeparator(), 'd'); }else{ for(int i=0;i<upperLeft.length;i++){ sb.append('x'); synchronized(DECIMAL_FORMAT){ sb.append(DECIMAL_FORMAT.format(upperLeft[i])); } } return sb.toString().replace('.', 'd'); } } public Path getFolder() { if (folder == null) { final Path pyramidDirectory = getPyramid().getFolder(); folder = pyramidDirectory.resolve(getId()); // For retro-compatibility purpose. if (!Files.isDirectory(folder)) { final Path tmpFolder = pyramidDirectory.resolve(String.valueOf(scale)); if (Files.isDirectory(tmpFolder)) { this.folder = tmpFolder; } // Else, it must be a new pyramid, the mosaic directory will be created when the first tile will be written. } } return folder; } @Override public XMLPyramid getPyramid() { return pyramid; } @Override public DirectPosition getUpperLeftCorner() { final GeneralDirectPosition ul = new GeneralDirectPosition(getPyramid().getCoordinateReferenceSystem()); for(int i=0;i<upperLeft.length;i++) ul.setOrdinate(i, upperLeft[i]); return ul; } @Override public Dimension getGridSize() { return new Dimension(gridWidth, gridHeight); } @Override public double getScale() { return scale; } @Override public Dimension getTileSize() { return new Dimension(tileWidth, tileHeight); } @Override public Envelope getEnvelope(final int col, final int row) { final GeneralDirectPosition ul = new GeneralDirectPosition(getUpperLeftCorner()); final int xAxis = CRSUtilities.firstHorizontalAxis(ul.getCoordinateReferenceSystem()); final int yAxis = xAxis + 1; final double minX = ul.getOrdinate(xAxis); final double maxY = ul.getOrdinate(yAxis); final double spanX = tileWidth * scale; final double spanY = tileHeight * scale; final GeneralEnvelope envelope = new GeneralEnvelope(ul,ul); envelope.setRange(xAxis, minX + col*spanX, minX + (col+1)*spanX); envelope.setRange(yAxis, maxY - (row+1)*spanY, maxY - row*spanY); return envelope; } @Override public Envelope getEnvelope() { final GeneralDirectPosition ul = new GeneralDirectPosition(getUpperLeftCorner()); final int xAxis = CRSUtilities.firstHorizontalAxis(ul.getCoordinateReferenceSystem()); assert xAxis >= 0; final int yAxis = xAxis + 1; final double minX = ul.getOrdinate(xAxis); final double maxY = ul.getOrdinate(yAxis); final double spanX = tileWidth * gridWidth * getScale(); final double spanY = tileHeight * gridHeight * getScale(); final GeneralEnvelope envelope = new GeneralEnvelope(ul,ul); envelope.setRange(xAxis, minX, minX + spanX); envelope.setRange(yAxis, maxY - spanY, maxY); return envelope; } @Override public boolean isMissing(int col, int row) throws PointOutsideCoverageException { bitsetLock.readLock().lock(); try { if (tileExist == null || tileExist.isEmpty()) { try { final Point key = new Point(col, row); return getIsMissingCache().getOrCreate(key, new Callable<Boolean>() { @Override public Boolean call() throws Exception { return getTileFile(key.x, key.y) == null; } }); } catch (PointOutsideCoverageException e) { throw e; } catch (Exception e) { LOGGER.log(Level.FINE, e.getLocalizedMessage(), e); return true; } } else { final int index = getTileIndex(col, row); if (index < 0) { LOGGER.log(Level.FINE, "You try to request a tile out of mosaic tile boundary at coordinates : X = "+col+", Y = "+row +"Expected grid boundary : [(0, 0) ; ("+getGridSize().width+","+getGridSize().height+")]"); return true; } return !tileExist.get(index); } } finally { bitsetLock.readLock().unlock(); } } private boolean isEmpty(int col, int row){ bitsetLock.readLock().lock(); try { if (tileEmpty == null || tileEmpty.isEmpty()) { /* For now, if we keep tile state in cache, we consider empty tiles as non-existing. Because without the * appropriate bitset, we would need to scan the tile file to know if it's empty. */ return false; } else { return tileEmpty.get(getTileIndex(col, row)); } } finally { bitsetLock.readLock().unlock(); } } @Override public TileReference getTile(int col, int row, Map hints) throws DataStoreException { final TileReference tile; if (isEmpty(col, row)) { try { tile = new DefaultTileReference(getPyramid().getPyramidSet().getReaderSpi(), ImageIO.createImageInputStream(new ByteArrayInputStream(createEmptyTile())), 0, new Point(col, row)); } catch (IOException ex) { throw new DataStoreException(ex); } } else { tile = new DefaultTileReference(getPyramid().getPyramidSet().getReaderSpi(), getTileFile(col, row), 0, new Point(col, row)); } return tile; } /** * {@inheritDoc } */ @Override public Rectangle getDataArea() { final Path folder = getFolder(); try (DirectoryStream<Path> tileStream = Files.newDirectoryStream(folder)) { Point start = new Point(gridWidth, gridHeight); Point end = new Point(0, 0); Point currPos = null; for (Path tile : tileStream) { final String tileFileName = tile.getFileName().toString(); currPos = parsePosition(tileFileName); start.x = Math.min(start.x, currPos.x); start.y = Math.min(start.y, currPos.y); end.x = Math.max(end.x, currPos.x); end.y = Math.max(end.y, currPos.y); } //no tiles in mosaic directory //return null as documentation says if (currPos == null) { return null; } assert end.x >= start.x; assert end.y >= start.y; return new Rectangle(start.x, start.y, end.x - start.x, end.y - start.y); } catch (IOException e) { LOGGER.log(Level.FINE, "Data area compute failed "+e.getLocalizedMessage(), e); //error with directory stream return null; } } private Point parsePosition(String tileFile) { String posStr = tileFile.substring(0, tileFile.lastIndexOf('.')); String[] split = posStr.split("_"); return new Point(Integer.valueOf(split[1]), Integer.valueOf(split[0])); } @Override public String toString() { final StringBuilder sb = new StringBuilder(Classes.getShortClassName(this)); sb.append(" scale = ").append(getScale()); sb.append(" gridSize[").append(getGridSize().width).append(',').append(getGridSize().height).append(']'); sb.append(" tileSize[").append(getTileSize().width).append(',').append(getTileSize().height).append(']'); return sb.toString(); } /** * Returns the {@linkplain Path file} of tile at col and row index position.<br><br> * * Moreover, this method check all possible path file suffix from pyramid SPI * and return the first that exist else return {@code null} if any exists. * * @param col mosaic column index. * @param row mosaic row index. * @return {@linkplain Path file} of tile at col and row index position if exist, else return {@code null}. * @throws DataStoreException if tile is not present at path file place. */ private Path getTileFile(int col, int row) throws DataStoreException { checkPosition(col, row); for (Path fil : getTileFiles(col, row)) { if (Files.isRegularFile(fil)) return fil; } return null; } /** * Return the first available tile path {@link Path} use to write tile.<br> * * You may choose another suffix {@link Path}, with travel {@link #getTileFiles(int, int) } results. * * @param col mosaic column index. * @param row mosaic row index. * @return the first available tile path {@link Path} use to write tile. * @throws DataStoreException */ private Path getDefaultTileFile(int col, int row) throws DataStoreException { final Path fil = getTileFiles(col, row)[0]; // assert !fil.exists(): "created file should not exist : path : "+fil.getPath(); return fil; } /** * Returns all possible {@linkplain Path files} from all suffix from reader spi. * * @param col mosaic column index. * @param row mosaic row index. * @return all possible {@linkplain Path files} from all suffix from reader spi. * @throws DataStoreException if problem during get suffix. */ private Path[] getTileFiles(int col, int row) throws DataStoreException { final String[] suffixx = getPyramid().getPyramidSet().getReaderSpi().getFileSuffixes(); final Path[] fils = new Path[suffixx.length]; for (int i=0;i<suffixx.length;i++) { fils[i] = getFolder().resolve(row+"_"+col+"."+suffixx[i]); } return fils; } ImageWriter acquireImageWriter() throws IOException { return XImageIO.getWriterByFormatName(getPyramid().getPyramidSet().getFormatName(), null, null); } void createTile(int col, int row, RenderedImage image) throws DataStoreException { ImageWriter writer = null; try { writer = acquireImageWriter(); createTile(col, row, image, writer); } catch (IOException ex) { throw new DataStoreException(ex.getMessage(), ex); } finally { if(writer != null){ writer.dispose(); } } } void createTile(final int col, final int row, final RenderedImage image, final ImageWriter writer) throws DataStoreException { try { checkMosaicFolderExist(); } catch (IOException e) { throw new DataStoreException("Unable to create mosaic folder "+e.getLocalizedMessage(), e); } // No empty tile with cached tile state. if (tileExist != null && isEmpty(image.getData())) { bitsetLock.writeLock().lock(); try { tileExist.set(getTileIndex(col, row), true); tileEmpty.set(getTileIndex(col, row), true); } finally { bitsetLock.writeLock().unlock(); } return; } checkPosition(col, row); Path tilePath = getTileFile(col, row); if (tilePath == null) tilePath = getDefaultTileFile(col, row); ImageOutputStream out = null; try { final Class[] outTypes = writer.getOriginatingProvider().getOutputTypes(); if(ArraysExt.contains(outTypes, Path.class)){ //writer support files directly, let him handle it writer.setOutput(tilePath); }else{ out = ImageIO.createImageOutputStream(tilePath); writer.setOutput(out); } if (out == null) { out = ImageIO.createImageOutputStream(tilePath); } writer.write(image); if (tileExist != null) { final int ti = getTileIndex(col, row); bitsetLock.writeLock().lock(); try { tileExist.set(ti, true); tileEmpty.set(ti, false); } finally { bitsetLock.writeLock().unlock(); } } else { getIsMissingCache().put(new Point(col, row), false); } } catch (IOException ex) { throw new DataStoreException(ex.getMessage(), ex); } finally { if (writer != null) { writer.setOutput(null); } if(out!=null){ try { out.close(); } catch (IOException ex) { throw new DataStoreException(ex); } } } } void writeTiles(final RenderedImage image, final Rectangle area, final boolean onlyMissing, final ProgressMonitor monitor) throws DataStoreException{ try { checkMosaicFolderExist(); } catch (IOException e) { throw new DataStoreException("Unable to create mosaic folder "+e.getLocalizedMessage(), e); } final int offsetX = image.getMinTileX(); final int offsetY = image.getMinTileY(); final int startX = (int)area.getMinX(); final int startY = (int)area.getMinY(); final int endX = (int)area.getMaxX(); final int endY = (int)area.getMaxY(); assert startX >= 0; assert startY >= 0; assert endX > startX && endX <= image.getNumXTiles(); assert endY > startY && endY <= image.getNumYTiles(); final List<Future> futurs = new ArrayList<>(); for(int y=startY; y < endY; y++){ for(int x=startX; x < endX; x++){ if (monitor != null && monitor.isCanceled()) { // Stops submitting new thread return; } final int tx = offsetX+x; final int ty = offsetY+y; if(onlyMissing && !isMissing(tx, ty)){ continue; } final int tileIndex = getTileIndex(tx, ty); checkPosition(tx, ty); Path tilePath = getTileFile(tx, ty); if (tilePath == null) tilePath = getDefaultTileFile(tx, ty); Future fut = TILEWRITEREXECUTOR.submit(new TileWriter(tilePath, image, tx, ty, tileIndex, image.getColorModel(), getPyramid().getPyramidSet().getFormatName(), monitor)); futurs.add(fut); } } //wait for all writing tobe done for (Future f : futurs) { try { f.get(); } catch (InterruptedException | ExecutionException ex) { LOGGER.log(Level.WARNING, ex.getMessage(), ex); } } } private void checkPosition(int col, int row) throws PointOutsideCoverageException { // TODO : Negative indices are allowed ? if(col < 0 || row < 0 || col >= getGridSize().width || row >=getGridSize().height){ throw new PointOutsideCoverageException("Tile position is outside the grid : " + col + " " + row, new GeneralDirectPosition(col, row)); } } /** * Check if a current mosaic folder exist. * If not create it. * * @throws IOException */ private void checkMosaicFolderExist() throws IOException { final Path mosaicFolder = getFolder(); if (!Files.isDirectory(mosaicFolder)) { Files.createDirectories(mosaicFolder); } } private int getTileIndex(int col, int row){ final int index = row*getGridSize().width + col; return index; } @XmlElement protected String getExistMask() { // Flush only if user did not specify to cache tile states. bitsetLock.readLock().lock(); try { if (tileExist == null || cacheTileState) { return null; } return existMask = updateCompletionString(tileExist); } catch (IOException ex) { LOGGER.log(Level.WARNING, ex.getMessage(), ex); return existMask = null; } finally { bitsetLock.readLock().unlock(); } } protected void setExistMask(String newValue) { existMask = newValue; if (existMask != null && !existMask.isEmpty()) { try { tileExist = BitSet.valueOf(Base64.decode(existMask)); } catch (IOException ex) { LOGGER.log(Level.WARNING, ex.getMessage(), ex); } } } @XmlElement protected String getEmptyMask() { // Flush only if user did not specify to cache tile states. bitsetLock.readLock().lock(); try { if (tileEmpty == null || cacheTileState) { return null; } return emptyMask = updateCompletionString(tileEmpty); } catch (IOException ex) { LOGGER.log(Level.WARNING, ex.getMessage(), ex); return existMask = null; } finally { bitsetLock.readLock().unlock(); } } protected void setEmptyMask(String newValue) { emptyMask = newValue; if (emptyMask != null && !emptyMask.isEmpty()) { try { tileEmpty = BitSet.valueOf(Base64.decode(emptyMask,Base64.GZIP)); } catch (IOException ex) { LOGGER.log(Level.WARNING, ex.getMessage(), ex); } } } /** * check if image is empty */ private static boolean isEmpty(Raster raster) { //-- maybe use iterator is more efficiency double[] array = null; searchEmpty: for(int x=0,width=raster.getWidth(); x<width; x++){ for(int y=0,height=raster.getHeight(); y<height; y++){ array = raster.getPixel(x, y, array); for(double d : array){ if(d != 0){ return false; } } } } return true; } @Override public BlockingQueue<Object> getTiles(Collection<? extends Point> positions, Map hints) throws DataStoreException{ return AbstractGridMosaic.getTiles(this, positions, hints); } private class TileWriter implements Runnable{ private final Path tilePath; private final RenderedImage image; private final int idx; private final int idy; private final int tileIndex; private final ColorModel cm; private final String formatName; private final ProgressMonitor monitor; public TileWriter(Path tilePath, RenderedImage image, int idx, int idy, int tileIndex, ColorModel cm, String formatName, ProgressMonitor monitor) { ArgumentChecks.ensureNonNull("file", tilePath); ArgumentChecks.ensureNonNull("image", image); this.tilePath = tilePath; this.image = image; this.idx = idx; this.idy = idy; this.tileIndex = tileIndex; this.cm = cm; this.formatName = formatName; this.monitor = monitor; } @Override public void run() { // Stops writing tile if process cancelled if (monitor != null && monitor.isCanceled()) { return; } ImageWriter writer = null; ImageOutputStream out = null; try { final int offsetX = image.getMinTileX(); final int offsetY = image.getMinTileY(); Raster raster = image.getTile(offsetX+idx, offsetY+idy); //check if image is empty if (tileEmpty != null && (raster == null || isEmpty(raster))) { bitsetLock.writeLock().lock(); try { tileExist.set(tileIndex, true); tileEmpty.set(tileIndex, true); } finally { bitsetLock.writeLock().unlock(); } return; } writer = ImageIO.getImageWritersByFormatName(formatName).next(); final Class[] outTypes = writer.getOriginatingProvider().getOutputTypes(); if (ArraysExt.contains(outTypes, Path.class)) { //writer support files directly, let him handle it writer.setOutput(tilePath); } else { out = ImageIO.createImageOutputStream(tilePath); writer.setOutput(out); } final boolean canWriteRaster = writer.canWriteRasters(); //write tile if (canWriteRaster) { final IIOImage buffer = new IIOImage(raster, null, null); writer.write(buffer); } else { //encapsulate image in a buffered image with parent color model final BufferedImage buffer = new BufferedImage( cm, (WritableRaster) raster, cm.isAlphaPremultiplied(), null); writer.write(buffer); } if (tileExist != null) { bitsetLock.writeLock().lock(); try { tileExist.set(tileIndex, true); tileEmpty.set(tileIndex, false); } finally { bitsetLock.writeLock().unlock(); } } else { getIsMissingCache().put(new Point(idx, idy), false); } } catch (Exception ex) { LOGGER.log(Level.WARNING, ex.getMessage(), ex); throw new RuntimeException(ex.getMessage(), ex); } finally { if (writer != null) { writer.dispose(); if (out != null) { try { out.close(); } catch (IOException ex) { LOGGER.log(Level.SEVERE, null, ex); } } } } } } /** * For retro-compatibility purpose with the 2D-limited pyramids. X coordinate of the upper-left point of the mosaic. * DO NOT put a getter, as we don't want it to be written, only read from description file. * @param x The X coordinate of the upper-left point of the mosaic. */ @XmlElement private void setupperleftX(double x) { if (upperLeft == null) { upperLeft = new double[2]; } upperLeft[0] = x; } /** * For retro-compatibility purpose. Return null, because we don't want to write it, just need it at reading. * @return null */ private Double getupperleftX() { return null; } /** * For retro-compatibility purpose with the 2D-limited pyramids. Y coordinate of the upper-left point of the mosaic. * DO NOT put a getter, as we don't want it to be written, only read from description file. * @param y The Y coordinate of the upper-left point of the mosaic. */ @XmlElement private void setupperleftY(double y) { if (upperLeft == null) { upperLeft = new double[2]; } upperLeft[1] = y; } /** * For retro-compatibility purpose. Return null, because we don't want to write it, just need it at reading. * @return null */ private Double getupperleftY() { return null; } }