/* * Geotoolkit - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2008 - 2010, 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.style.renderer; import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.LinearGradientPaint; import java.awt.MultipleGradientPaint; import java.awt.Rectangle; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.text.DecimalFormat; import java.text.NumberFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.sis.math.DecimalFunctions; import org.geotoolkit.display2d.canvas.RenderingContext2D; import org.geotoolkit.display2d.style.CachedRasterSymbolizer; import org.geotoolkit.map.MapLayer; import org.geotoolkit.style.StyleConstants; import org.geotoolkit.style.function.Categorize; import org.geotoolkit.style.function.Interpolate; import org.geotoolkit.style.function.InterpolationPoint; import org.geotoolkit.style.function.Jenks; import org.apache.sis.util.ObjectConverters; import org.apache.sis.measure.NumberRange; import org.apache.sis.measure.Range; import org.opengis.filter.expression.Expression; import org.opengis.filter.expression.Function; import org.opengis.style.ColorMap; import org.opengis.style.RasterSymbolizer; import org.apache.sis.util.UnconvertibleObjectException; import org.apache.sis.util.logging.Logging; /** * @author Johann Sorel (Geomatys) * @module */ public class DefaultRasterSymbolizerRendererService extends AbstractSymbolizerRendererService<RasterSymbolizer, CachedRasterSymbolizer>{ private static final Logger LOGGER = Logging.getLogger("org.geotoolkit.display2d.style.renderer"); private static final int LEGEND_PALETTE_WIDTH = 30; private static final Font LEGEND_FONT = new Font(Font.SERIF, Font.BOLD, 12); @Override public boolean isGroupSymbolizer() { return false; } /** * {@inheritDoc } */ @Override public Class<RasterSymbolizer> getSymbolizerClass() { return RasterSymbolizer.class; } /** * {@inheritDoc } */ @Override public Class<CachedRasterSymbolizer> getCachedSymbolizerClass() { return CachedRasterSymbolizer.class; } /** * {@inheritDoc } */ @Override public CachedRasterSymbolizer createCachedSymbolizer(final RasterSymbolizer symbol) { return new CachedRasterSymbolizer(symbol,this); } /** * {@inheritDoc } */ @Override public SymbolizerRenderer createRenderer(final CachedRasterSymbolizer symbol, final RenderingContext2D context) { return new DefaultRasterSymbolizerRenderer(this, symbol, context); } @Override public Rectangle2D glyphPreferredSize(CachedRasterSymbolizer symbol, MapLayer layer) { final Map<Object, Color> colorMap = getMapColor(symbol); if (colorMap.isEmpty()) { return super.glyphPreferredSize(symbol, layer); } else { final int mapLength = colorMap.size(); int maxX = LEGEND_PALETTE_WIDTH; final BufferedImage img = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB); final FontMetrics fm = img.createGraphics().getFontMetrics(LEGEND_FONT); Object[] keys = colorMap.keySet().toArray(new Object[colorMap.size()]); for (int i = 0; i < keys.length; i++) { final Object current = keys[i]; final Object next = (i<keys.length - 1) ? keys[i+1] : null; final StringBuilder text = getLineText(current, next, null); int lineWidth = LEGEND_PALETTE_WIDTH + fm.stringWidth(text.toString()); maxX = Math.max(maxX, lineWidth); } int maxY = mapLength * fm.getHeight(); return new Rectangle2D.Double(0, 0, maxX+5, maxY); } } /** * {@inheritDoc } */ @Override public void glyph(final Graphics2D g, final Rectangle2D rectangle, final CachedRasterSymbolizer symbol, final MapLayer layer) { float[] fractions; Color[] colors; final ColorMap cm = symbol.getSource().getColorMap(); //paint default Glyph if (cm == null || cm.getFunction() == null || ( !(cm.getFunction() instanceof Interpolate) && !(cm.getFunction() instanceof Jenks) && !(cm.getFunction() instanceof Categorize) )) { fractions = new float[] {0.0f, 0.5f, 1.0f}; colors = new Color[] {Color.RED, Color.GREEN, Color.BLUE}; final MultipleGradientPaint.CycleMethod cycleMethod = MultipleGradientPaint.CycleMethod.NO_CYCLE; final LinearGradientPaint paint = new LinearGradientPaint( new Point2D.Double(rectangle.getMinX(),rectangle.getMinY()), new Point2D.Double(rectangle.getMaxX(),rectangle.getMinY()), fractions, colors, cycleMethod ); g.setPaint(paint); g.fill(rectangle); return; } //paint Interpolation, Categorize and Jenks Glyphs final Map<Object, Color> colorMap = getMapColor(symbol); if (!colorMap.isEmpty()) { boolean doInterpolation = true; if (colorMap.keySet().iterator().next() instanceof Range) { doInterpolation = false; } //find an appropriate number format without too much digits for this value range NumberFormat numFormat = null; if(doInterpolation){ double min = Double.POSITIVE_INFINITY; double max = Double.NEGATIVE_INFINITY; for(Object o : colorMap.keySet()){ if(o instanceof String){ try{ o = Double.valueOf(o.toString().trim()); }catch(NumberFormatException ex){ continue; } } if(o instanceof Number){ min = Math.min( ((Number)o).doubleValue(), min); max = Math.max( ((Number)o).doubleValue(), max); } } if(!Double.isInfinite(max)){ final double step = (max-min) / (colorMap.size()*10); final int nbDigit = DecimalFunctions.fractionDigitsForDelta(step, false); numFormat = NumberFormat.getNumberInstance(); numFormat.setMaximumFractionDigits(nbDigit); } } final int colorMapSize = colorMap.size(); int fillHeight = Double.valueOf(rectangle.getHeight()).intValue(); int intervalHeight = fillHeight / colorMapSize; Rectangle2D paintRectangle = new Rectangle((int) rectangle.getX(), (int) rectangle.getY(), LEGEND_PALETTE_WIDTH, fillHeight); g.setClip(rectangle); if (doInterpolation) { //fill color array colors = colorMap.values().toArray(new Color[colorMapSize]); //fill fraction array final float interval = 0.9f / colorMapSize; float fraction = 0.1f; fractions = new float[colorMapSize]; for (int i = 0; i < colorMapSize; i++) { fractions[i] = fraction; fraction += interval; } //paint nothing if(colors.length == 0){ return; } //ensure we have at least 2 colors if(colors.length == 1){ colors = new Color[]{colors[0],colors[0]}; fractions = new float[]{fractions[0], 1.0f}; } //create gradient final LinearGradientPaint paint = new LinearGradientPaint( new Point2D.Double(paintRectangle.getMinX(),rectangle.getMinY()), new Point2D.Double(paintRectangle.getMinX(),rectangle.getMaxY()), fractions, colors, MultipleGradientPaint.CycleMethod.NO_CYCLE); g.setPaint(paint); g.fill(paintRectangle); } else { //paint all colors rectangles Collection<Color> colorsList = colorMap.values(); int intX = Double.valueOf(rectangle.getMinX()).intValue(); int intY = Double.valueOf(rectangle.getMinY()).intValue(); for (Color color : colorsList) { final Rectangle2D colorRect = new Rectangle(intX, intY, LEGEND_PALETTE_WIDTH, intervalHeight); g.setPaint(color); g.fill(colorRect); intY += intervalHeight; } } //paint text float Y = Double.valueOf(rectangle.getMinY()).floatValue(); float shift = doInterpolation ? 0.6f : 0.7f; g.setColor(Color.BLACK); Object[] keys = colorMap.keySet().toArray(new Object[colorMap.size()]); for (int i = 0; i < keys.length; i++) { final Object current = keys[i]; final Object next = (i<keys.length - 1) ? keys[i+1] : null; final StringBuilder text = getLineText(current, next, numFormat); g.drawString(text.toString(), LEGEND_PALETTE_WIDTH + 1f , Y + intervalHeight * shift ); Y += intervalHeight; } } } private static StringBuilder getLineText(Object currentElem, Object nextElement, NumberFormat numFormat) { final StringBuilder text = new StringBuilder(" < "); if (currentElem instanceof NumberRange) { double min = ((NumberRange) currentElem).getMaxDouble(); double max = Double.POSITIVE_INFINITY; if (nextElement instanceof NumberRange) { max = ((NumberRange) nextElement).getMinDouble(); } text.append('['); text.append(String.format("%.3f", min)); text.append(" ... "); text.append(String.format("%.3f", max)); text.append(']'); } else if (numFormat != null) { if(currentElem instanceof String){ try{ currentElem = Double.valueOf(currentElem.toString().trim()); text.append(numFormat.format(currentElem)); }catch(NumberFormatException ex){ text.append(currentElem); } }else if(currentElem instanceof Number){ text.append(numFormat.format(currentElem)); } } else { text.append(currentElem); } return text; } /** * Create a map of object and colors from symbolizer colormap functions like * Interpolate, Jenks and Categorize. * * @param symbol CachedRaserSymbolizer * @return a Map containing Object like Range or String for key and Color as value. */ private Map<Object, Color> getMapColor(final CachedRasterSymbolizer symbol) { Map<Object, Color> colorMap = new LinkedHashMap<>(); final ColorMap cm = symbol.getSource().getColorMap(); if (cm != null && cm.getFunction() != null ) { final Function fct = cm.getFunction(); if (fct instanceof Interpolate) { final Interpolate interpolate = (Interpolate) fct; final List<InterpolationPoint> points = interpolate.getInterpolationPoints(); final int size = points.size(); for(int i=0;i<size;i++){ final InterpolationPoint pt = points.get(i); Color color = pt.getValue().evaluate(null, Color.class); if(color == null) try { color = ObjectConverters.convert(pt.getValue().toString(), Color.class); } catch (UnconvertibleObjectException e) { Logging.recoverableException(LOGGER, DefaultRasterSymbolizerRendererService.class, "getMapColor", e); // TODO - do we really want to ignore? } colorMap.put(pt.getData().toString(), color); } } else if(fct instanceof Jenks) { final Jenks jenks = (Jenks) fct; final Map<Double, Color> jenksColorMap = jenks.getColorMap(); final Map<Color, List<Double>> rangeJenksMap = new HashMap<>(); for (Map.Entry<Double, Color> elem : jenksColorMap.entrySet()) { if (rangeJenksMap.containsKey(elem.getValue())) { final List<Double> values = rangeJenksMap.get(elem.getValue()); values.add(elem.getKey()); Collections.sort(values); rangeJenksMap.put(elem.getValue(), values); } else { final List<Double> values = new ArrayList<Double>(); values.add(elem.getKey()); rangeJenksMap.put(elem.getValue(), values); } } //create range sorted map. colorMap = new TreeMap(new RangeComparator()); for (Map.Entry<Color, List<Double>> elem : rangeJenksMap.entrySet()) { final List<Double> values = elem.getValue(); Collections.sort(values); colorMap.put(new NumberRange<>(Double.class, values.get(0), true, values.get(values.size()-1), true), elem.getKey()); } } else if(fct instanceof Categorize) { final Categorize categorize = (Categorize) fct; final Map<Expression, Expression> thresholds = categorize.getThresholds(); final Map<Color, List<Double>> colorValuesMap = new HashMap<>(); for (Map.Entry<Expression, Expression> entry : thresholds.entrySet()) { final Color currentColor = entry.getValue().evaluate(null, Color.class); Double currentValue = Double.NEGATIVE_INFINITY; try { Double value = entry.getKey().evaluate(null, Double.class); if (value != null) { currentValue = value; } } catch (Exception e) { if (StyleConstants.CATEGORIZE_LESS_INFINITY.equals(entry.getKey())) { currentValue = Double.NEGATIVE_INFINITY; } else { // Cannot read value, it's not a number, neither a "categorize less infinity". LOGGER.log(Level.INFO, "A color map value cannot be evaluated. it will be ignored.\nCause : "+ e.getLocalizedMessage()); currentValue = null; } } if (currentColor != null && currentValue != null) { if (colorValuesMap.containsKey(currentColor)) { final LinkedList<Double> values = (LinkedList<Double>)colorValuesMap.get(currentColor); values.add(currentValue); colorValuesMap.put(currentColor, values); } else { final LinkedList<Double> values = new LinkedList<Double>(); values.add(currentValue); colorValuesMap.put(currentColor, values); } } } //create range sorted map. colorMap = new TreeMap(new RangeComparator()); for (Map.Entry<Color, List<Double>> elem : colorValuesMap.entrySet()) { final List<Double> values = elem.getValue(); Collections.sort(values); colorMap.put(new NumberRange<>(Double.class, values.get(0), true, values.get(values.size()-1), true), elem.getKey()); } } } return colorMap; } /** * Range comparator. */ private class RangeComparator implements Comparator<Range> { @Override public int compare(Range o1, Range o2) { return o1.getMaxValue().compareTo(o2.getMinValue()); } } }