/*
* Geotoolkit - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2013-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.display2d.ext.cellular;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.Polygon;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.image.RenderedImage;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import org.apache.sis.geometry.GeneralEnvelope;
import org.apache.sis.math.Statistics;
import org.geotoolkit.coverage.grid.GridCoverage2D;
import org.geotoolkit.display.PortrayalException;
import org.geotoolkit.display2d.canvas.RenderingContext2D;
import org.geotoolkit.display2d.container.stateless.DefaultCachedRule;
import org.geotoolkit.display2d.container.stateless.StatelessContextParams;
import org.geotoolkit.display2d.primitive.ProjectedCoverage;
import org.geotoolkit.display2d.primitive.ProjectedFeature;
import org.geotoolkit.display2d.primitive.ProjectedGeometry;
import org.geotoolkit.display2d.primitive.ProjectedObject;
import org.geotoolkit.display2d.style.CachedRule;
import org.geotoolkit.display2d.style.renderer.AbstractCoverageSymbolizerRenderer;
import org.geotoolkit.display2d.style.renderer.SymbolizerRenderer;
import org.geotoolkit.display2d.style.renderer.SymbolizerRendererService;
import org.geotoolkit.filter.identity.DefaultFeatureId;
import org.geotoolkit.geometry.jts.JTS;
import org.geotoolkit.referencing.operation.matrix.XAffineTransform;
import org.apache.sis.internal.referencing.j2d.AffineTransform2D;
import org.apache.sis.util.ObjectConverters;
import org.opengis.filter.Filter;
import org.opengis.geometry.Envelope;
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 org.apache.sis.util.UnconvertibleObjectException;
import org.apache.sis.util.logging.Logging;
import org.geotoolkit.coverage.grid.ViewType;
import org.geotoolkit.image.iterator.PixelIterator;
import org.geotoolkit.image.iterator.PixelIteratorFactory;
import org.geotoolkit.internal.referencing.CRSUtilities;
import org.apache.sis.geometry.Envelopes;
import org.apache.sis.internal.feature.AttributeConvention;
import org.opengis.feature.AttributeType;
import org.opengis.feature.Feature;
import org.opengis.feature.FeatureType;
import org.opengis.feature.PropertyType;
/**
* TODO : For features, compute statistics only if input symbolizer needs
* it, and compute them only on required fields.
*
* @author Johann Sorel (Geomatys)
*/
public class CellSymbolizerRenderer extends AbstractCoverageSymbolizerRenderer<CachedCellSymbolizer>{
private static final GeometryFactory GF = new GeometryFactory();
public CellSymbolizerRenderer(SymbolizerRendererService service,
CachedCellSymbolizer symbol, RenderingContext2D context) {
super(service, symbol, context);
}
/**
* This symbolizer works with groups when it's features.
*
* @param graphics
* @throws PortrayalException
*/
@Override
public void portray(Iterator<? extends ProjectedObject> graphics) throws PortrayalException {
if(symbol.getCachedRule() == null){
return;
}
//calculate the cells
final int cellSize = symbol.getSource().getCellSize();
final AffineTransform trs = renderingContext.getDisplayToObjective();
final double objCellSize = XAffineTransform.getScale(trs) * cellSize;
//find min and max cols/rows
final Envelope env = renderingContext.getCanvasObjectiveBounds2D();
final CoordinateReferenceSystem crs = env.getCoordinateReferenceSystem();
final int minCol = (int)(env.getMinimum(0) / objCellSize);
final int maxCol = (int)((env.getMaximum(0) / objCellSize)+0.5);
final int minRow = (int)(env.getMinimum(1) / objCellSize);
final int maxRow = (int)((env.getMaximum(1) / objCellSize)+0.5);
final int nbRow = maxRow - minRow;
final int nbCol = maxCol - minCol;
//create all cell contours
final Polygon[][] contours = new Polygon[nbRow][nbCol];
for(int r=0; r<nbRow; r++){
for(int c=0; c<nbCol; c++){
final double minx = (minCol+c) * objCellSize;
final double maxx = minx + objCellSize;
final double miny = (minRow+r) * objCellSize;
final double maxy = miny + objCellSize;
contours[r][c] = GF.createPolygon(new Coordinate[]{
new Coordinate(minx, miny),
new Coordinate(minx, maxy),
new Coordinate(maxx, maxy),
new Coordinate(maxx, miny),
new Coordinate(minx, miny),
});
JTS.setCRS(contours[r][c],crs);
}
}
FeatureType baseType = null;
FeatureType cellType = null;
String[] numericProperties = null;
Statistics[][][] stats = null;
try{
while(graphics.hasNext()){
final ProjectedObject obj = graphics.next();
final ProjectedFeature projFeature = (ProjectedFeature) obj;
if(baseType==null){
//we expect all features to have the same type
baseType = projFeature.getCandidate().getType();
cellType = CellSymbolizer.buildCellType(baseType,crs);
final List<String> props = new ArrayList<>();
for(PropertyType desc : baseType.getProperties(true)){
if(desc instanceof AttributeType){
final AttributeType att = (AttributeType) desc;
final Class binding = att.getValueClass();
if(Number.class.isAssignableFrom(binding) || String.class.isAssignableFrom(binding)){
props.add(att.getName().toString());
}
}
}
numericProperties = props.toArray(new String[props.size()]);
stats = new Statistics[numericProperties.length][nbRow][nbCol];
for(int i=0;i<numericProperties.length;i++){
for(int j=0;j<nbRow;j++){
for(int k=0;k<nbCol;k++){
stats[i][j][k] = new Statistics("");
}
}
}
}
final ProjectedGeometry pg = projFeature.getGeometry(geomPropertyName);
final Geometry[] geoms = pg.getObjectiveGeometryJTS();
//find in which cell it intersects
int row=-1;
int col=-1;
loop:
for(Geometry g : geoms){
if(g==null) continue;
for(int r=0;r<nbRow;r++){
for(int c=0;c<nbCol;c++){
if (contours[r][c].intersects(g)){
row = r;
col = c;
break loop;
}
}
}
}
//fill stats
if(row!=-1){
final Feature feature = projFeature.getCandidate();
for(int i=0;i<numericProperties.length;i++){
final Object value = feature.getProperty(numericProperties[i]).getValue();
try {
final Number num = ObjectConverters.convert(value, Number.class);
if (num != null) {
stats[i][row][col].accept(num.doubleValue());
}
} catch (UnconvertibleObjectException e) {
Logging.recoverableException(LOGGER, CellSymbolizerRenderer.class, "portray", e);
// TODO - do we really want to ignore?
}
}
}
}
}catch(TransformException ex){
throw new PortrayalException(ex);
}
if(numericProperties==null){
//nothing in the iterator
return;
}
//render the cell features
final Object[] values = new Object[2+7*numericProperties.length];
final Feature feature = cellType.newInstance();
feature.setPropertyValue(AttributeConvention.IDENTIFIER_PROPERTY.toString(), "cell-n");
final StatelessContextParams params = new StatelessContextParams(renderingContext.getCanvas(), null);
params.update(renderingContext);
final ProjectedFeature pf = new ProjectedFeature(params,feature);
final DefaultCachedRule renderers = new DefaultCachedRule(new CachedRule[]{symbol.getCachedRule()},renderingContext);
//expand the search area by the maximum symbol size
float symbolsMargin = renderers.getMargin(null, renderingContext);
if(symbolsMargin==0) symbolsMargin = 300f;
if(symbolsMargin>0 && params.objectiveJTSEnvelope!=null){
params.objectiveJTSEnvelope = new com.vividsolutions.jts.geom.Envelope(params.objectiveJTSEnvelope);
params.objectiveJTSEnvelope.expandBy(symbolsMargin);
}
for(int r=0;r<nbRow;r++){
for(int c=0;c<nbCol;c++){
pf.setCandidate(feature);
values[0] = contours[r][c].getCentroid();
JTS.setCRS( ((Geometry)values[0]), crs);
values[1] = contours[r][c];
int k=1;
for(int b=0,n=numericProperties.length;b<n;b++){
values[++k] = stats[b][r][c].count();
values[++k] = stats[b][r][c].minimum();
values[++k] = stats[b][r][c].mean();
values[++k] = stats[b][r][c].maximum();
values[++k] = stats[b][r][c].span();
values[++k] = stats[b][r][c].rms();
values[++k] = stats[b][r][c].sum();
}
renderCellFeature(feature, pf, renderers);
pf.setCandidate(null);
}
}
}
@Override
public void portray(final ProjectedCoverage projectedCoverage) throws PortrayalException {
if(symbol.getCachedRule() == null){
return;
}
//adjust envelope, we need cells to start at crs 0,0 to avoid artifacts
//when building tiles
final int cellSize = symbol.getSource().getCellSize();
final AffineTransform2D displayToObjective = renderingContext.getDisplayToObjective();
double objCellSize = XAffineTransform.getScale(displayToObjective) * cellSize;
final GeneralEnvelope env = new GeneralEnvelope(renderingContext.getCanvasObjectiveBounds());
final int hidx = CRSUtilities.firstHorizontalAxis(env.getCoordinateReferenceSystem());
//round under and above to match cell size
env.setRange(hidx, objCellSize * Math.floor(env.getMinimum(hidx)/objCellSize), objCellSize * Math.ceil(env.getMaximum(hidx)/objCellSize));
env.setRange(hidx+1, objCellSize * Math.floor(env.getMinimum(hidx+1)/objCellSize), objCellSize * Math.ceil(env.getMaximum(hidx+1)/objCellSize));
GridCoverage2D coverage;
try {
coverage = getObjectiveCoverage(projectedCoverage,env,renderingContext.getResolution(),
renderingContext.getObjectiveToDisplay(),false);
} catch (Exception ex) {
throw new PortrayalException(ex);
}
if(coverage!=null){
coverage = coverage.view(ViewType.GEOPHYSICS);
}
if(coverage == null){
LOGGER.log(Level.WARNING, "Reprojected coverage is null.");
return;
}
//create all cell features
final GeneralEnvelope area = new GeneralEnvelope(coverage.getEnvelope2D());
//round under and above to match cell size
area.setRange(hidx, objCellSize * Math.floor(area.getMinimum(hidx)/objCellSize), objCellSize * Math.ceil(area.getMaximum(hidx)/objCellSize));
area.setRange(hidx+1, objCellSize * Math.floor(area.getMinimum(hidx+1)/objCellSize), objCellSize * Math.ceil(area.getMaximum(hidx+1)/objCellSize));
final int nbx = (int) Math.ceil(area.getSpan(0) / objCellSize);
final int nby = (int) Math.ceil(area.getSpan(1) / objCellSize);
final RenderedImage image = coverage.getRenderedImage();
final int nbBand = image.getSampleModel().getNumBands();
final Statistics[][][] stats = new Statistics[nbBand][nby][nbx];
MathTransform2D gridToCRS = coverage.getGridGeometry().getGridToCRS2D();
final PixelIterator ite = PixelIteratorFactory.createDefaultIterator(image);
int i,x,y;
final double[] gridCoord = new double[gridToCRS.getSourceDimensions()];
final double[] crsCoord = new double[gridToCRS.getTargetDimensions()];
try{
while(ite.next()){
gridCoord[0] = ite.getX();
gridCoord[1] = ite.getY();
gridToCRS.transform(gridCoord, 0, crsCoord, 0, 1);
crsCoord[0] = (crsCoord[0]-area.getMinimum(0))/objCellSize;
crsCoord[1] = (crsCoord[1]-area.getMinimum(1))/objCellSize;
x = (int) crsCoord[0];
y = (int) crsCoord[1];
for(i=0;i<nbBand;i++){
if(stats[i][y][x]==null) stats[i][y][x] = new Statistics("");
stats[i][y][x].accept(ite.getSampleDouble());
if(i<nbBand-1) ite.next();
}
}
}catch(TransformException ex){
throw new PortrayalException(ex);
}
//prepare the cell feature type
final FeatureType cellType = CellSymbolizer.buildCellType(coverage);
final Feature feature = cellType.newInstance();
final StatelessContextParams params = new StatelessContextParams(renderingContext.getCanvas(), null);
params.update(renderingContext);
params.objectiveJTSEnvelope = new com.vividsolutions.jts.geom.Envelope(
env.getMinimum(0), env.getMaximum(0),
env.getMinimum(1), env.getMaximum(1));
params.displayClipRect = null;
params.displayClip = null;
final ProjectedFeature pf = new ProjectedFeature(params,feature);
final DefaultCachedRule renderers = new DefaultCachedRule(new CachedRule[]{symbol.getCachedRule()},renderingContext);
//force image interpolation here
Object oldValue = g2d.getRenderingHint(RenderingHints.KEY_INTERPOLATION);
if(oldValue == null) oldValue = RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR;
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION,RenderingHints.VALUE_INTERPOLATION_BICUBIC);
renderingContext.getRenderingHints().put(RenderingHints.KEY_INTERPOLATION,RenderingHints.VALUE_INTERPOLATION_BICUBIC);
for(y=0;y<nby;y++){
for(x=0;x<nbx;x++){
if(stats[0][y][x]==null){
for(i=0;i<nbBand;i++)
stats[i][y][x] = new Statistics("");
}
pf.clearDataCache();
double cx = area.getMinimum(0) + (0.5+x)*objCellSize;
double cy = area.getMinimum(1) + (0.5+y)*objCellSize;
feature.setPropertyValue(CellSymbolizer.PROPERY_GEOM_CENTER, GF.createPoint(new Coordinate(cx,cy)));
int k=0;
for(int b=0,n=nbBand;b<n;b++){
feature.setPropertyValue("band_"+b+CellSymbolizer.PROPERY_SUFFIX_COUNT,(double)stats[b][y][x].count());
feature.setPropertyValue("band_"+b+CellSymbolizer.PROPERY_SUFFIX_MIN,stats[b][y][x].minimum());
feature.setPropertyValue("band_"+b+CellSymbolizer.PROPERY_SUFFIX_MEAN,stats[b][y][x].mean());
feature.setPropertyValue("band_"+b+CellSymbolizer.PROPERY_SUFFIX_MAX,stats[b][y][x].maximum());
feature.setPropertyValue("band_"+b+CellSymbolizer.PROPERY_SUFFIX_RANGE,stats[b][y][x].span());
feature.setPropertyValue("band_"+b+CellSymbolizer.PROPERY_SUFFIX_RMS,stats[b][y][x].rms());
feature.setPropertyValue("band_"+b+CellSymbolizer.PROPERY_SUFFIX_SUM,stats[b][y][x].sum());
}
renderCellFeature(feature, pf, renderers);
}
}
//restore image interpolation
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION,oldValue);
renderingContext.getRenderingHints().put(RenderingHints.KEY_INTERPOLATION,oldValue);
}
private void renderCellFeature(Feature feature, final ProjectedFeature pf, DefaultCachedRule renderers) throws PortrayalException{
boolean painted = false;
for(int i=0; i<renderers.elseRuleIndex; i++){
final CachedRule rule = renderers.rules[i];
final Filter ruleFilter = rule.getFilter();
//test if the rule is valid for this feature
if (ruleFilter == null || ruleFilter.evaluate(feature)) {
painted = true;
for (final SymbolizerRenderer renderer : renderers.renderers[i]) {
renderer.portray(pf);
}
}
}
//the feature hasn't been painted, paint it with the 'else' rules
if(!painted){
for(int i=renderers.elseRuleIndex; i<renderers.rules.length; i++){
final CachedRule rule = renderers.rules[i];
final Filter ruleFilter = rule.getFilter();
//test if the rule is valid for this feature
if (ruleFilter == null || ruleFilter.evaluate(feature)) {
for (final SymbolizerRenderer renderer : renderers.renderers[i]) {
renderer.portray(pf);
}
}
}
}
}
private AffineTransform calculateAverageAffine(final RenderingContext2D context,
final GridCoverage2D coverage) throws FactoryException, TransformException{
final MathTransform trs = context.getMathTransform(context.getObjectiveCRS(),coverage.getCoordinateReferenceSystem2D() );
final Envelope refEnv = context.getCanvasObjectiveBounds();
final GeneralEnvelope coverageEnv = Envelopes.transform(trs, refEnv);
final double objX = refEnv.getSpan(0);
final double objY = refEnv.getSpan(1);
final double covX = coverageEnv.getMaximum(0)-coverageEnv.getMinimum(0);
final double covY = coverageEnv.getMaximum(1)-coverageEnv.getMinimum(1);
final double scaleX = covX/objX;
final double scaleY = covY/objY;
AffineTransform aff = new AffineTransform();
aff.setToScale(scaleX,scaleY);
return aff;
}
/**
* Evaluate the lenght of the given vector.
*
* @param delta : vector to evaluate
* @return integer : lenght of the vector rounded at the above integer
*/
private static int length(final Point2D delta) {
return Math.max(1, (int) Math.ceil(Math.hypot(delta.getX(), delta.getY())));
}
/**
* {@inheritDoc }
* <br>
* Note : do nothing only return coverageSource.
* In attempt to particulary comportement if exist.
*/
@Override
protected GridCoverage2D prepareCoverageToResampling(GridCoverage2D coverageSource, CachedCellSymbolizer symbolizer) {
return coverageSource;
}
}