/*
* 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.container.statefull;
import org.apache.sis.geometry.GeneralEnvelope;
import org.apache.sis.storage.DataStoreException;
import org.geotoolkit.storage.coverage.CoverageStoreContentEvent;
import org.geotoolkit.storage.coverage.CoverageStoreListener;
import org.geotoolkit.storage.coverage.CoverageStoreManagementEvent;
import org.geotoolkit.storage.coverage.GridMosaic;
import org.geotoolkit.storage.coverage.Pyramid;
import org.geotoolkit.storage.coverage.PyramidSet;
import org.geotoolkit.storage.coverage.PyramidalCoverageReference;
import org.geotoolkit.coverage.finder.CoverageFinder;
import org.geotoolkit.display.canvas.control.CanvasMonitor;
import org.geotoolkit.display2d.GO2Hints;
import org.geotoolkit.display2d.GO2Utilities;
import org.geotoolkit.display2d.canvas.J2DCanvas;
import org.geotoolkit.display2d.canvas.RenderingContext2D;
import org.geotoolkit.display2d.style.CachedRule;
import org.opengis.util.GenericName;
import org.geotoolkit.internal.referencing.CRSUtilities;
import org.geotoolkit.map.CoverageMapLayer;
import org.geotoolkit.referencing.CRS;
import org.geotoolkit.referencing.ReferencingUtilities;
import org.opengis.geometry.DirectPosition;
import org.opengis.geometry.Envelope;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.TransformException;
import org.opengis.util.FactoryException;
import javax.vecmath.Point3d;
import java.awt.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.logging.Level;
import org.geotoolkit.coverage.finder.DefaultCoverageFinder;
import org.apache.sis.geometry.Envelopes;
/**
* Graphic for pyramidal coverage layers.
*
* @author Johann Sorel (Geomatys)
* @module
*/
public class StatefullPyramidalCoverageLayerJ2D extends StatefullMapLayerJ2D<CoverageMapLayer> implements CoverageStoreListener{
protected CoverageStoreListener.Weak weakStoreListener = new CoverageStoreListener.Weak(this);
private final PyramidalCoverageReference model;
private final double tolerance;
private final CoverageFinder coverageFinder;
private final Map<Point3d,StatefullTileJ2D> gtiles = new TreeMap<>(new Comparator<Point3d>() {
@Override
public int compare(Point3d c1, Point3d c2) {
final double zdiff = c2.z - c1.z; //we want lower res tiles to be painted first
if(zdiff > 0) return +1;
else if(zdiff < 0) return -1;
else{
final double ydiff = c1.y - c2.y;
if(ydiff > 0) return +1;
else if(ydiff < 0) return -1;
else{
final double xdiff = c1.x - c2.x;
if(xdiff > 0) return +1;
else if(xdiff < 0) return -1;
else return 0; //same point
}
}
}
});
public StatefullPyramidalCoverageLayerJ2D(final J2DCanvas canvas, final CoverageMapLayer layer) {
super(canvas, layer, true);
this.coverageFinder = new DefaultCoverageFinder();
model = (PyramidalCoverageReference)layer.getCoverageReference();
tolerance = 0.1; // in % , TODO use a flag to allow change value
weakStoreListener.registerSource(layer.getCoverageReference());
}
public StatefullPyramidalCoverageLayerJ2D(final J2DCanvas canvas, final CoverageMapLayer layer, CoverageFinder coverageFinder) {
super(canvas, layer, true);
this.coverageFinder = coverageFinder;
model = (PyramidalCoverageReference)layer.getCoverageReference();
tolerance = 0.1; // in % , TODO use a flag to allow change value
weakStoreListener.registerSource(layer.getCoverageReference());
}
/**
* {@inheritDoc }
* @param context2D
*/
@Override
public void paint(RenderingContext2D context2D) {
if(!item.isVisible()) return;
final GenericName coverageName = item.getCoverageReference().getName();
final CachedRule[] rules = GO2Utilities.getValidCachedRules(item.getStyle(),
context2D.getSEScale(), coverageName,null);
//we perform a first check on the style to see if there is at least
//one valid rule at this scale, if not we just continue.
if (rules.length == 0) {
return;
}
final CanvasMonitor monitor = context2D.getMonitor();
final Envelope canvasEnv2D = context2D.getCanvasObjectiveBounds2D();
final Envelope canvasEnv = context2D.getCanvasObjectiveBounds2D();
final PyramidSet pyramidSet;
try {
pyramidSet = model.getPyramidSet();
} catch (DataStoreException ex) {
monitor.exceptionOccured(ex, Level.WARNING);
return;
}
Pyramid pyramid = null;
try {
pyramid = coverageFinder.findPyramid(pyramidSet, canvasEnv.getCoordinateReferenceSystem());
} catch (FactoryException ex) {
monitor.exceptionOccured(ex, Level.WARNING);
return;
}
if(pyramid == null){
//no reliable pyramid
return;
}
final CoordinateReferenceSystem pyramidCRS = pyramid.getCoordinateReferenceSystem();
final CoordinateReferenceSystem pyramidCRS2D;
GeneralEnvelope wantedEnv2D;
GeneralEnvelope wantedEnv;
try {
pyramidCRS2D = CRSUtilities.getCRS2D(pyramidCRS);
wantedEnv2D = new GeneralEnvelope(Envelopes.transform(canvasEnv2D, pyramidCRS2D));
wantedEnv = new GeneralEnvelope(ReferencingUtilities.transform(canvasEnv, pyramidCRS));
} catch (TransformException ex) {
monitor.exceptionOccured(ex, Level.WARNING);
return;
}
//ensure we don't go out of the crs envelope
final Envelope maxExt = CRS.getEnvelope(pyramidCRS);
if(maxExt != null){
if(Double.isNaN(wantedEnv2D.getMinimum(0))){ wantedEnv2D.setRange(0, maxExt.getMinimum(0), wantedEnv2D.getMaximum(0)); }
if(Double.isNaN(wantedEnv2D.getMaximum(0))){ wantedEnv2D.setRange(0, wantedEnv2D.getMinimum(0), maxExt.getMaximum(0)); }
if(Double.isNaN(wantedEnv2D.getMinimum(1))){ wantedEnv2D.setRange(1, maxExt.getMinimum(1), wantedEnv2D.getMaximum(1)); }
if(Double.isNaN(wantedEnv2D.getMaximum(1))){ wantedEnv2D.setRange(1, wantedEnv2D.getMinimum(1), maxExt.getMaximum(1)); }
wantedEnv.setRange(0, wantedEnv2D.getMinimum(0), wantedEnv2D.getMaximum(0));
wantedEnv.setRange(1, wantedEnv2D.getMinimum(1), wantedEnv2D.getMaximum(1));
}
//the wanted image resolution
final double wantedResolution;
try {
wantedResolution = GO2Utilities.pixelResolution(context2D, wantedEnv);
} catch (TransformException ex) {
monitor.exceptionOccured(ex, Level.WARNING);
return;
}
GridMosaic mosaic = null;
try {
mosaic = coverageFinder.findMosaic(pyramid, wantedResolution, tolerance, wantedEnv,100);
} catch (FactoryException ex) {
monitor.exceptionOccured(ex, Level.WARNING);
return;
}
if(mosaic == null){
//no reliable mosaic
return;
}
GO2Utilities.removeNaN(wantedEnv);
final DirectPosition ul = mosaic.getUpperLeftCorner();
final double tileMatrixMinX = ul.getOrdinate(0);
final double tileMatrixMaxY = ul.getOrdinate(1);
final Dimension gridSize = mosaic.getGridSize();
final Dimension tileSize = mosaic.getTileSize();
final double scale = mosaic.getScale();
final double tileSpanX = scale * tileSize.width;
final double tileSpanY = scale * tileSize.height;
final int gridWidth = gridSize.width;
final int gridHeight = gridSize.height;
//find all the tiles we need --------------------------------------
final double epsilon = 1e-6;
final double bBoxMinX = wantedEnv.getMinimum(0);
final double bBoxMaxX = wantedEnv.getMaximum(0);
final double bBoxMinY = wantedEnv.getMinimum(1);
final double bBoxMaxY = wantedEnv.getMaximum(1);
double tileMinCol = Math.floor( (bBoxMinX - tileMatrixMinX) / tileSpanX + epsilon);
double tileMaxCol = Math.floor( (bBoxMaxX - tileMatrixMinX) / tileSpanX - epsilon)+1;
double tileMinRow = Math.floor( (tileMatrixMaxY - bBoxMaxY) / tileSpanY - epsilon);
double tileMaxRow = Math.floor( (tileMatrixMaxY - bBoxMinY) / tileSpanY + epsilon)+1;
//ensure we dont go out of the grid
if(tileMinCol < 0) tileMinCol = 0;
if(tileMaxCol > gridWidth) tileMaxCol = gridWidth;
if(tileMinRow < 0) tileMinRow = 0;
if(tileMaxRow > gridHeight) tileMaxRow = gridHeight;
//don't render layer if it requieres more then 100 queries
Integer maxTiles = (Integer)context2D.getRenderingHints().get(GO2Hints.KEY_MAX_TILES);
if(maxTiles==null) maxTiles = 500;
if( (tileMaxCol-tileMinCol) * (tileMaxRow-tileMinRow) > maxTiles) {
LOGGER.log(Level.INFO, "Too much tiles requiered to render layer at this scale.");
return;
}
//tiles to render
final Set<Point3d> ttiles = new HashSet<>();
for(int tileCol=(int)tileMinCol; tileCol<tileMaxCol; tileCol++){
for(int tileRow=(int)tileMinRow; tileRow<tileMaxRow; tileRow++){
if(mosaic.isMissing(tileCol, tileRow)){
//tile not available
continue;
}
ttiles.add(new Point3d(tileCol, tileRow, scale));
}
}
//update graphic tiles -------------------------------------------------
final Collection<Point3d> toRemove = new ArrayList<>();
loop:
for(StatefullTileJ2D st : gtiles.values()){
if(ttiles.contains(st.getCoordinate())){
continue loop;
}
//avoids loading the tile if it's already in the update queue
st.setObsoleted(true);
if(st.isLoaded()){
final Point3d[] coords = getReplacements(pyramid, st.getCoordinate(), mosaic,
tileMinCol,tileMaxCol,tileMinRow,tileMaxRow);
for(Point3d c : coords){
if(!ttiles.contains(c)) continue;
final StatefullTileJ2D rst = gtiles.get(c);
if(rst == null || !rst.isLoaded()){
//replacement tiles are not all loaded, keep the previous tile
continue loop;
}
}
}
toRemove.add(st.getCoordinate());
}
//remove old tiles
for(Point3d pt : toRemove){
StatefullTileJ2D tile = gtiles.remove(pt);
getChildren().remove(tile);
}
//add new tiles
for(Point3d c : ttiles){
if(!gtiles.containsKey(c)){
StatefullTileJ2D tile = new StatefullTileJ2D(mosaic, c, getCanvas(), item, rules);
gtiles.put(c,tile);
getChildren().add(tile);
}
}
//paint sub tiles ------------------------------------------------------
final Object[] cp = getChildren().toArray();
for(Object obj : cp){
((StatefullTileJ2D)obj).paint(context2D);
}
}
@Override
protected synchronized void update() {
}
@Override
public void structureChanged(CoverageStoreManagementEvent event) {
}
@Override
public void contentChanged(CoverageStoreContentEvent event) {
//TODO should call a repaint only on this graphic
gtiles.clear();
getCanvas().repaint();
}
private Point3d[] getReplacements(Pyramid pyramid, Point3d coord, final GridMosaic mosaicUpdate,
double qtileMinCol, double qtileMaxCol, double qtileMinRow, double qtileMaxRow){
double[] tscales = pyramid.getScales();
final int indexBase = Arrays.binarySearch(tscales, coord.z);
final GridMosaic mosaicBase = pyramid.getMosaics(indexBase).iterator().next();
final Envelope env = mosaicBase.getEnvelope((int)coord.x, (int)coord.y);
double bBoxMinX = env.getMinimum(0);
double bBoxMinY = env.getMinimum(1);
double bBoxMaxX = env.getMaximum(0);
double bBoxMaxY = env.getMaximum(1);
final DirectPosition ul = mosaicUpdate.getUpperLeftCorner();
final double tileMatrixMinX = ul.getOrdinate(0);
final double tileMatrixMaxY = ul.getOrdinate(1);
final Dimension gridSize = mosaicUpdate.getGridSize();
final Dimension tileSize = mosaicUpdate.getTileSize();
final double scale = mosaicUpdate.getScale();
final double tileSpanX = scale * tileSize.width;
final double tileSpanY = scale * tileSize.height;
final int gridWidth = gridSize.width;
final int gridHeight = gridSize.height;
final double epsilon = 1e-6;
double tileMinCol = Math.floor( (bBoxMinX - tileMatrixMinX) / tileSpanX + epsilon);
double tileMaxCol = Math.floor( (bBoxMaxX - tileMatrixMinX) / tileSpanX - epsilon)+1;
double tileMinRow = Math.floor( (tileMatrixMaxY - bBoxMaxY) / tileSpanY + epsilon);
double tileMaxRow = Math.floor( (tileMatrixMaxY - bBoxMinY) / tileSpanY - epsilon)+1;
tileMinCol = Math.max(tileMinCol, qtileMinCol);
tileMaxCol = Math.min(tileMaxCol, qtileMaxCol);
tileMinRow = Math.max(tileMinRow, qtileMinRow);
tileMaxRow = Math.min(tileMaxRow, qtileMaxRow);
//ensure we dont go out of the grid
if(tileMinCol < 0) tileMinCol = 0;
if(tileMaxCol > gridWidth) tileMaxCol = gridWidth;
if(tileMinRow < 0) tileMinRow = 0;
if(tileMaxRow > gridHeight) tileMaxRow = gridHeight;
//tiles to render
final Set<Point3d> ttiles = new HashSet<>();
if( ((tileMaxCol-tileMinCol)*(tileMaxRow-tileMinRow)) > 100){
}
for(int tileCol=(int)tileMinCol; tileCol<tileMaxCol; tileCol++){
for(int tileRow=(int)tileMinRow; tileRow<tileMaxRow; tileRow++){
if(mosaicUpdate.isMissing(tileCol, tileRow)){
//tile not available
continue;
}
ttiles.add(new Point3d(tileCol, tileRow, scale));
}
}
return ttiles.toArray(new Point3d[0]);
}
}