Package org.vfny.geoserver.wms.responses

Source Code of org.vfny.geoserver.wms.responses.LegendUtils

package org.vfny.geoserver.wms.responses;

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.RenderingHints.Key;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.IndexColorModel;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.geotools.coverage.grid.GridCoverage2D;
import org.geotools.coverage.grid.io.AbstractGridCoverage2DReader;
import org.geotools.renderer.i18n.ErrorKeys;
import org.geotools.renderer.i18n.Errors;
import org.geotools.renderer.lite.StreamingRenderer;
import org.geotools.styling.ColorMapEntry;
import org.geotools.styling.FeatureTypeStyle;
import org.geotools.styling.RasterSymbolizer;
import org.geotools.styling.Rule;
import org.geotools.styling.Style;
import org.geotools.styling.Symbolizer;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.FeatureType;
import org.opengis.feature.type.PropertyDescriptor;
import org.opengis.feature.type.PropertyType;
import org.opengis.filter.expression.Expression;
import org.opengis.style.ChannelSelection;
import org.vfny.geoserver.wms.requests.GetLegendGraphicRequest;

/**
* Utility class for building legends, it exposes many methods that could be reused anywhere.
*
* <p>
* I am not preventin people from subclassing this method so that they could add their own utility methods.
* @author Simone Giannecchini, GeoSolutions SAS
*
*/
@SuppressWarnings({"deprecation","unchecked"})
public class LegendUtils {
  /**
   * Ensures that the provided argument is not <code>null</code>.
   * <p>
   * If it <code>null</code> it must throw a {@link NullPointerException}.
   *
   * @param argument argument to check for <code>null</code>.
   */
  protected static void ensureNotNull(final Object argument){
    ensureNotNull(argument,"Argument cannot be null");
  }
 
  /**
   * Ensures that the provided argument is not <code>null</code>.
   * <p>
   * If it <code>null</code> it must throw a {@link NullPointerException}.
   *
   * @param argument argument to check for <code>null</code>.
   * @param message  leading message to print out in case the test fails.
   */
  protected static void ensureNotNull(final Object argument,final String message){
    if(message==null)
      throw new NullPointerException("Message cannot be null");
    if(argument==null)
      throw new NullPointerException(message+" cannot be null");
  }
 
  public enum VAlign{
    TOP,MIDDLE,BOTTOM;   
  }
  public enum HAlign{
    LEFT,CENTERED,RIGHT,JUSTIFIED;   
  }
 
  /**Default {@link Font} name for legends.*/
  public final static String DEFAULT_FONT_NAME="Sans-Serif";
 
  /**Default channel name for {@link ChannelSelection} elements.*/
  public final static String DEFAULT_CHANNEL_NAME="1";
 
  /**Default {@link Font} for legends.*/
  public final static int DEFAULT_FONT_TYPE= Font.PLAIN;
 
 
  /**Default {@link Font} for legends.*/
  public final static int DEFAULT_FONT_SIZE= 12;
 
  /**Default {@link Font} for legends.*/
  public final static Font DEFAULT_FONT= new Font("Sans-Serif",Font.PLAIN,12);
 
  /**
   * Default Legend graphics background color
   */
  public static final Color DEFAULT_BG_COLOR = Color.WHITE;
  /**
   * Default label color
   */
  public static final Color DEFAULT_FONT_COLOR = Color.BLACK;
 
  /** padding percentage factor at both sides of the legend. */
  public static final float hpaddingFactor = 0.15f;
  /** top & bottom padding percentage factor for the legend */
  public static final float vpaddingFactor = 0.15f;
 
  /** padding percentage factor at both sides of the legend. */
  public static final float rowPaddingFactor = 0.15f;
  /** top & bottom padding percentage factor for the legend */
  public static final float columnPaddingFactor = 0.15f;
 
  /** padding percentage factor at both sides of the legend. */
  public static final float marginFactor = 0.015f;
 
 
  /**
   * shared package's logger
   */
  private static final Logger LOGGER = org.geotools.util.logging.Logging
      .getLogger(LegendUtils.class.getPackage().getName());

  public static final Color DEFAULT_BORDER_COLOR = Color.black;

