/*
* Geotoolkit - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2008 - 2009, 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;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.LinearRing;
import com.vividsolutions.jts.geom.MultiLineString;
import com.vividsolutions.jts.geom.MultiPoint;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;
import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.awt.image.WritableRenderedImage;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.measure.UnitConverter;
import javax.measure.quantity.Length;
import javax.measure.Unit;
import org.apache.sis.feature.FeatureExt;
import org.apache.sis.geometry.GeneralEnvelope;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.logging.Logging;
import org.geotoolkit.coverage.grid.GridCoverage2D;
import org.geotoolkit.coverage.grid.ViewType;
import org.geotoolkit.coverage.io.CoverageStoreException;
import org.geotoolkit.coverage.io.GridCoverageReadParam;
import org.geotoolkit.coverage.io.GridCoverageReader;
import org.geotoolkit.coverage.processing.CoverageProcessingException;
import org.geotoolkit.coverage.processing.Operations;
import org.geotoolkit.display.VisitFilter;
import org.geotoolkit.display.canvas.control.CanvasMonitor;
import org.geotoolkit.display.PortrayalException;
import org.geotoolkit.display.shape.TransformedShape;
import org.geotoolkit.display2d.canvas.RenderingContext2D;
import org.geotoolkit.display2d.primitive.ProjectedCoverage;
import org.geotoolkit.display2d.primitive.ProjectedFeature;
import org.geotoolkit.display2d.primitive.SearchAreaJ2D;
import org.geotoolkit.display2d.primitive.iso.ISOGeometryJ2D;
import org.geotoolkit.display2d.primitive.jts.DecimateJTSGeometryJ2D;
import org.geotoolkit.display2d.primitive.jts.JTSGeometryJ2D;
import org.geotoolkit.display2d.style.CachedRule;
import org.geotoolkit.display2d.style.CachedSymbolizer;
import org.geotoolkit.display2d.style.renderer.SymbolizerRendererService;
import org.geotoolkit.factory.FactoryFinder;
import org.geotoolkit.factory.Hints;
import org.geotoolkit.filter.visitor.IsStaticExpressionVisitor;
import org.geotoolkit.filter.visitor.ListingPropertyVisitor;
import org.geotoolkit.geometry.isoonjts.spatialschema.geometry.JTSGeometry;
import org.geotoolkit.image.jai.FloodFill;
import org.geotoolkit.internal.referencing.CRSUtilities;
import org.geotoolkit.referencing.CRS;
import org.apache.sis.internal.referencing.j2d.AffineTransform2D;
import org.apache.sis.referencing.operation.transform.LinearTransform;
import org.geotoolkit.style.MutableStyleFactory;
import org.geotoolkit.style.StyleConstants;
import org.geotoolkit.style.visitor.PrepareStyleVisitor;
import static org.apache.sis.util.ArgumentChecks.*;
import org.apache.sis.util.NullArgumentException;
import org.apache.sis.util.collection.Cache;
import org.geotoolkit.utility.parameter.ParametersExt;
import org.geotoolkit.process.ProcessDescriptor;
import org.geotoolkit.process.ProcessException;
import org.geotoolkit.processing.coverage.resample.ResampleDescriptor;
import org.opengis.coverage.Coverage;
import org.opengis.coverage.grid.GridCoverage;
import org.opengis.util.GenericName;
import org.geotoolkit.math.XMath;
import org.geotoolkit.renderer.style.WKMMarkFactory;
import org.opengis.filter.FilterFactory2;
import org.opengis.filter.Id;
import org.opengis.filter.expression.Expression;
import org.opengis.filter.expression.PropertyName;
import org.opengis.geometry.Envelope;
import org.opengis.metadata.spatial.PixelOrientation;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.crs.GeographicCRS;
import org.opengis.referencing.cs.AxisDirection;
import org.opengis.referencing.cs.CoordinateSystem;
import org.opengis.referencing.cs.CoordinateSystemAxis;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.MathTransform2D;
import org.opengis.referencing.operation.TransformException;
import org.opengis.style.FeatureTypeStyle;
import org.opengis.style.Fill;
import org.opengis.style.Mark;
import org.opengis.style.RasterSymbolizer;
import org.opengis.style.Rule;
import org.opengis.style.SelectedChannelType;
import org.opengis.style.SemanticType;
import org.opengis.style.Stroke;
import org.opengis.style.Style;
import org.opengis.style.StyleVisitor;
import org.opengis.style.Symbolizer;
import org.apache.sis.geometry.Envelopes;
import org.apache.sis.internal.feature.AttributeConvention;
import org.apache.sis.util.Utilities;
import org.apache.sis.measure.Units;
import org.opengis.feature.AttributeType;
import org.opengis.feature.Feature;
import org.opengis.feature.FeatureType;
import org.opengis.feature.PropertyType;
/**
*
* @author Johann Sorel (Geomatys)
* @module
*/
public final class GO2Utilities {
public static final GeometryFactory JTS_FACTORY = new GeometryFactory();
private static final Cache<Symbolizer,CachedSymbolizer> CACHE = new Cache<Symbolizer, CachedSymbolizer>(50,50,true);
private static final Map<Class<? extends CachedSymbolizer>,SymbolizerRendererService> RENDERERS =
new HashMap<Class<? extends CachedSymbolizer>, SymbolizerRendererService>();
private static final double SE_EPSILON = 1e-6;
public static final MutableStyleFactory STYLE_FACTORY;
public static final FilterFactory2 FILTER_FACTORY;
public static final float SELECTION_LOWER_ALPHA = 0.09f;
public static final int SELECTION_PIXEL_MARGIN = 2;
public static final AlphaComposite ALPHA_COMPOSITE_0F = AlphaComposite.getInstance(AlphaComposite.CLEAR, 0.0f);
public static final AlphaComposite ALPHA_COMPOSITE_1F = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f);
//returned interior point when geometry in unvalid
private static final Coordinate INVALID_INTERIOR_POINT = new Coordinate(-0.5, -0.5, Double.NaN);
public static final Shape GLYPH_LINE;
public static final Shape GLYPH_POLYGON;
public static final Point2D GLYPH_POINT;
public static final Shape GLYPH_TEXT;
protected static final Logger LOGGER = Logging.getLogger("org.geotoolkit.display2d");
/**
* A tolerance value for black color. Used in {@linkplain #removeBlackBorder(java.awt.image.WritableRenderedImage)}
* to define an applet of black colors to replace with alpha data.
*/
private static final int COLOR_TOLERANCE = 13;
/**
* Palette of black colors samples computed with {@link #COLOR_TOLERANCE}.
* Used in {@linkplain #removeBlackBorder(java.awt.image.WritableRenderedImage)}.
*/
private static final double[][] BLACK_COLORS;
static{
List<double[]> blackColorsList = new ArrayList<>();
fillColorToleranceTable(0, 2, blackColorsList, new double[]{0, 0, 0, 255}, COLOR_TOLERANCE);
BLACK_COLORS = blackColorsList.toArray(new double[0][]);
final ServiceLoader<SymbolizerRendererService> loader = ServiceLoader.load(SymbolizerRendererService.class);
for(SymbolizerRendererService renderer : loader){
RENDERERS.put(renderer.getCachedSymbolizerClass(), renderer);
}
final Hints hints = new Hints();
hints.put(Hints.STYLE_FACTORY, MutableStyleFactory.class);
hints.put(Hints.FILTER_FACTORY, FilterFactory2.class);
STYLE_FACTORY = (MutableStyleFactory)FactoryFinder.getStyleFactory(hints);
FILTER_FACTORY = (FilterFactory2) FactoryFinder.getFilterFactory(hints);
//LINE -----------------------------------------------------------------
final float x2Points[] = {0, 0.4f, 0.6f, 1f};
final float y2Points[] = {0.2f, 0.6f, 0.4f, 0.8f};
final GeneralPath polyline = new GeneralPath(GeneralPath.WIND_EVEN_ODD, x2Points.length);
polyline.moveTo (x2Points[0], y2Points[0]);
for (int index = 1; index < x2Points.length; index++) {
polyline.lineTo(x2Points[index], y2Points[index]);
}
GLYPH_LINE = polyline;
//POLYGON --------------------------------------------------------------
final float x1Points[] = {0.2f, 0.4f, 1f, 1f, 0.2f};
final float y1Points[] = {1f, 0.4f, 0.2f, 1f, 1f};
final GeneralPath polygon = new GeneralPath(GeneralPath.WIND_EVEN_ODD, x1Points.length);
polygon.moveTo(x1Points[0], y1Points[0]);
for (int index = 1; index < x1Points.length; index++) {
polygon.lineTo(x1Points[index], y1Points[index]);
}
GLYPH_POLYGON = polygon;
//POINT ----------------------------------------------------------------
GLYPH_POINT = new Point2D.Float(0.5f,0.5f);
//TEXT -----------------------------------------------------------------
final float xtPoints[] = {0.1f, 0.3f, 0.2f, 0.2f};
final float ytPoints[] = {0.6f, 0.6f, 0.6f, 0.9f};
final GeneralPath textLine = new GeneralPath(GeneralPath.WIND_EVEN_ODD, xtPoints.length);
textLine.moveTo (xtPoints[0], ytPoints[0]);
for (int index = 1; index < xtPoints.length; index++) {
textLine.lineTo(xtPoints[index], ytPoints[index]);
}
GLYPH_TEXT = textLine;
}
private GO2Utilities() {}
public static void portray(final ProjectedFeature feature, final CachedSymbolizer symbol,
final RenderingContext2D context) throws PortrayalException{
final SymbolizerRendererService renderer = findRenderer(symbol);
if(renderer != null){
renderer.portray(feature, symbol, context);
}
}
public static void portray(final ProjectedCoverage graphic, final CachedSymbolizer symbol,
final RenderingContext2D context) throws PortrayalException {
final SymbolizerRendererService renderer = findRenderer(symbol);
if(renderer != null){
renderer.portray(graphic, symbol, context);
}
}
public static void portray(final RenderingContext2D renderingContext, GridCoverage2D dataCoverage) throws PortrayalException{
final CanvasMonitor monitor = renderingContext.getMonitor();
final Graphics2D g2d = renderingContext.getGraphics();
final CoordinateReferenceSystem coverageCRS = dataCoverage.getCoordinateReferenceSystem();
boolean sameCRS = true;
try{
final CoordinateReferenceSystem candidate2D = CRSUtilities.getCRS2D(coverageCRS);
if(!Utilities.equalsIgnoreMetadata(candidate2D,renderingContext.getObjectiveCRS2D()) ){
sameCRS = false;
dataCoverage = GO2Utilities.resample(dataCoverage.view(ViewType.NATIVE),renderingContext.getObjectiveCRS2D());
if(dataCoverage != null){
dataCoverage = dataCoverage.view(ViewType.RENDERED);
}
}
} catch (CoverageProcessingException ex) {
monitor.exceptionOccured(ex, Level.WARNING);
return;
} catch(Exception ex){
//several kind of errors can happen here, we catch anything to avoid blocking the map component.
monitor.exceptionOccured(
new IllegalStateException("Coverage is not in the requested CRS, found : " +
"\n"+ coverageCRS +
" was expecting : \n" +
renderingContext.getObjectiveCRS() +
"\nOriginal Cause:"+ ex.getMessage(), ex), Level.WARNING);
return;
}
if(dataCoverage == null){
monitor.exceptionOccured(new NullArgumentException("GO2Utilities : Reprojected coverage is null."),Level.WARNING);
return;
}
//we must switch to objectiveCRS for grid coverage
renderingContext.switchToObjectiveCRS();
RenderedImage img = dataCoverage.getRenderedImage();
if(!sameCRS){
//will be reprojected, we must check that image has alpha support
//otherwise we will have black borders after reprojection
if(!img.getColorModel().hasAlpha()){
//ensure we have a bufferedImage for floodfill operation
final BufferedImage buffer;
if(img instanceof BufferedImage){
buffer = (BufferedImage) img;
}else{
buffer = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_ARGB);
buffer.createGraphics().drawRenderedImage(img, new AffineTransform());
}
//remove black borders+
FloodFill.fill(buffer, new Color[]{Color.BLACK}, new Color(0f,0f,0f,0f),
new java.awt.Point(0,0),
new java.awt.Point(buffer.getWidth()-1,0),
new java.awt.Point(buffer.getWidth()-1,buffer.getHeight()-1),
new java.awt.Point(0,buffer.getHeight()-1)
);
img = buffer;
}
}
final MathTransform2D trs2D = dataCoverage.getGridGeometry().getGridToCRS2D(PixelOrientation.UPPER_LEFT);
if(trs2D instanceof AffineTransform){
g2d.setComposite(GO2Utilities.ALPHA_COMPOSITE_1F);
g2d.drawRenderedImage(img, (AffineTransform)trs2D);
}else if (trs2D instanceof LinearTransform) {
final LinearTransform lt = (LinearTransform) trs2D;
final int col = lt.getMatrix().getNumCol();
final int row = lt.getMatrix().getNumRow();
//TODO using only the first parameters of the linear transform
throw new PortrayalException("Could not render image, GridToCRS is a not an AffineTransform, found a " + trs2D.getClass());
}else{
throw new PortrayalException("Could not render image, GridToCRS is a not an AffineTransform, found a " + trs2D.getClass() );
}
}
public static boolean hit(final ProjectedFeature graphic, final CachedSymbolizer symbol,
final RenderingContext2D context, final SearchAreaJ2D mask, final VisitFilter filter){
final SymbolizerRendererService renderer = findRenderer(symbol);
if(renderer != null){
return renderer.hit(graphic, symbol, context, mask, filter);
}
return false;
}
public static boolean hit(final ProjectedCoverage graphic, final CachedSymbolizer symbol,
final RenderingContext2D renderingContext, final SearchAreaJ2D mask, final VisitFilter filter) {
final SymbolizerRendererService renderer = findRenderer(symbol);
if(renderer != null){
return renderer.hit(graphic, symbol, renderingContext, mask, filter);
}
return false;
}
////////////////////////////////////////////////////////////////////////////
// Glyph utils /////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
/**
* Paint a mark.
*
* @param mark : mark to paint
* @param size : expected mark size
* @param target : Graphics2D
*/
public static void renderGraphic(final Mark mark, final float size, final Graphics2D target){
final Expression wkn = mark.getWellKnownName();
final Shape shape;
if(StyleConstants.MARK_CIRCLE.equals(wkn)){
shape = WKMMarkFactory.CIRCLE;
}else if(StyleConstants.MARK_CROSS.equals(wkn)){
shape = WKMMarkFactory.CROSS;
}else if(StyleConstants.MARK_SQUARE.equals(wkn)){
shape = WKMMarkFactory.SQUARE;
}else if(StyleConstants.MARK_STAR.equals(wkn)){
shape = WKMMarkFactory.STAR;
}else if(StyleConstants.MARK_TRIANGLE.equals(wkn)){
shape = WKMMarkFactory.TRIANGLE;
}else if(StyleConstants.MARK_X.equals(wkn)){
shape = WKMMarkFactory.X;
}else{
shape = null;
}
if(shape != null){
final TransformedShape trs = new TransformedShape();
trs.setOriginalShape(shape);
trs.scale(size, size);
renderFill(trs, mark.getFill(), target);
renderStroke(trs, mark.getStroke(), Units.METRE, target);
}
}
/**
* Paint a stroked shape.
*
* @param shape : java2d shape
* @param stroke : sld stroke
* @param uom
* @param target : Graphics2D
*/
public static void renderStroke(final Shape shape, final Stroke stroke, final Unit uom, final Graphics2D target){
final Expression expColor = stroke.getColor();
final Expression expOpa = stroke.getOpacity();
final Expression expCap = stroke.getLineCap();
final Expression expJoin = stroke.getLineJoin();
final Expression expWidth = stroke.getWidth();
Paint color;
final float width;
final float opacity;
final int cap;
final int join;
final float[] dashes;
if(GO2Utilities.isStatic(expColor)){
color = expColor.evaluate(null, Color.class);
}else{
color = Color.RED;
}
if(color == null){
color = Color.RED;
}
if(expOpa != null && GO2Utilities.isStatic(expOpa)){
Number num = expOpa.evaluate(null, Number.class);
if(num != null){
opacity = XMath.clamp(num.floatValue(),0f,1f);
}else{
opacity = 0.6f;
}
}else{
opacity = 0.6f;
}
if(GO2Utilities.isStatic(expCap)){
if(StyleConstants.STROKE_CAP_ROUND.equals(expCap)){
cap = BasicStroke.CAP_ROUND;
}else if(StyleConstants.STROKE_CAP_SQUARE.equals(expCap)){
cap = BasicStroke.CAP_SQUARE;
}else {
cap = BasicStroke.CAP_BUTT;
}
}else{
cap = BasicStroke.CAP_BUTT;
}
if(GO2Utilities.isStatic(expJoin)){
if(StyleConstants.STROKE_JOIN_ROUND.equals(expJoin)){
join = BasicStroke.JOIN_ROUND;
}else if(StyleConstants.STROKE_JOIN_MITRE.equals(expJoin)){
join = BasicStroke.JOIN_MITER;
}else {
join = BasicStroke.JOIN_BEVEL;
}
}else{
join = BasicStroke.JOIN_BEVEL;
}
if(Units.POINT.equals(uom) && GO2Utilities.isStatic(expWidth)){
width = expWidth.evaluate(null, Number.class).floatValue();
if(stroke.getDashArray() != null && stroke.getDashArray().length >0){
dashes = stroke.getDashArray();
}else{
dashes = null;
}
}else{
width = 1f;
dashes = null;
}
target.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opacity));
target.setPaint(color);
if(dashes != null){
target.setStroke(new BasicStroke(width, cap, join,1,dashes,0));
}else{
target.setStroke(new BasicStroke(width, cap, join));
}
target.draw(shape);
}
/**
* Paint a filled shape.
*
* @param shape : java2d shape
* @param fill : sld fill
* @param target : Graphics2D
*/
public static void renderFill(final Shape shape, final Fill fill, final Graphics2D target){
if(fill == null){
return;
}
final Expression expColor = fill.getColor();
final Expression expOpa = fill.getOpacity();
Paint color;
final float opacity;
if(GO2Utilities.isStatic(expColor)){
color = expColor.evaluate(null, Color.class);
}else{
color = Color.RED;
}
if(color == null){
color = Color.RED;
}
if(GO2Utilities.isStatic(expOpa)){
opacity = XMath.clamp(expOpa.evaluate(null, Number.class).floatValue(),0f,1f);
}else{
opacity = 0.6f;
}
target.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opacity));
target.setPaint(color);
target.fill(shape);
}
////////////////////////////////////////////////////////////////////////////
// geometries operations ///////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
public static Shape toJava2D(final Geometry geom){
return new JTSGeometryJ2D(geom);
}
public static Shape toJava2D(final Geometry geom, final double[] resolution){
return new DecimateJTSGeometryJ2D(geom,resolution);
}
public static Shape toJava2D(final org.opengis.geometry.Geometry geom){
if(geom instanceof JTSGeometry){
final JTSGeometry geo = (JTSGeometry) geom;
return toJava2D(geo.getJTSGeometry());
}else{
return new ISOGeometryJ2D(geom);
}
}
public static Geometry toJTS(final Shape candidate){
final PathIterator ite = candidate.getPathIterator(null);
final List<Coordinate> coords = new ArrayList<Coordinate>();
final float[] xy = new float[2];
while(!ite.isDone()){
ite.currentSegment(xy);
coords.add(new Coordinate(xy[0], xy[1]));
ite.next();
}
coords.add(coords.get(0));
final LinearRing ring = JTS_FACTORY.createLinearRing(coords.toArray(new Coordinate[coords.size()]));
return JTS_FACTORY.createPolygon(ring, new LinearRing[0]);
}
public static boolean testHit(final VisitFilter filter, final Geometry left, final Geometry right){
switch(filter){
case INTERSECTS :
return left.intersects(right);
case WITHIN :
return left.contains(right);
}
return false;
}
public static boolean testHit(final VisitFilter filter, final org.opengis.geometry.Geometry left, final org.opengis.geometry.Geometry right){
switch(filter){
case INTERSECTS :
return left.intersects(right);
case WITHIN :
return left.contains(right);
}
return false;
}
////////////////////////////////////////////////////////////////////////////
// work on envelope ////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
/**
* Calculate the most accurate pixel resolution for the given envelope.
*
* @param context2D
* @param wanted
* @return double, in envelope crs unit by pixel
* @throws TransformException
*/
public static double pixelResolution(final RenderingContext2D context2D, final Envelope wanted) throws TransformException{
final Dimension dim = context2D.getCanvasDisplayBounds().getSize();
final double[] resolution = context2D.getResolution(context2D.getDisplayCRS());
//resolution contain dpi adjustments, to obtain an image of the correct dpi
//we raise the request dimension so that when we reduce it it will have the
//wanted dpi.
dim.width /= resolution[0];
dim.height /= resolution[1];
final MathTransform objToDisp = context2D.getObjectiveToDisplay();
Envelope cropped = wanted;
if(!CRS.equalsApproximatively(context2D.getCanvasObjectiveBounds2D(), wanted.getCoordinateReferenceSystem())){
cropped = Envelopes.transform(wanted, context2D.getObjectiveCRS2D());
}
cropped = Envelopes.transform(objToDisp, cropped);
//we assume we only have a regular
return wanted.getSpan(0) / cropped.getSpan(0);
}
////////////////////////////////////////////////////////////////////////////
// renderers cache /////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
public static SymbolizerRendererService findRenderer(final CachedSymbolizer symbol){
final Class<? extends CachedSymbolizer> type = symbol.getClass();
SymbolizerRendererService candidate = RENDERERS.get(type);
if (candidate != null) {
return candidate;
}
candidate = findRendererForCachedClass(type.getSuperclass());
if (candidate != null) {
return candidate;
}
return null;
}
private static SymbolizerRendererService findRendererForCachedClass(Class<?> type) {
while (type != null) {
SymbolizerRendererService candidate = RENDERERS.get(type);
if (candidate != null) {
// synchronized (RENDERERS) {
// RENDERERS.put(type, candidate);
// }
return candidate;
}
// Checks interfaces implemented by this class.
for (final Class<?> interf : type.getInterfaces()) {
candidate = findRendererForCachedClass(interf);
if (candidate != null) {
return candidate;
}
}
type = type.getSuperclass();
}
return null;
}
public static SymbolizerRendererService findRenderer(final Class<? extends Symbolizer> type){
for(SymbolizerRendererService renderer : RENDERERS.values()){
if(renderer.getSymbolizerClass().isAssignableFrom(type)){
return renderer;
}
}
return null;
}
/**
* @param candidate class
* @param references classes
* @return closesest reference class or null if none match the candidate class
*/
private static Class findClosestParent(final Class candidate, final Class ... references){
int closestIndice = Integer.MAX_VALUE;
Class<?> closest = null;
for(final Class<?> reference : references){
final int indice = findHierarchyLevel(candidate, reference);
if(indice != -1 && indice <= closestIndice){
closestIndice = indice;
closest = reference;
}
}
return closest;
}
/**
* @param candidate class
* @param reference class
* @return -1 if reference is not an interface or parent of the candidate class
* 0 if the parent class matches extacly the reference class
* >1 the class hierarchy level, the smaller is the number, the closer is
* the candidate class to the reference class
*/
private static int findHierarchyLevel(final Class candidate, final Class reference){
int level = 0;
Class c = candidate;
while(c != Object.class){
//check the class
if(c == reference){
return level;
}else{
level += 1000;
}
//check it's interfaces
for(final Class<?> i : c.getInterfaces()){
if(i == reference){
return level;
}else{
level += 1;
}
}
c = c.getSuperclass();
}
return -1;
}
private static Collection<Class<?>> findMostSpecialize(final Collection<Class<?>> classes) {
final Set<Class<?>> specialized = new HashSet<Class<?>>();
candidates :
for(final Class candidate : classes){
compare:
for(final Class compared : classes){
//continue if same class
if(compared == candidate) continue compare;
final Class result = findMostSpecialize(candidate, compared);
//candidate is not much specialized
if(result == compared) continue candidates;
}
specialized.add(candidate);
}
return specialized;
}
private static Class findMostSpecialize(final Class a, final Class b){
final boolean aisb = b.isAssignableFrom(a);
final boolean bisa = a.isAssignableFrom(b);
if(aisb && !bisa){
return a;
}else if(!aisb && bisa){
return b;
}else{
return null;
}
}
////////////////////////////////////////////////////////////////////////////
// rewrite coverage read param ////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
public static GridCoverage reCalculate(final GridCoverageReader reader, final GridCoverageReadParam params,
final RenderingContext2D context) throws CoverageStoreException, TransformException{
final CoordinateReferenceSystem sourceCRS = reader.getGridGeometry(0).getCoordinateReferenceSystem();
final CoordinateReferenceSystem targetCRS = params.getEnvelope().getCoordinateReferenceSystem();
if(!Utilities.equalsIgnoreMetadata(sourceCRS, targetCRS)){
//projection is not the same, must reproject it
final GridCoverageReadParam newParams = new GridCoverageReadParam();
final double[] newRes = context.getResolution(targetCRS);
final Envelope newEnv= Envelopes.transform(params.getEnvelope(), targetCRS);
newParams.setEnvelope(newEnv);
newParams.setResolution(newRes);
GridCoverage2D cov = (GridCoverage2D) reader.read(0, newParams);
cov = (GridCoverage2D) Operations.DEFAULT.resample(cov.view(ViewType.NATIVE), targetCRS);
cov = cov.view(ViewType.RENDERED);
return cov;
}
return reader.read(0, params);
}
public static GridCoverage2D resample(final Coverage dataCoverage, final CoordinateReferenceSystem targetCRS) throws ProcessException{
final ProcessDescriptor desc = ResampleDescriptor.INSTANCE;
final ParameterValueGroup params = desc.getInputDescriptor().createValue();
ParametersExt.getOrCreateValue(params, ResampleDescriptor.IN_COVERAGE.getName().getCode()).setValue(dataCoverage);
ParametersExt.getOrCreateValue(params, ResampleDescriptor.IN_COORDINATE_REFERENCE_SYSTEM.getName().getCode()).setValue(targetCRS);
final org.geotoolkit.process.Process process = desc.createProcess(params);
final ParameterValueGroup result = process.call();
return (GridCoverage2D) result.parameter("result").getValue();
}
////////////////////////////////////////////////////////////////////////////
// some scale utility methods //////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
/**
* Calculate the coefficient between the objective unit and the given one.
*/
public static float calculateScaleCoefficient(final RenderingContext2D context, final Unit<Length> symbolUnit){
final CoordinateReferenceSystem objectiveCRS = context.getObjectiveCRS();
ensureNonNull("symbol unit", symbolUnit);
ensureNonNull("objective crs", objectiveCRS);
//we have a special unit we must adjust the coefficient
final CoordinateSystem cs = objectiveCRS.getCoordinateSystem();
final int dimension = cs.getDimension();
final List<Double> converters = new ArrayList<Double>();
//go throw each dimension and append valid converters
for (int i=0; i<dimension; i++){
final CoordinateSystemAxis axis = cs.getAxis(i);
final Unit axisUnit = axis.getUnit();
if (axisUnit.isCompatible(symbolUnit)){
final UnitConverter converter = axisUnit.getConverterTo(symbolUnit);
if (!converter.isLinear()) {
throw new UnsupportedOperationException("Cannot convert nonlinear units yet");
}else{
converters.add(converter.convert(1) - converter.convert(0));
}
}else if(axisUnit == Units.DEGREE){
//calculate coefficient at center of the screen.
final Rectangle rect = context.getCanvasDisplayBounds();
final AffineTransform2D trs = context.getDisplayToObjective();
Point2D pt = new Point2D.Double(rect.getCenterX(), rect.getCenterY());
pt = trs.transform(pt,pt);
//TODO not correct yet, I'm not sure how to select the correct
//axis for calculation
if(!axis.getDirection().equals(AxisDirection.NORTH)) continue;
final GeographicCRS crs = (GeographicCRS) objectiveCRS;
final double a = crs.getDatum().getEllipsoid().getSemiMajorAxis();
final double b = crs.getDatum().getEllipsoid().getSemiMinorAxis();
final double e2 = 1 - Math.pow((b/a),2);
//TODO not sure of this neither
final double phi = Math.toRadians((i==0)? pt.getY() : pt.getX());
double s = a * (Math.cos(phi)) / Math.sqrt( 1 - e2 * Math.pow(Math.sin(phi),2) );
s = Math.toRadians(s);
final Unit ellipsoidUnit = crs.getDatum().getEllipsoid().getAxisUnit();
final UnitConverter converter = ellipsoidUnit.getConverterTo(symbolUnit);
s = converter.convert(s) - converter.convert(0);
converters.add(s);
}
}
final float coeff;
//calculate coefficient
if(converters.isEmpty()){
coeff = 1;
}else if(converters.size() == 1){
//only one valid converter
coeff = converters.get(0).floatValue();
}else{
double sum = 0;
for(final Double coef : converters){
sum += coef*coef ;
}
coeff = (float) Math.sqrt( sum/2d );
}
return 1/coeff;
}
/**
* Compute Euclidean distance between a point and a line define by 2 points (ptA, ptB).
*
* @param point
* @param ptA
* @param ptB
*/
public static double euclidianDistance(final double[] point, final double[] ptA, final double[] ptB) {
ArgumentChecks.ensureNonNull("point", point);
ArgumentChecks.ensureNonNull("dp1", ptA);
ArgumentChecks.ensureNonNull("dp2", ptB);
final int dimension = point.length;
if (ptA.length != dimension || ptB.length != dimension)
throw new IllegalArgumentException("All points should have same dimension.");
if (dimension == 2) {
final double u0 = ptB[0] - ptA[0];
final double u1 = ptB[1] - ptA[1];
return Math.abs((point[0]-ptA[0])*u1 - (point[1]-ptB[1])*u0)/ (u0*u0+u1*u1);
} else {
double dist = 0;
double normU = 0;
int prodCursorMin = 0;
int prodCursorMax = 1;
for (int i = 0; i < dimension; i++) {
if (++prodCursorMax == dimension) prodCursorMax = 0;
if (++prodCursorMin == dimension) prodCursorMin = 0;
final double ui = ptB[i] - ptA[i];
normU += ui * ui;
final double uCMax = ptB[prodCursorMax] - ptA[prodCursorMax];
final double uCMin = ptB[prodCursorMin] - ptA[prodCursorMin];
final double vMAMax = point[prodCursorMax] - ptA[prodCursorMax];
final double vMAMin = point[prodCursorMin] - ptA[prodCursorMin];
final double di = Math.abs(vMAMin*uCMax - vMAMax*uCMin);
dist += di*di;
}
return Math.sqrt(dist / normU);
}
}
////////////////////////////////////////////////////////////////////////////
// information about styles ////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
public static float[] validDashes(final float[] dashes) {
if (dashes == null || dashes.length == 0) {
return null;
} else {
return dashes;
}
}
public static <T> T evaluate(final Expression exp, final Object candidate, final Class<T> type, final T defaultValue ){
if(exp==null) return defaultValue;
T value;
try{
value = exp.evaluate(candidate, type);
if(value == null){
value = defaultValue;
}
}catch(IllegalArgumentException ex){
//if functions or candidate do not have the proper field we will have a IllegalArgumentException
value = defaultValue;
}
return value;
}
public static Float evaluate(final Expression exp, final Object candidate,
final float defaultValue, final float min, final float max){
if(exp==null) return defaultValue;
Float value;
try{
value = exp.evaluate(candidate, Float.class);
if(value == null){
value = defaultValue;
}else{
//ensure min/max
value = XMath.clamp(value, min, max);
}
}catch(IllegalArgumentException ex){
//if functions or candidate do not have the proper field we will have a IllegalArgumentException
value = defaultValue;
}
return value;
}
public static Geometry getGeometry(final Object obj, final Expression geomExp){
return geomExp.evaluate(obj, Geometry.class);
}
public static Class getGeometryClass(final FeatureType featuretype, final String geomName){
final PropertyType prop;
if (geomName != null && !geomName.trim().isEmpty()) {
prop = featuretype.getProperty(geomName);
}else if(featuretype != null){
prop = featuretype.getProperty(AttributeConvention.GEOMETRY_PROPERTY.toString());
}else{
prop = null;
}
if(prop instanceof AttributeType){
return ((AttributeType)prop).getValueClass();
}else{
return Geometry.class;
}
}
public static Geometry getGeometry(final Feature feature, final Expression geomExp){
if(isNullorEmpty(geomExp)){
final Object att = feature.getPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString());
return (Geometry) att;
}else{
return geomExp.evaluate(feature, Geometry.class);
}
}
public static Collection<String> getRequieredAttributsName(final Expression exp, final Collection<String> collection){
return (Collection<String>) exp.accept(ListingPropertyVisitor.VISITOR, collection);
}
public static boolean isStatic(final Expression exp){
if(exp == null) return true;
return (Boolean) exp.accept(IsStaticExpressionVisitor.VISITOR, null);
}
/**
* Test if an expression is :
* - null
* - Expression.NIL
* - PropertyName with null or empty name
*
* @param exp
* @return true if empty
*/
public static boolean isNullorEmpty(Expression exp){
if(exp==null || exp==Expression.NIL){
return true;
}else if(exp instanceof PropertyName){
final PropertyName pn = (PropertyName) exp;
final String str = pn.getPropertyName();
if(str==null || str.trim().isEmpty()){
return true;
}
}
return false;
}
/**
* Returns the symbolizers that apply on the given feature.
*/
public static List<CachedSymbolizer> getSymbolizer(final Feature feature, final Style style) {
final List<CachedSymbolizer> symbols = new ArrayList<CachedSymbolizer>();
final FeatureType ftype = feature.getType();
final String typeName = ftype.getName().toString();
final Collection<? extends FeatureTypeStyle> ftss = style.featureTypeStyles();
for (FeatureTypeStyle fts : ftss) {
//store "else" rules
boolean doElse = true;
final List<Rule> elseRules = new ArrayList<Rule>();
//test if the featutetype is valid
if (true) {
// if (typeName == null || (typeName.equalsIgnoreCase(fts.getFeatureTypeName())) ) {
final Collection<? extends Rule> rules = fts.rules();
for (final Rule rule : rules) {
//test if the rule is valid and is not a "else" rule
if (!rule.isElseFilter() && (rule.getFilter() == null || rule.getFilter().evaluate(feature))) {
doElse = false;
//append all the symbolizers
final Collection<? extends Symbolizer> syms = rule.symbolizers();
for (Symbolizer sym : syms) {
symbols.add(getCached(sym,ftype));
}
} else {
elseRules.add(rule);
}
}
}
//explore else rules if necessary
if (doElse) {
for (final Rule rule : elseRules) {
//append all the symbolizers
final Collection<? extends Symbolizer> syms = rule.symbolizers();
for (final Symbolizer sym : syms) {
symbols.add(getCached(sym,ftype));
}
}
}
}
return symbols;
}
public static Set<String> propertiesNames(final Collection<? extends Rule> rules){
org.geotoolkit.style.visitor.ListingPropertyVisitor visitor = new org.geotoolkit.style.visitor.ListingPropertyVisitor();
final Set<String> names = new HashSet<>();
for(Rule r : rules){
visitor.visit(r, names);
}
return names;
}
public static Set<String> propertiesCachedNames(final Collection<CachedRule> rules){
final Set<String> atts = new HashSet<>();
for(final CachedRule r : rules){
r.getRequieredAttributsName(atts);
}
return atts;
}
public static Set<String> propertiesCachedNames(final CachedRule[] rules){
final Set<String> atts = new HashSet<>();
for(final CachedRule r : rules){
r.getRequieredAttributsName(atts);
}
return atts;
}
/**
* This information can be used to determinate if geometries smaller then a pixel
* can be ignore when rendering.
*
* @return true if there is a visible margin used in this symbols.
*/
public static boolean visibleMargin(final CachedRule[] rules, final float minMargin, final RenderingContext2D context){
for(CachedRule r : rules){
for(CachedSymbolizer s : r.symbolizers()){
final float m = s.getMargin(null, context);
if(Float.isNaN(m) || m>=minMargin){
//margin can not be evaluate or is bigger
return true;
}
}
}
return false;
}
public static List<Rule> getValidRules(final Style style, final double scale, final FeatureType type) {
final List<Rule> validRules = new ArrayList<Rule>();
final List<? extends FeatureTypeStyle> ftss = style.featureTypeStyles();
for(final FeatureTypeStyle fts : ftss){
final Id ids = fts.getFeatureInstanceIDs();
final Set<GenericName> names = fts.featureTypeNames();
//check semantic, only if we have a feature type
if(type != null){
final Collection<SemanticType> semantics = fts.semanticTypeIdentifiers();
if(!semantics.isEmpty()){
final AttributeType<?> gtype = FeatureExt.getDefaultGeometryAttribute(type);
final Class ctype;
if(gtype == null){
ctype = null;
}else{
ctype = gtype.getValueClass();
}
boolean valid = false;
for(SemanticType semantic : semantics){
if(semantic == SemanticType.ANY){
valid = true;
break;
}else if(semantic == SemanticType.LINE){
if(ctype == LineString.class || ctype == MultiLineString.class || ctype == Geometry.class ){
valid = true;
break;
}
}else if(semantic == SemanticType.POINT){
if(ctype == Point.class || ctype == MultiPoint.class || ctype == Geometry.class){
valid = true;
break;
}
}else if(semantic == SemanticType.POLYGON){
if(ctype == Polygon.class || ctype == MultiPolygon.class || ctype == Geometry.class){
valid = true;
break;
}
}else if(semantic == SemanticType.RASTER){
// can not test this on feature datas
}else if(semantic == SemanticType.TEXT){
//no text type in JTS, that's a stupid thing this Text semantic
}
}
if(!valid) continue;
}
}
//TODO filter correctly possibilities
//test if the featutetype is valid
//we move to next feature type if not valid
if (false) continue;
//if (typeName != null && !(typeName.equalsIgnoreCase(fts.getFeatureTypeName())) ) continue;
final List<? extends Rule> rules = fts.rules();
for(final Rule rule : rules){
//test if the scale is valid for this rule
if(rule.getMinScaleDenominator()-SE_EPSILON <= scale && rule.getMaxScaleDenominator()+SE_EPSILON > scale){
validRules.add(rule);
}
}
}
return validRules;
}
public static CachedRule[] getValidCachedRules(final Style style, final double scale, final FeatureType type) {
final List<CachedRule> validRules = new ArrayList<CachedRule>();
final List<? extends FeatureTypeStyle> ftss = style.featureTypeStyles();
for(final FeatureTypeStyle fts : ftss){
final Id ids = fts.getFeatureInstanceIDs();
final Set<GenericName> names = fts.featureTypeNames();
//check semantic, only if we have a feature type
if(type != null){
final Collection<SemanticType> semantics = fts.semanticTypeIdentifiers();
if(!semantics.isEmpty()){
final AttributeType<?> gtype = FeatureExt.getDefaultGeometryAttribute(type);
final Class ctype = gtype.getValueClass();
boolean valid = false;
for(SemanticType semantic : semantics){
if(semantic == SemanticType.ANY){
valid = true;
break;
}else if(semantic == SemanticType.LINE){
if(ctype == LineString.class || ctype == MultiLineString.class){
valid = true;
break;
}
}else if(semantic == SemanticType.POINT){
if(ctype == Point.class || ctype == MultiPoint.class){
valid = true;
break;
}
}else if(semantic == SemanticType.POLYGON){
if(ctype == Polygon.class || ctype == MultiPolygon.class){
valid = true;
break;
}
}else if(semantic == SemanticType.RASTER){
// can not test this on feature datas
}else if(semantic == SemanticType.TEXT){
//no text type in JTS, that's a stupid thing this Text semantic
}
}
if(!valid) continue;
}
}
//TODO filter correctly possibilities
//test if the featutetype is valid
//we move to next feature type if not valid
//if (false) continue;
//if (typeName != null && !(typeName.equalsIgnoreCase(fts.getFeatureTypeName())) ) continue;
final List<? extends Rule> rules = fts.rules();
for(final Rule rule : rules){
//test if the scale is valid for this rule
if(rule.getMinScaleDenominator()-SE_EPSILON <= scale && rule.getMaxScaleDenominator()+SE_EPSILON > scale){
validRules.add(getCached(rule,type));
}
}
}
return validRules.toArray(new CachedRule[validRules.size()]);
}
public static CachedRule[] getValidCachedRules(final Style style, final double scale, final GenericName type, final FeatureType expected) {
final List<CachedRule> validRules = new ArrayList<>();
final List<? extends FeatureTypeStyle> ftss = style.featureTypeStyles();
for(final FeatureTypeStyle fts : ftss){
final Id ids = fts.getFeatureInstanceIDs();
final Set<GenericName> names = fts.featureTypeNames();
final Collection<SemanticType> semantics = fts.semanticTypeIdentifiers();
//TODO filter correctly possibilities
//test if the featutetype is valid
//we move to next feature type if not valid
if (false) continue;
//if (typeName != null && !(typeName.equalsIgnoreCase(fts.getFeatureTypeName())) ) continue;
final List<? extends Rule> rules = fts.rules();
for(final Rule rule : rules){
//test if the scale is valid for this rule
if(rule.getMinScaleDenominator()-SE_EPSILON <= scale && rule.getMaxScaleDenominator()+SE_EPSILON > scale){
validRules.add(getCached(rule,expected));
}
}
}
return validRules.toArray(new CachedRule[validRules.size()]);
}
/**
* Return true if this raster symbolizer define the original data style.
* That means no rendering operations need to be applied on the coverage
* before painting.
*/
public static boolean isDefaultRasterSymbolizer(final Symbolizer symbolizer){
if(!(symbolizer instanceof RasterSymbolizer)){
return false;
}
final RasterSymbolizer rs = (RasterSymbolizer)symbolizer;
if(rs.getShadedRelief() != null &&
rs.getShadedRelief().getReliefFactor().evaluate(null, Float.class) != 0){
return false;
}
if(rs.getOpacity() != null && rs.getOpacity().evaluate(null, Float.class) != 1f){
return false;
}
if(rs.getImageOutline() != null){
return false;
}
if(rs.getContrastEnhancement() != null &&
rs.getContrastEnhancement().getGammaValue().evaluate(null, Float.class) != 1f){
return false;
}
if(rs.getColorMap() != null && rs.getColorMap().getFunction() != null){
return false;
}
final SelectedChannelType[] bands = (rs.getChannelSelection()==null) ? null
: rs.getChannelSelection().getRGBChannels();
if(bands != null){
if(bands.length != 3){
return false;
}
//todo should check each band
}
return true;
}
/**
* Try to get the most representative point of a a geometry.
* This is used by PointSymbolizer and TextSymbiolizer.
*
* @param geom, not null
* @return
*/
public static Point getBestPoint(Geometry geom){
Point pt = null;
// 1 : try to get an interior point
//NOTE : this sometimes fails with TopololyException or IllegalArgumentException
try{
pt = geom.getInteriorPoint();
if(pt.isValid() && !pt.getCoordinate().equals2D(INVALID_INTERIOR_POINT)) return pt;
}catch(Throwable ex){
//JTS error sometimes happen
}
// 2 : fallback on centroid
//NOTE : even for valid geometries, the centroid happened to be NaN
try{
pt = geom.getCentroid();
if(pt.isValid()) return pt;
}catch(Throwable ex){
//JTS error sometimes happen
}
// 3 : extract from envelope
final com.vividsolutions.jts.geom.Envelope env = geom.getEnvelopeInternal();
pt = JTS_FACTORY.createPoint(new Coordinate(
(env.getMaxX()+env.getMinX())/2.0,
(env.getMaxY()+env.getMinY())/2.0));
return pt.isValid() ? pt : null;
}
////////////////////////////////////////////////////////////////////////////
// SYMBOLIZER CACHES ///////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
public static CachedRule getCached(final Rule rule,final FeatureType expected){
return new CachedRule(rule,expected);
}
public static CachedSymbolizer getCached(Symbolizer symbol,final FeatureType expected){
CachedSymbolizer value;
if(expected != null){
//optimize the symbolizer before caching it
final StyleVisitor sv = new PrepareStyleVisitor(Feature.class, expected);
symbol = (Symbolizer)symbol.accept(sv, null);
}
final SymbolizerRendererService renderer = findRenderer(symbol.getClass());
if(renderer != null){
value = renderer.createCachedSymbolizer(symbol);
} else {
throw new IllegalStateException("No renderer for the style "+ symbol);
}
return value;
// CachedSymbolizer value = CACHE.peek(symbol);
// if (value == null) {
// Cache.Handler<CachedSymbolizer> handler = CACHE.lock(symbol);
// try {
// value = handler.peek();
// if (value == null) {
// final SymbolizerRendererService renderer = findRenderer(symbol.getClass());
// if(renderer != null){
// value = renderer.createCachedSymbolizer(symbol,expected);
// } else {
// throw new IllegalStateException("No renderer for the style "+ symbol);
// }
// }
// } finally {
// handler.putAndUnlock(value);
// }
// }
// return value;
}
public static void clearCache(){
CACHE.clear();
}
////////////////////////////////////////////////////////////////////////////
// OTHER UTILS /////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
/**
* Merge colors, the first color is placed at the back.
* The first color is expected to be opaque.
*
* @return Opaque color resulting from the merge
* @throws IllegalArgumentException if first color is not opaque
*/
public static Color mergeColors(final Color c1, final Color c2) throws IllegalArgumentException{
if(c1.getAlpha() != 255){
throw new IllegalArgumentException("First color must be opaque");
}
final float alpha = (float)c2.getAlpha()/255f;
final int r = Math.min(c1.getRed(), c2.getRed()) + (int) (Math.abs(c1.getRed()-c2.getRed())*alpha);
final int g = Math.min(c1.getGreen(), c2.getGreen()) + (int) (Math.abs(c1.getGreen()-c2.getGreen())*alpha);
final int b = Math.min(c1.getBlue(), c2.getBlue()) + (int) (Math.abs(c1.getBlue()-c2.getBlue())*alpha);
return new Color(r, g, b);
}
public static void removeNaN(GeneralEnvelope env){
//we definitly do not want some NaN values
if(Double.isNaN(env.getMinimum(0))){ env.setRange(0, Double.NEGATIVE_INFINITY, env.getMaximum(0)); }
if(Double.isNaN(env.getMaximum(0))){ env.setRange(0, env.getMinimum(0), Double.POSITIVE_INFINITY); }
if(Double.isNaN(env.getMinimum(1))){ env.setRange(1, Double.NEGATIVE_INFINITY, env.getMaximum(1)); }
if(Double.isNaN(env.getMaximum(1))){ env.setRange(1, env.getMinimum(1), Double.POSITIVE_INFINITY); }
}
//-- Some utility methods for any Renderer.
/**
* Remove black border of an ARGB image to replace them with transparent pixels.
*
* @param toFilter Image to remove black border from.
*/
public static void removeBlackBorder(final WritableRenderedImage toFilter) {
// remove black border only on image larger than 1x1 pixels
if (toFilter.getHeight() > 1 && toFilter.getWidth() > 1) {
FloodFill.fill(toFilter, BLACK_COLORS, new double[]{0d, 0d, 0d, 0d},
new java.awt.Point(0, 0),
new java.awt.Point(toFilter.getWidth() - 1, 0),
new java.awt.Point(toFilter.getWidth() - 1, toFilter.getHeight() - 1),
new java.awt.Point(0, toFilter.getHeight() - 1)
);
} else {
LOGGER.log(Level.FINER, "Ignoring black border removal, because image is too small (image < 1x1)");
}
}
/**
* Add an alpha band to the image and remove any black border if asked.
*
* TODO, this could be done more efficiently by adding an ImageLayout hints
* when doing the coverage reprojection. but hints can not be passed currently.
*/
public static RenderedImage forceAlpha(RenderedImage img) {
if (!img.getColorModel().hasAlpha()) {
//Add alpha channel
final BufferedImage buffer = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_ARGB);
buffer.createGraphics().drawRenderedImage(img, new AffineTransform());
img = buffer;
}
return img;
}
private static void fillColorToleranceTable(int index, int maxIndex, List<double[]> container, double[] baseColor, int tolerance) {
for (int j = 0 ; j < tolerance; j++) {
final double[] color = new double[baseColor.length];
System.arraycopy(baseColor, 0, color, 0, baseColor.length);
color[index] += j;
if (index >= maxIndex) {
container.add(color);
} else {
for (int i = index +1 ; i <= maxIndex ; i++) {
fillColorToleranceTable(i, maxIndex, container, color, tolerance);
}
}
}
}
}