/*
* Geotoolkit - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2012 - 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.display2d.process.pyramid;
import org.apache.sis.geometry.Envelopes;
import org.apache.sis.geometry.GeneralDirectPosition;
import org.apache.sis.geometry.GeneralEnvelope;
import org.apache.sis.internal.referencing.j2d.AffineTransform2D;
import org.apache.sis.referencing.operation.transform.MathTransforms;
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.util.ArgumentChecks;
import org.geotoolkit.storage.coverage.CoverageUtilities;
import org.geotoolkit.storage.coverage.GridMosaic;
import org.geotoolkit.storage.coverage.Pyramid;
import org.geotoolkit.storage.coverage.PyramidalCoverageReference;
import org.geotoolkit.display.PortrayalException;
import org.geotoolkit.display2d.service.CanvasDef;
import org.geotoolkit.display2d.service.PortrayalRenderedImage;
import org.geotoolkit.display2d.service.SceneDef;
import org.geotoolkit.display2d.service.ViewDef;
import org.geotoolkit.factory.Hints;
import org.geotoolkit.internal.referencing.CRSUtilities;
import org.geotoolkit.map.MapBuilder;
import org.geotoolkit.map.MapContext;
import org.geotoolkit.processing.AbstractProcess;
import org.geotoolkit.process.ProcessException;
import org.apache.sis.referencing.CRS;
import org.geotoolkit.referencing.ReferencingUtilities;
import org.opengis.geometry.DirectPosition;
import org.opengis.geometry.Envelope;
import org.opengis.parameter.ParameterNotFoundException;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.MathTransform2D;
import org.opengis.referencing.operation.TransformException;
import org.opengis.util.FactoryException;
import javax.swing.*;
import java.awt.*;
import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.CancellationException;
import static org.geotoolkit.parameter.Parameters.getOrCreate;
import static org.geotoolkit.parameter.Parameters.value;
import static org.geotoolkit.display2d.process.pyramid.MapcontextPyramidDescriptor.*;
/**
* Create a pyramid in the given PyramidalModel.
* If a pyramid with the given CRS already exist it will be reused.
* If a mosaic at the given scale exist it will be used.
* Missing tiles in the mosaic will be generated.
*
* @author Johann Sorel (Geomatys)
* @module
*/
public final class MapcontextPyramidProcess extends AbstractProcess {
private volatile long progress = 0;
private long total = 0;
MapcontextPyramidProcess(final ParameterValueGroup input) {
super(INSTANCE,input);
}
@Override
protected void execute() throws ProcessException {
ArgumentChecks.ensureNonNull("inputParameters", inputParameters);
final MapContext context = value(IN_MAPCONTEXT, inputParameters);
final Envelope envelope = value(IN_EXTENT, inputParameters);
final Dimension tileSize = value(IN_TILE_SIZE, inputParameters);
final double[] scales = value(IN_SCALES, inputParameters);
Integer nbpainter = value(IN_NBPAINTER, inputParameters);
final PyramidalCoverageReference container = value(IN_CONTAINER, inputParameters);
final Boolean update = value(IN_UPDATE, inputParameters);
if(nbpainter == null){
nbpainter = Runtime.getRuntime().availableProcessors();
}
Hints hints = null;
try{
hints = value(IN_HINTS, inputParameters);
} catch (ParameterNotFoundException ex) {
// Get the hint parameter if exist, otherwise keep it to null.
}
final Envelope ctxEnv;
try {
ctxEnv = context.getBounds();
} catch (IOException e) {
throw new ProcessException(e.getMessage(), this, e);
}
if (update != null && update) {
context.layers().add(0, MapBuilder.createCoverageLayer(container));
}
final CoordinateReferenceSystem ctxCRS = context.getCoordinateReferenceSystem();
final CoordinateReferenceSystem destCRS = envelope.getCoordinateReferenceSystem();
final int widthAxis = CoverageUtilities.getMinOrdinate(destCRS);
final int heightAxis = widthAxis + 1;
//calculate the number of tile to generate
double destEnvWidth = envelope.getSpan(widthAxis);
double destEnvHeight = envelope.getSpan(heightAxis);
for(double scale : scales){
final double gridWidth = destEnvWidth / (scale*tileSize.width);
final double gridHeight = destEnvHeight / (scale*tileSize.height);
total += Math.ceil(gridWidth) * Math.ceil(gridHeight);
}
final CanvasDef canvasDef = new CanvasDef(new Dimension(1, 1), null);
final ViewDef viewDef = new ViewDef(envelope);
final SceneDef sceneDef = new SceneDef(context,hints);
//find if we already have a pyramid in the given CRS
Pyramid pyramid = null;
try {
final Envelope clipEnv = ReferencingUtilities.intersectEnvelopes(ctxEnv, envelope);
final MathTransform destCRS_to_ctxCRS = CRS.findOperation(CRSUtilities.getCRS2D(destCRS), CRSUtilities.getCRS2D(ctxCRS), null).getMathTransform();
for (Pyramid candidate : container.getPyramidSet().getPyramids()) {
if (org.geotoolkit.referencing.CRS.equalsApproximatively(destCRS, candidate.getCoordinateReferenceSystem())) {
pyramid = candidate;
break;
}
}
if (pyramid == null) {
//we didn't find a pyramid, create one
pyramid = container.createPyramid(destCRS);
}
final String pyramidId = pyramid.getId();
//generate each mosaic
for (final double scale : scales) {
if (isCanceled()) {
throw new CancellationException();
}
//compute mosaic gridSize
final double gridWidth = destEnvWidth / (scale*tileSize.width);
final double gridHeight = destEnvHeight / (scale*tileSize.height);
final Dimension gridSize = new Dimension( (int)(Math.ceil(gridWidth)), (int)(Math.ceil(gridHeight)));
//find mosaic
final GridMosaic mosaic = getOrCreateMosaic(container, pyramidId, scale, envelope, tileSize, gridSize);
if (isCanceled()) {
throw new CancellationException();
}
final PortrayalRenderedImage image = new PortrayalRenderedImage(
canvasDef, sceneDef, viewDef,
mosaic.getGridSize(), mosaic.getTileSize(), scale);
image.addProgressListener(new PortrayalRenderedImage.ProgressListener() {
@Override
public void tileCreated(int x, int y) {
progress++;
progress();
}
});
// Transform context envelope into mosaic grid system to
// find tiles impacted on container mosaic
final double min0 = envelope.getMinimum(widthAxis);
final double max1 = envelope.getMaximum(heightAxis);
final MathTransform2D gridDest_to_crs = new AffineTransform2D(scale, 0, 0, -scale, min0, max1);
final MathTransform ctxCRS_to_gridDest = MathTransforms.concatenate(gridDest_to_crs, destCRS_to_ctxCRS).inverse();
final GeneralEnvelope ctxExtent = Envelopes.transform(ctxCRS_to_gridDest, clipEnv);
final int startTileX = (int)ctxExtent.getMinimum(widthAxis) / tileSize.width;
final int startTileY = (int)ctxExtent.getMinimum(heightAxis) / tileSize.height;
final int endTileX = ((int)(ctxExtent.getMaximum(widthAxis) + tileSize.width - 1) / tileSize.width) - startTileX;
final int endTileY = ((int)(ctxExtent.getMaximum(heightAxis) + tileSize.height - 1) / tileSize.height) - startTileY;
final Rectangle area = new Rectangle(startTileX, startTileY, endTileX, endTileY);
container.writeTiles(pyramid.getId(), mosaic.getId(), image, area, false, new MapcontextPyramidMonitor(this));
if (isCanceled()) {
throw new CancellationException();
}
getOrCreate(OUT_CONTAINER, outputParameters).setValue(container);
}
} catch (DataStoreException | FactoryException | TransformException | PortrayalException ex) {
throw new ProcessException(ex.getMessage(), this, ex);
}
}
/**
* Get or create a new mosaic in container for a specific scale and destination envelope.
*
* @param container
* @param pyramidId
* @param scale
* @param destEnvelope
* @param tileSize
* @param gridSize
* @return GridMosaic
* @throws FactoryException
* @throws DataStoreException
*/
private GridMosaic getOrCreateMosaic(PyramidalCoverageReference container, String pyramidId, double scale,
final Envelope destEnvelope, final Dimension tileSize, final Dimension gridSize)
throws FactoryException, DataStoreException {
final Pyramid pyramid = container.getPyramidSet().getPyramid(pyramidId);
final CoordinateReferenceSystem destCRS = pyramid.getCoordinateReferenceSystem();
final int widthAxis = CoverageUtilities.getMinOrdinate(destCRS);
final int heightAxis = widthAxis + 1;
final DirectPosition upperLeft = new GeneralDirectPosition(destCRS);
upperLeft.setOrdinate(widthAxis, destEnvelope.getMinimum(widthAxis));
upperLeft.setOrdinate(heightAxis, destEnvelope.getMaximum(heightAxis));
int outDim = destEnvelope.getDimension();
for (int d = 0; d < outDim; d++) {
if (d != widthAxis && d != heightAxis) {
//set upperLeft extra dimension ordinate from requested envelope
upperLeft.setOrdinate(d, destEnvelope.getMinimum(d));
}
}
//search with scale and upperLeft
for(GridMosaic gm : pyramid.getMosaics()){
if(gm.getScale() == scale && Arrays.equals(upperLeft.getCoordinate(), gm.getUpperLeftCorner().getCoordinate())){
return gm;
}
}
//create a new mosaic
return container.createMosaic(pyramid.getId(),gridSize, tileSize, upperLeft, scale);
}
private void progress(){
fireProgressing(progress+"/"+total, (float)((double)progress/(double)total)*100f, false);
}
/**
* Allow to cancel the tiles creation process.
*/
private class MapcontextPyramidMonitor extends ProgressMonitor {
private final MapcontextPyramidProcess process;
public MapcontextPyramidMonitor(final MapcontextPyramidProcess process) {
super(new JLabel(), "", "", 0, 100);
this.process = process;
}
@Override
public boolean isCanceled() {
return process.isCanceled();
}
}
}