  /**
   * Retrieves the font from the provided {@link GetLegendGraphicRequest}.
   *
   * @param req a {@link GetLegendGraphicRequest} from which we should extract the {@link Font} information.
   * @return the {@link Font} specified in the provided {@link GetLegendGraphicRequest} or a default {@link Font}.
   *
   */
  public static Font getLabelFont(final GetLegendGraphicRequest req) {
    ensureNotNull(req, "GetLegendGraphicRequestre");
    final Map legendOptions = req.getLegendOptions();
    if(legendOptions==null)
      return DEFAULT_FONT;
    String legendFontName=LegendUtils.DEFAULT_FONT_NAME;
    if (legendOptions.get("fontName") != null) {
      legendFontName = (String) legendOptions.get("fontName");
    }
   
    int legendFontFamily=LegendUtils.DEFAULT_FONT_TYPE;
    if (legendOptions.get("fontStyle") != null) {
      String legendFontFamily_ = (String) legendOptions.get("fontStyle");
      if (legendFontFamily_.equalsIgnoreCase("italic")) {
        legendFontFamily= Font.ITALIC;
      } else if (legendFontFamily_.equalsIgnoreCase("bold")) {
        legendFontFamily= Font.BOLD;
      }
    }
   
    int legendFontSize=LegendUtils.DEFAULT_FONT_SIZE  ;
    if (legendOptions.get("fontSize") != null) {
      try {
        legendFontSize = Integer.valueOf((String) legendOptions
            .get("fontSize"));
      } catch (NumberFormatException e) {
        LOGGER
            .warning("Error trying to interpret legendOption 'fontSize': "+ legendOptions.get("fontSize"));
        legendFontSize=LegendUtils.DEFAULT_FONT_SIZE;
      }
    }

    if(legendFontFamily==LegendUtils.DEFAULT_FONT_TYPE&& legendFontName.equalsIgnoreCase(LegendUtils.DEFAULT_FONT_NAME)&&
        (legendFontSize==LegendUtils.DEFAULT_FONT_SIZE||legendFontSize<=0))
      return DEFAULT_FONT;
   
    return new Font(legendFontName, legendFontFamily, legendFontSize);
   
  }

  /**
   * Extracts the Label {@link Font} {@link Color} from the provided {@link GetLegendGraphicRequest}.
   *
   * <p>
   * If there is no label {@link Font} specified a default {@link Font} {@link Color} will be provided.
   *
   * @param req the {@link GetLegendGraphicRequest} from which to extract label color information.
   * @return  the Label {@link Font} {@link Color} extracted from the provided {@link GetLegendGraphicRequest} or
   *  a default {@link Font} {@link Color}.
   */
  public static Color getLabelFontColor(final GetLegendGraphicRequest req) {
    ensureNotNull(req, "GetLegendGraphicRequestre");
    final Map legendOptions = req.getLegendOptions();
    final String color = legendOptions!=null?(String) legendOptions.get("fontColor"):null;
    if (color == null) {
      // return the default
      return DEFAULT_FONT_COLOR;
    }

    try {
      return color(color);
    } catch (NumberFormatException e) {
      if (LOGGER.isLoggable(Level.WARNING))
        LOGGER.warning("Could not decode label color: " + color
            + ", default to " + DEFAULT_FONT_COLOR.toString());
      return DEFAULT_FONT_COLOR;
    }
  }

  /**
   * Returns the image background color for the given
   * {@link GetLegendGraphicRequest}.
   *
   * @param req a {@link GetLegendGraphicRequest} from which we should extract the background color.
   * @return the Color for the hexadecimal value passed as the
   *         <code>BGCOLOR</code>
   *         {@link GetLegendGraphicRequest#getLegendOptions() legend option},
   *         or the default background color if no bgcolor were passed.
   */
  public static Color getBackgroundColor(final GetLegendGraphicRequest req) {
    ensureNotNull(req, "GetLegendGraphicRequestre");
    final Map legendOptions = req.getLegendOptions();
    if(legendOptions==null)
      return DEFAULT_BG_COLOR;
    final String color = (String) legendOptions.get("bgColor");
    if (color == null) {
      // return the default
      return DEFAULT_BG_COLOR;
    }

    try {
      return color(color);
    } catch (NumberFormatException e) {
      LOGGER.warning("Could not decode background color: " + color
          + ", default to " + DEFAULT_BG_COLOR.toString());
      return DEFAULT_BG_COLOR;
    }

  }

