/* * 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.stateless; import com.vividsolutions.jts.geom.Geometry; import org.apache.sis.geometry.GeneralEnvelope; import org.apache.sis.storage.DataStoreException; import org.geotoolkit.storage.coverage.AbstractGridMosaic; import org.geotoolkit.storage.coverage.AbstractPyramidalCoverageReference; 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.storage.coverage.TileReference; import org.geotoolkit.coverage.finder.CoverageFinder; import org.geotoolkit.coverage.grid.GridCoverage2D; import org.geotoolkit.display.PortrayalException; import org.geotoolkit.display.SearchArea; import org.geotoolkit.display.VisitFilter; import org.geotoolkit.display.canvas.RenderingContext; 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.primitive.DefaultSearchAreaJ2D; import org.geotoolkit.display2d.primitive.GraphicJ2D; import org.geotoolkit.display2d.primitive.ProjectedCoverage; import org.geotoolkit.display2d.primitive.SearchAreaJ2D; import org.geotoolkit.display2d.style.CachedRule; import org.geotoolkit.display2d.style.CachedSymbolizer; import org.geotoolkit.display2d.style.renderer.DefaultRasterSymbolizerRenderer; import org.opengis.util.GenericName; import org.geotoolkit.geometry.jts.JTS; import org.geotoolkit.internal.referencing.CRSUtilities; import org.geotoolkit.map.CoverageMapLayer; import org.geotoolkit.map.GraphicBuilder; import org.geotoolkit.map.MapBuilder; import org.apache.sis.referencing.CRS; import org.geotoolkit.referencing.ReferencingUtilities; import org.geotoolkit.util.Cancellable; import org.opengis.display.primitive.Graphic; import org.opengis.geometry.DirectPosition; import org.opengis.geometry.Envelope; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.TransformException; import org.opengis.util.FactoryException; import java.awt.*; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.BlockingQueue; import java.util.concurrent.TimeUnit; 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 StatelessPyramidalCoverageLayerJ2D extends StatelessMapLayerJ2D<CoverageMapLayer> implements CoverageStoreListener{ protected CoverageStoreListener.Weak weakStoreListener = new CoverageStoreListener.Weak(this); private final PyramidalCoverageReference model; private final double tolerance; private final CoverageFinder coverageFinder; public StatelessPyramidalCoverageLayerJ2D(final J2DCanvas canvas, final CoverageMapLayer layer){ super(canvas, layer, false); this.coverageFinder = new DefaultCoverageFinder(); model = (PyramidalCoverageReference)layer.getCoverageReference(); tolerance = 0.25; // in % , TODO use a flag to allow change value this.weakStoreListener.registerSource(layer.getCoverageReference()); } public StatelessPyramidalCoverageLayerJ2D(final J2DCanvas canvas, final CoverageMapLayer layer, CoverageFinder coverageFinder){ super(canvas, layer, false); this.coverageFinder = coverageFinder; model = (PyramidalCoverageReference)layer.getCoverageReference(); tolerance = 0.25; // in % , TODO use a flag to allow change value this.weakStoreListener.registerSource(layer.getCoverageReference()); } /** * {@inheritDoc } * @param context2D */ @Override public void paintLayer(final RenderingContext2D context2D) { final CanvasMonitor monitor = context2D.getMonitor(); final TileSetResult result = listTiles(context2D); if(result==null){ //no pyramid or tiles match this context definition return; } //tiles to render final Map<Point,MathTransform> queries = new HashMap<Point,MathTransform>(); final Map hints = new HashMap(item.getUserProperties()); for(int tileCol=(int)result.tileMinCol; tileCol<result.tileMaxCol; tileCol++){ for(int tileRow=(int)result.tileMinRow; tileRow<result.tileMaxRow; tileRow++){ if(result.mosaic.isMissing(tileCol, tileRow)){ //tile not available continue; } final Point pt = new Point(tileCol, tileRow); final MathTransform trs = AbstractGridMosaic.getTileGridToCRS(result.mosaic, pt); queries.put(pt,trs); } } //paint tiles ---------------------------------------------------------- if(queries.isEmpty()){ //bypass if no queries return; } Integer maxTiles = (Integer)context2D.getRenderingHints().get(GO2Hints.KEY_MAX_TILES); if(maxTiles==null) maxTiles = 500; if( queries.size() > maxTiles) { LOGGER.log(Level.INFO, "Too much tiles required to render layer at this scale."); return; } final BlockingQueue<Object> queue; try { queue = result.mosaic.getTiles(queries.keySet(), hints); } catch (DataStoreException ex) { monitor.exceptionOccured(ex, Level.WARNING); return; } final StatelessContextParams params = new StatelessContextParams(getCanvas(), getUserObject()); params.update(context2D); while(true){ Object obj = null; try { obj = queue.poll(50, TimeUnit.MILLISECONDS); } catch (InterruptedException ex) { monitor.exceptionOccured(ex, Level.INFO); } if(monitor.stopRequested()){ if(queue instanceof Cancellable){ ((Cancellable)queue).cancel(); } break; } if(obj == GridMosaic.END_OF_QUEUE){ break; } if(obj instanceof TileReference){ final TileReference tile = (TileReference)obj; try { paintTile(context2D, params, result.rules, result.pyramid.getId(), result.mosaic.getId(), tile); } catch (Exception e) { LOGGER.log(Level.WARNING, "Error on tile : "+tile.getPosition()+". Input is : "+tile.getInput(), e); } } } } /** * {@inheritDoc } * @param context * @param mask * @param filter * @param graphics */ @Override public List<Graphic> getGraphicAt(final RenderingContext context, final SearchArea mask, final VisitFilter filter, List<Graphic> graphics) { if(!(context instanceof RenderingContext2D) ) return graphics; if(!item.isSelectable()) return graphics; if(!item.isVisible()) return graphics; final RenderingContext2D renderingContext = (RenderingContext2D) context; //search special graphic builders final GraphicBuilder<GraphicJ2D> builder = (GraphicBuilder<GraphicJ2D>) item.getGraphicBuilder(GraphicJ2D.class); if(builder != null){ //this layer hasa special graphic rendering, use it instead of normal rendering final Collection<GraphicJ2D> gras = builder.createGraphics(item, canvas); for(final GraphicJ2D gra : gras){ graphics = gra.getGraphicAt(renderingContext, mask, filter,graphics); } return graphics; } if(graphics == null) graphics = new ArrayList<>(); if(mask instanceof SearchAreaJ2D){ graphics = searchAt(renderingContext,(SearchAreaJ2D)mask,filter,graphics); }else{ graphics = searchAt(renderingContext,new DefaultSearchAreaJ2D(mask),filter,graphics); } return graphics; } private List<Graphic> searchAt(final RenderingContext2D context2D, final SearchAreaJ2D mask, final VisitFilter filter, List<Graphic> graphics) { //search tiles visible on this area final TileSetResult result = listTiles(context2D); if(result==null){ //no pyramid or tiles match this context definition return graphics; } final Geometry searchGeom; try { searchGeom = JTS.transform(mask.getObjectiveGeometryJTS(), CRS.findOperation(context2D.getObjectiveCRS2D(), result.pyramidCRS2D, null).getMathTransform()); } catch (Exception ex) { LOGGER.log(Level.INFO, ex.getMessage(),ex); return graphics; } final StatelessContextParams params = new StatelessContextParams(getCanvas(), getUserObject()); params.update(context2D); //search for a tile which intersects the seach area final PyramidalCoverageReference covRef = (PyramidalCoverageReference) item.getCoverageReference(); for(int tileCol=(int)result.tileMinCol; tileCol<result.tileMaxCol; tileCol++){ for(int tileRow=(int)result.tileMinRow; tileRow<result.tileMaxRow; tileRow++){ if(result.mosaic.isMissing(tileCol, tileRow)){ //tile not available continue; } final Envelope tileEnvelope = result.mosaic.getEnvelope(tileCol, tileRow); final Geometry geom = JTS.toGeometry(tileEnvelope); if(searchGeom.intersects(geom)){ final ProjectedCoverage pc = asCoverage(context2D, params, covRef, result.pyramid.getId(), result.mosaic.getId(),tileCol,tileRow); graphics.add(pc); } } } return graphics; } /** * File the tiles to read for rendering. * * @param context2D * @return */ private TileSetResult listTiles(RenderingContext2D context2D){ final TileSetResult result = new TileSetResult(); final GenericName coverageName = item.getCoverageReference().getName(); result.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 (result.rules.length == 0) { return null; } final CanvasMonitor monitor = context2D.getMonitor(); final Envelope canvasEnv2D = context2D.getCanvasObjectiveBounds2D(); final Envelope canvasEnv = context2D.getCanvasObjectiveBounds(); //find the best pyramid try { result.pyramidSet = model.getPyramidSet(); } catch (DataStoreException ex) { monitor.exceptionOccured(ex, Level.WARNING); return null; } try { result.pyramid = coverageFinder.findPyramid(result.pyramidSet, canvasEnv2D.getCoordinateReferenceSystem()); } catch (FactoryException ex) { monitor.exceptionOccured(ex, Level.WARNING); return null; } if(result.pyramid == null){ //no reliable pyramid return null; } //convert context envelope to pyramid CRS final CoordinateReferenceSystem pyramidCRS = result.pyramid.getCoordinateReferenceSystem(); GeneralEnvelope wantedEnv2D; GeneralEnvelope wantedEnv; try { result.pyramidCRS2D = CRSUtilities.getCRS2D(pyramidCRS); wantedEnv2D = new GeneralEnvelope(Envelopes.transform(canvasEnv2D, result.pyramidCRS2D)); wantedEnv = new GeneralEnvelope(ReferencingUtilities.transform(canvasEnv, pyramidCRS)); } catch (TransformException ex) { monitor.exceptionOccured(ex, Level.WARNING); return null; } /* * Apply CoverageMapLayer query (if not null) to wantedEnv Envelope. */ final Map<String, Double> queryValues = DefaultRasterSymbolizerRenderer.extractQuery(item); wantedEnv = new GeneralEnvelope(DefaultRasterSymbolizerRenderer.fixEnvelopeWithQuery(queryValues, wantedEnv, pyramidCRS)); //ensure we don't go out of the crs envelope final Envelope maxExt = org.geotoolkit.referencing.CRS.getEnvelope(pyramidCRS); if(maxExt != null){ wantedEnv2D.intersect(maxExt); 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)); } //find the correct pyramid final double wantedResolution; try { wantedResolution = GO2Utilities.pixelResolution(context2D, wantedEnv); } catch (TransformException ex) { monitor.exceptionOccured(ex, Level.WARNING); return null; } try { result.mosaic = coverageFinder.findMosaic(result.pyramid, wantedResolution, tolerance, wantedEnv,100); } catch (FactoryException ex) { monitor.exceptionOccured(ex, Level.WARNING); return null; } if(result.mosaic == null){ //no reliable mosaic return null; } //we definitly do not want some NaN values if(Double.isNaN(wantedEnv.getMinimum(0))){ wantedEnv.setRange(0, Double.NEGATIVE_INFINITY, wantedEnv.getMaximum(0)); } if(Double.isNaN(wantedEnv.getMaximum(0))){ wantedEnv.setRange(0, wantedEnv.getMinimum(0), Double.POSITIVE_INFINITY); } if(Double.isNaN(wantedEnv.getMinimum(1))){ wantedEnv.setRange(1, Double.NEGATIVE_INFINITY, wantedEnv.getMaximum(1)); } if(Double.isNaN(wantedEnv.getMaximum(1))){ wantedEnv.setRange(1, wantedEnv.getMinimum(1), Double.POSITIVE_INFINITY); } final DirectPosition ul = result.mosaic.getUpperLeftCorner(); final double tileMatrixMinX = ul.getOrdinate(0); final double tileMatrixMaxY = ul.getOrdinate(1); final Dimension gridSize = result.mosaic.getGridSize(); final Dimension tileSize = result.mosaic.getTileSize(); final double scale = result.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); result.tileMinCol = Math.floor( (bBoxMinX - tileMatrixMinX) / tileSpanX + epsilon); result.tileMaxCol = Math.floor( (bBoxMaxX - tileMatrixMinX) / tileSpanX - epsilon)+1; result.tileMinRow = Math.floor( (tileMatrixMaxY - bBoxMaxY) / tileSpanY - epsilon); result.tileMaxRow = Math.floor( (tileMatrixMaxY - bBoxMinY) / tileSpanY + epsilon)+1; //ensure we dont go out of the grid if(result.tileMinCol < 0) result.tileMinCol = 0; if(result.tileMaxCol > gridWidth) result.tileMaxCol = gridWidth; if(result.tileMinRow < 0) result.tileMinRow = 0; if(result.tileMaxRow > gridHeight) result.tileMaxRow = gridHeight; return result; } private ProjectedCoverage asCoverage(final RenderingContext2D context, StatelessContextParams params, PyramidalCoverageReference ref, String pyramidId, String mosaicId, int tileX, int tileY) { final GridCoverage2D coverage; try { coverage = AbstractPyramidalCoverageReference.getTileAsCoverage(ref,pyramidId,mosaicId,tileX,tileY); } catch (DataStoreException ex) { LOGGER.log(Level.WARNING, ex.getMessage(), ex); return null; } final CoverageMapLayer tilelayer = MapBuilder.createCoverageLayer(coverage, getUserObject().getStyle(), getUserObject().getName()); tilelayer.setElevationModel(getUserObject().getElevationModel()); return new ProjectedCoverage(params, tilelayer); } private ProjectedCoverage asCoverage(final RenderingContext2D context, StatelessContextParams params, PyramidalCoverageReference ref, String pyramidId, String mosaicId, TileReference tile) { final GridCoverage2D coverage; try { coverage = AbstractPyramidalCoverageReference.getTileAsCoverage(ref,pyramidId,mosaicId,tile); } catch (DataStoreException ex) { LOGGER.log(Level.WARNING, ex.getMessage(), ex); return null; } final CoverageMapLayer tilelayer = MapBuilder.createCoverageLayer(coverage, getUserObject().getStyle(), getUserObject().getName()); tilelayer.setElevationModel(getUserObject().getElevationModel()); return new ProjectedCoverage(params, tilelayer); } private void paintTile(final RenderingContext2D context, StatelessContextParams params, CachedRule[] rules, final String pyramidId, final String mosaicId, final TileReference tile) { final PyramidalCoverageReference covRef = (PyramidalCoverageReference) item.getCoverageReference(); final ProjectedCoverage projectedCoverage = asCoverage(context, params, covRef, pyramidId, mosaicId, tile); for(final CachedRule rule : rules){ for(final CachedSymbolizer symbol : rule.symbolizers()){ try { GO2Utilities.portray(projectedCoverage, symbol, context); } catch (PortrayalException ex) { context.getMonitor().exceptionOccured(ex, Level.WARNING); } } } } @Override public void structureChanged(CoverageStoreManagementEvent event) { } @Override public void contentChanged(CoverageStoreContentEvent event) { if(item.isVisible() && getCanvas().isAutoRepaint()){ //TODO should call a repaint only on this graphic getCanvas().repaint(); } } private static class TileSetResult{ //style informations private CachedRule[] rules; private CoordinateReferenceSystem pyramidCRS2D; //tiles informations private PyramidSet pyramidSet; private Pyramid pyramid; private GridMosaic mosaic; private double tileMinCol; private double tileMaxCol; private double tileMinRow; private double tileMaxRow; } }