  /**
   * Extracts the opacity from the provided {@link ColorMapEntry}.
   *
   * <p>
   * 1.0 is returned in case the provided {@link ColorMapEntry} is null or invalid.
   *
   * @param entry
   * @return the opacity from the provided {@link ColorMapEntry} or 1.0 if something bad happens.
   *
   * @throws IllegalArgumentException
   * @throws MissingResourceException
   */
  public static double getOpacity(final ColorMapEntry entry)
      throws IllegalArgumentException, MissingResourceException {
   
    ensureNotNull(entry, "ColorMapEntry");
    // //
    //
    // As stated in <a
    // href="https://portal.opengeospatial.org/files/?artifact_id=1188">
    // OGC Styled-Layer Descriptor Report (OGC 02-070) version
    // 1.0.0.</a>:
    // "Not all systems can support opacity in colormaps. The default
    // opacity is 1.0 (fully opaque)."
    //
    // //
    final Expression opacity = entry.getOpacity();
    Double opacityValue = null;
    if (opacity != null)
      opacityValue = opacity.evaluate(null, Double.class);
    else
      return 1.0;
    if ((opacityValue.doubleValue() - 1) > 0
        || opacityValue.doubleValue() < 0) {
      throw new IllegalArgumentException(Errors.format(
          ErrorKeys.ILLEGAL_ARGUMENT_$2, "Opacity", opacityValue));
    }
    return opacityValue.doubleValue();
  }

  /**
   * Tries to decode the provided {@link String} into an HEX color definition.
   *
   * <p>
   * In case the {@link String} is not correct a {@link NumberFormatException} will be thrown.
   *
   * @param hex a {@link String} that should contain an Hexadecimal color representation.
   * @return a {@link Color} representing the provided {@link String}.
   * @throws NumberFormatException in case the string is badly formatted.
   */
  public static Color color(final String hex) {
    ensureNotNull(hex,"hex value");
    if (!hex.startsWith("#")) {
      final String hex_ = "#" + hex;
      return Color.decode(hex_);

     
    }
    return Color.decode(hex);
  }
 
  /**
   * Get the {@link Color} out of this {@link ColorMapEntry}.
   * @param entry the {@link ColorMapEntry} from which to extract the {@link Color} component.
   *
   * @return the {@link Color} out of this {@link ColorMapEntry}.
   * @throws NumberFormatException in case the color string is badly formatted.
   */
  public static Color color(final ColorMapEntry entry){
    ensureNotNull(entry, "entry");
    final Expression color = entry.getColor();
    ensureNotNull(color, "color");
    final String  colorString= color.evaluate(null, String.class);
    ensureNotNull(colorString, "colorString");
    return color(colorString);
  }
 
  /**
   * Extracts the quantity part from the provided {@link ColorMapEntry}.
   *
   * @param entry the provided {@link ColorMapEntry} from which we should extract the quantity part.
   * @return   the quantity part for the provided {@link ColorMapEntry}.
   */
  public static double getQuantity(final ColorMapEntry entry) {
    ensureNotNull(entry, "entry");
    Expression quantity = entry.getQuantity();
    ensureNotNull(quantity, "quantity");
    Double quantityString = quantity.evaluate(null, Double.class);
    ensureNotNull(quantityString, "quantityString");
    return quantityString.doubleValue();
  }
 
  /**
   * Finds the applicable Rules for the given scale denominator.
   *
   * @param ftStyles
   * @param scaleDenominator
   *
   * @return an array of {@link Rule}s.
   */
  public static Rule[] getApplicableRules(final FeatureTypeStyle[] ftStyles, double scaleDenominator) {
    ensureNotNull(ftStyles, "FeatureTypeStyle array ");
      /**
       * Holds both the rules that apply and the ElseRule's if any, in the
       * order they appear
       */
      final List<Rule> ruleList = new ArrayList<Rule>();
 
      // get applicable rules at the current scale
      for (int i = 0; i < ftStyles.length; i++) {
          FeatureTypeStyle fts = ftStyles[i];
          Rule[] rules = fts.getRules();
 
          for (int j = 0; j < rules.length; j++) {
              Rule r = rules[j];
 
              if (isWithInScale(r, scaleDenominator)) {
                  ruleList.add(r);
 
                  /*
                   * I'm commented this out since I guess it has no sense
                   * for producing the legend, since whether or not the rule
                   * has an else filter, the legend is drawn only if the
                   * scale denominator lies inside the rule's scale range.
                            if (r.hasElseFilter()) {
                                ruleList.add(r);
                            }
                   */
              }
          }
      }
 
      return ruleList.toArray(new Rule[ruleList.size()]);
  }

  /**
   * Checks if a rule can be triggered at the current scale level
   *
   * @param r The rule
   * @param scaleDenominator the scale denominator to check if it is between
   *        the rule's scale range. -1 means that it allways is.
   *
   * @return true if the scale is compatible with the rule settings
   */
  public static boolean isWithInScale(final Rule r,final  double scaleDenominator) {
      return (scaleDenominator == -1)
      || (((r.getMinScaleDenominator() - DefaultRasterLegendProducer.TOLERANCE) <= scaleDenominator)
      && ((r.getMaxScaleDenominator() + DefaultRasterLegendProducer.TOLERANCE) > scaleDenominator));
  }

  /**
   * Return a {@link BufferedImage} representing this label.
   * The characters '\n' '\r' and '\f' are interpreted as linebreaks,
   * as is the characater combination "\n" (as opposed to the actual '\n' character).
   * This allows people to force line breaks in their labels by
   * including the character "\" followed by "n" in their
   * label.
   *
   * @param label - the label to render
   * @param g - the Graphics2D that will be used to render this label
   * @return a {@link BufferedImage} of the properly rendered label.
   */
  public static BufferedImage renderLabel(final String label,final  Graphics2D g,final  GetLegendGraphicRequest req) {
    ensureNotNull(label);
    ensureNotNull(g);
    ensureNotNull(req);
      // We'll accept '/n' as a text string
      //to indicate a line break, as well as a traditional 'real' line-break in the XML.
      BufferedImage renderedLabel;
      Color labelColor = getLabelFontColor(req);
      if ((label.indexOf("\n") != -1) || (label.indexOf("\\n") != -1)) {
          //this is a label WITH line-breaks...we need to figure out it's height *and*
          //width, and then adjust the legend size accordingly
          Rectangle2D bounds = new Rectangle2D.Double(0, 0, 0, 0);
          ArrayList<Integer> lineHeight = new ArrayList<Integer>();
          // four backslashes... "\\" -> '\', so "\\\\n" -> '\' + '\' + 'n'
          final String realLabel = label.replaceAll("\\\\n", "\n");
          StringTokenizer st = new StringTokenizer(realLabel, "\n\r\f");
 
          while (st.hasMoreElements()) {
              final String token = st.nextToken();
              Rectangle2D thisLineBounds = g.getFontMetrics().getStringBounds(token, g);
 
              //if this is directly added as thisLineBounds.getHeight(), then there are rounding errors
              //because we can only DRAW fonts at discrete integer coords.
              final int thisLineHeight = (int) Math.ceil(thisLineBounds.getHeight());
              bounds.add(0, thisLineHeight + bounds.getHeight());
              bounds.add(thisLineBounds.getWidth(), 0);
              lineHeight.add((int) Math.ceil(thisLineBounds.getHeight()));
          }
 
          //make the actual label image
          renderedLabel = new BufferedImage((int) Math.ceil(bounds.getWidth()),
                  (int) Math.ceil(bounds.getHeight()), BufferedImage.TYPE_INT_ARGB);
 
          st = new StringTokenizer(realLabel, "\n\r\f");
 
          Graphics2D rlg = renderedLabel.createGraphics();
          rlg.setColor(labelColor);
          rlg.setFont(g.getFont());
          rlg.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
              g.getRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING));
 
          int y = 0 - g.getFontMetrics().getDescent();
          int c = 0;
 
          while (st.hasMoreElements()) {
              y += lineHeight.get(c++).intValue();
              rlg.drawString(st.nextToken(), 0, y);
          }
          rlg.dispose();
      } else {
          //this is a traditional 'regular-old' label.  Just figure the
          //size and act accordingly.
          int height = (int) Math.ceil(g.getFontMetrics().getStringBounds(label, g).getHeight());
          int width = (int) Math.ceil(g.getFontMetrics().getStringBounds(label, g).getWidth());
          renderedLabel = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
 
          Graphics2D rlg = renderedLabel.createGraphics();
          rlg.setColor(labelColor);
          rlg.setFont(g.getFont());
          rlg.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
              g.getRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING));
          rlg.drawString(label, 0, height - rlg.getFontMetrics().getDescent());
          rlg.dispose();
      }
 
      return renderedLabel;
  }
 
  /**
   * This method tries to merge horizontally 3 {@link BufferedImage}. The first one must be not null, the others can be null.
   *
   * @param left first {@link BufferedImage} to merge.
   * @param center second {@link BufferedImage} to merge.
   * @param right third {@link BufferedImage} to merge.
   * @param hintsMap hints to use for drawing
   * @param dx buffer between images
   * @param transparent tells me whether or not the bkg should be transparent
   * @param backgroundColor the background color
   * @return a {@link BufferedImage} for the union of the provided images.
   */
  public static BufferedImage hMergeBufferedImages(
      final BufferedImage left,
      final BufferedImage center,
      final BufferedImage right,
      final Map<Key, Object> hintsMap,
      final boolean transparent,
      final Color backgroundColor,
      final double dx) {
    if(right==null&&center==null)
      return left;
    if(left==null)
      throw new NullPointerException("Left image cannot be null");
    int totalHeight =  (int) (Math.max(left.getHeight(),Math.max((center!=null?center.getHeight():Double.NEGATIVE_INFINITY), right!=null?right.getHeight():0))+0.5);
    final int totalWidth = (int) (left.getWidth() +(center!=null? center.getWidth():0)+(right!=null?right.getWidth():0)+2*dx+0.5);           
        final BufferedImage finalImage = ImageUtils.createImage(totalWidth, totalHeight, (IndexColorModel)null, transparent);
        final Graphics2D finalGraphics = ImageUtils.prepareTransparency(transparent, backgroundColor, finalImage, hintsMap);
       
        //place the left element
        int offsetX=0;
        finalGraphics.drawImage(left, offsetX,0,null);

        ///place the central element
        offsetX=(int) (left.getWidth()+dx+0.5);
        if(center!=null){
          finalGraphics.drawImage(center, offsetX,0,null);
          offsetX+=(int) (center.getWidth()+dx+0.5);
        }
       
        ///place the right element in case we have it
        if(right!=null){
          finalGraphics.drawImage(right, offsetX,0,null);       
        }
       
        finalGraphics.dispose();
    return (BufferedImage) finalImage;
  }

  /**
   * Checks if the provided {@link FeatureType} contains a coverage as per used by the {@link StreamingRenderer}.
   *
   * @param layer a {@link FeatureType} to check if it contains a  grid.
   * @return <code>true</code> if this layer contains a gridcoverage, <code>false</code> otherwise.
   */
  public static boolean checkGridLayer(final FeatureType layer) {
    if(!(layer instanceof SimpleFeatureType))
      return false;
    boolean found=false;
    final Collection<PropertyDescriptor> descriptors = layer.getDescriptors();
    for(PropertyDescriptor descriptor: descriptors){
     
      //get the type
      final PropertyType type=descriptor.getType();
      if(type.getBinding().isAssignableFrom(GridCoverage2D.class)||type.getBinding().isAssignableFrom(AbstractGridCoverage2DReader.class))
      {
        found=true;
        break;
      }
     
    }
    return found;
  }


    /**
     * Checks if the provided style contains at least one {@link RasterSymbolizer}
     */
    public static boolean checkRasterSymbolizer(final Style style) {
        for (FeatureTypeStyle fts : style.featureTypeStyles()) {
            for (Rule r : fts.rules()) {
                for (Symbolizer s : r.symbolizers()) {
                    if (s instanceof RasterSymbolizer) {
                        return true;
                    }
                }
            }
        }
        return false;
    }
}


TOP

Related Classes of org.vfny.geoserver.wms.responses.LegendUtils

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.