/**
   * Extracts legend for layer based on LayerInfo configuration or style LegendGraphics.
   *
   * @param published FeatureType representing the layer
   * @param w width for the image (hint)
   * @param h height for the image (hint)
   * @param transparent (should the image be transparent)
   * @param request GetLegendGraphicRequest being built
   * @return image with the title
   */
  private RenderedImage getLayerLegend(
      LegendRequest legend, int w, int h, boolean transparent, GetLegendGraphicRequest request) {

    LegendInfo legendInfo = legend.getLegendInfo();
    if (legendInfo == null) {
      return null; // nothing provided will need to dynamically generate
    }
    String onlineResource = legendInfo.getOnlineResource();
    if (onlineResource == null || onlineResource.isEmpty()) {
      return null; // nothing provided will need to dynamically generate
    }
    URL url = null;
    try {
      url = new URL(onlineResource);
    } catch (MalformedURLException invalid) {
      LOGGER.fine("Unable to obtain " + onlineResource);
      return null; // should log this!
    }
    try {
      BufferedImage image = ImageIO.read(url);

      if (image.getWidth() == w && image.getHeight() == h) {
        return image;
      }
      final BufferedImage rescale = ImageUtils.createImage(w, h, (IndexColorModel) null, true);

      Graphics2D g = (Graphics2D) rescale.getGraphics();
      g.setColor(new Color(255, 255, 255, 0));
      g.fillRect(0, 0, w, h);

      double aspect = ((double) h) / ((double) image.getHeight());
      int legendWidth = (int) (aspect * ((double) image.getWidth()));

      g.drawImage(image, 0, 0, legendWidth, h, null);
      g.dispose();

      return rescale;
    } catch (IOException notFound) {
      LOGGER.log(Level.FINE, "Unable to legend graphic:" + url, notFound);
      return null; // unable to access image
    }
  }
  /**
   * Receives a list of <code>BufferedImages</code> and produces a new one which holds all the
   * images in <code>imageStack</code> one above the other, handling labels.
   *
   * @param imageStack the list of BufferedImages, one for each applicable Rule
   * @param rules The applicable rules, one for each image in the stack (if not null it's used to
   *     compute labels)
   * @param request The request.
   * @param forceLabelsOn true for force labels on also with a single image.
   * @param forceLabelsOff true for force labels off also with more than one rule.
   * @return the stack image with all the images on the argument list.
   * @throws IllegalArgumentException if the list is empty
   */
  private BufferedImage mergeLegends(
      List<RenderedImage> imageStack,
      Rule[] rules,
      GetLegendGraphicRequest req,
      boolean forceLabelsOn,
      boolean forceLabelsOff) {

    Font labelFont = LegendUtils.getLabelFont(req);
    boolean useAA = LegendUtils.isFontAntiAliasing(req);

    if (imageStack.size() == 0) {
      return null;
    }

    final BufferedImage finalLegend;

    if (imageStack.size() == 1 && (!forceLabelsOn || rules == null)) {
      finalLegend = (BufferedImage) imageStack.get(0);
    } else {
      final int imgCount = imageStack.size();
      final String[] labels = new String[imgCount];

      BufferedImage img = ((BufferedImage) imageStack.get(0));

      int totalHeight = 0;
      int totalWidth = 0;
      int[] rowHeights = new int[imgCount];
      BufferedImage labelsGraphics[] = new BufferedImage[imgCount];
      for (int i = 0; i < imgCount; i++) {
        img = (BufferedImage) imageStack.get(i);

        if (forceLabelsOff || rules == null) {
          totalWidth = (int) Math.ceil(Math.max(img.getWidth(), totalWidth));
          rowHeights[i] = img.getHeight();
          totalHeight += img.getHeight();
        } else {

          Rule rule = rules[i];

          // What's the label on this rule? We prefer to use
          // the 'title' if it's available, but fall-back to 'name'
          final Description description = rule.getDescription();
          Locale locale = req.getLocale();

          if (description != null && description.getTitle() != null) {
            final InternationalString title = description.getTitle();
            if (locale != null) {
              labels[i] = title.toString(locale);
            } else {
              labels[i] = title.toString();
            }
          } else if (rule.getName() != null) {
            labels[i] = rule.getName();
          } else {
            labels[i] = "";
          }

          if (labels[i] != null && labels[i].length() > 0) {
            final BufferedImage renderedLabel = getRenderedLabel(img, labels[i], req);
            labelsGraphics[i] = renderedLabel;
            final Rectangle2D bounds =
                new Rectangle2D.Double(0, 0, renderedLabel.getWidth(), renderedLabel.getHeight());

            totalWidth = (int) Math.ceil(Math.max(img.getWidth() + bounds.getWidth(), totalWidth));
            rowHeights[i] = (int) Math.ceil(Math.max(img.getHeight(), bounds.getHeight()));
          } else {
            totalWidth = (int) Math.ceil(Math.max(img.getWidth(), totalWidth));
            rowHeights[i] = (int) Math.ceil(img.getHeight());
            labelsGraphics[i] = null;
          }

          totalHeight += rowHeights[i];
        }
      }

      // buffer the width a bit
      totalWidth += 2;

      final boolean transparent = req.isTransparent();
      final Color backgroundColor = LegendUtils.getBackgroundColor(req);
      final Map<RenderingHints.Key, Object> hintsMap = new HashMap<RenderingHints.Key, Object>();
      // create the final image
      finalLegend =
          ImageUtils.createImage(totalWidth, totalHeight, (IndexColorModel) null, transparent);
      Graphics2D finalGraphics =
          ImageUtils.prepareTransparency(transparent, backgroundColor, finalLegend, hintsMap);

      int topOfRow = 0;

      for (int i = 0; i < imgCount; i++) {
        img = (BufferedImage) imageStack.get(i);

        // draw the image
        int y = topOfRow;

        if (img.getHeight() < rowHeights[i]) {
          // move the image to the center of the row
          y += (int) ((rowHeights[i] - img.getHeight()) / 2d);
        }

        finalGraphics.drawImage(img, 0, y, null);
        if (forceLabelsOff || rules == null) {
          topOfRow += rowHeights[i];
          continue;
        }

        finalGraphics.setFont(labelFont);

        if (useAA) {
          finalGraphics.setRenderingHint(
              RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
        } else {
          finalGraphics.setRenderingHint(
              RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
        }

        // draw the label
        if (labels[i] != null && labels[i].length() > 0) {
          // first create the actual overall label image.
          final BufferedImage renderedLabel = labelsGraphics[i];

          y = topOfRow;

          if (renderedLabel.getHeight() < rowHeights[i]) {
            y += (int) ((rowHeights[i] - renderedLabel.getHeight()) / 2d);
          }

          finalGraphics.drawImage(renderedLabel, img.getWidth(), y, null);
          // cleanup
          renderedLabel.flush();
          labelsGraphics[i] = null;
        }

        topOfRow += rowHeights[i];
      }

      finalGraphics.dispose();
    }
    return finalLegend;
  }
 /**
  * Renders a title for a layer (to be put on top of the layer legend).
  *
  * @param legend FeatureType representing the layer
  * @param w width for the image (hint)
  * @param h height for the image (hint)
  * @param transparent (should the image be transparent)
  * @param request GetLegendGraphicRequest being built
  * @return image with the title
  */
 private RenderedImage getLayerTitle(
     LegendRequest legend, int w, int h, boolean transparent, GetLegendGraphicRequest request) {
   String title = legend.getTitle();
   final BufferedImage image = ImageUtils.createImage(w, h, (IndexColorModel) null, transparent);
   return getRenderedLabel(image, title, request);
 }
  /**
   * Takes a GetLegendGraphicRequest and produces a BufferedImage that then can be used by a
   * subclass to encode it to the appropriate output format.
   *
   * @param request the "parsed" request, where "parsed" means that it's values are already
   *     validated so this method must not take care of verifying the requested layer exists and the
   *     like.
   * @throws ServiceException if there are problems creating a "sample" feature instance for the
   *     FeatureType <code>request</code> returns as the required layer (which should not occur).
   */
  public BufferedImage buildLegendGraphic(GetLegendGraphicRequest request) throws ServiceException {
    // list of images to be rendered for the layers (more than one if
    // a layer group is given)
    List<RenderedImage> layersImages = new ArrayList<RenderedImage>();

    List<LegendRequest> layers = request.getLegends();
    // List<FeatureType> layers=request.getLayers();
    // List<Style> styles=request.getStyles();
    // List<String> rules=request.getRules();

    boolean forceLabelsOn = false;
    boolean forceLabelsOff = false;
    if (request.getLegendOptions().get("forceLabels") instanceof String) {
      String forceLabelsOpt = (String) request.getLegendOptions().get("forceLabels");
      if (forceLabelsOpt.equalsIgnoreCase("on")) {
        forceLabelsOn = true;
      } else if (forceLabelsOpt.equalsIgnoreCase("off")) {
        forceLabelsOff = true;
      }
    }

    boolean forceTitlesOff = false;
    if (request.getLegendOptions().get("forceTitles") instanceof String) {
      String forceTitlesOpt = (String) request.getLegendOptions().get("forceTitles");
      if (forceTitlesOpt.equalsIgnoreCase("off")) {
        forceTitlesOff = true;
      }
    }

    for (LegendRequest legend : layers) {
      FeatureType layer = legend.getFeatureType();

      // style and rule to use for the current layer
      Style gt2Style = legend.getStyle();
      if (gt2Style == null) {
        throw new NullPointerException("request.getStyle()");
      }

      // get rule corresponding to the layer index
      // normalize to null for NO RULE
      String ruleName = legend.getRule(); // was null

      // width and height, we might have to rescale those in case of DPI usage
      int w = request.getWidth();
      int h = request.getHeight();

      // apply dpi rescale
      double dpi = RendererUtilities.getDpi(request.getLegendOptions());
      double standardDpi = RendererUtilities.getDpi(Collections.emptyMap());
      if (dpi != standardDpi) {
        double scaleFactor = dpi / standardDpi;
        w = (int) Math.round(w * scaleFactor);
        h = (int) Math.round(h * scaleFactor);
        DpiRescaleStyleVisitor dpiVisitor = new DpiRescaleStyleVisitor(scaleFactor);
        dpiVisitor.visit(gt2Style);
        gt2Style = (Style) dpiVisitor.getCopy();
      }
      // apply UOM rescaling if we have a scale
      if (request.getScale() > 0) {
        double pixelsPerMeters =
            RendererUtilities.calculatePixelsPerMeterRatio(
                request.getScale(), request.getLegendOptions());
        UomRescaleStyleVisitor rescaleVisitor = new UomRescaleStyleVisitor(pixelsPerMeters);
        rescaleVisitor.visit(gt2Style);
        gt2Style = (Style) rescaleVisitor.getCopy();
      }

      boolean strict = request.isStrict();

      final boolean transparent = request.isTransparent();
      RenderedImage titleImage = null;
      // if we have more than one layer, we put a title on top of each layer legend
      if (layers.size() > 1 && !forceTitlesOff) {
        titleImage = getLayerTitle(legend, w, h, transparent, request);
      }

      // Check for rendering transformation
      boolean hasVectorTransformation = false;
      boolean hasRasterTransformation = false;
      List<FeatureTypeStyle> ftsList = gt2Style.featureTypeStyles();
      for (int i = 0; i < ftsList.size(); i++) {
        FeatureTypeStyle fts = ftsList.get(i);
        Expression exp = fts.getTransformation();
        if (exp != null) {
          ProcessFunction processFunction = (ProcessFunction) exp;
          Name processName = processFunction.getProcessName();
          Map<String, Parameter<?>> outputs = Processors.getResultInfo(processName, null);
          if (outputs.isEmpty()) {
            continue;
          }
          Parameter<?> output =
              outputs.values().iterator().next(); // we assume there is only one output
          if (SimpleFeatureCollection.class.isAssignableFrom(output.getType())) {
            hasVectorTransformation = true;
            break;
          } else if (GridCoverage2D.class.isAssignableFrom(output.getType())) {
            hasRasterTransformation = true;
            break;
          }
        }
      }

      final boolean buildRasterLegend =
          (!strict && layer == null && LegendUtils.checkRasterSymbolizer(gt2Style))
              || (LegendUtils.checkGridLayer(layer) && !hasVectorTransformation)
              || hasRasterTransformation;

      // Just checks LegendInfo currently, should check gtStyle
      final boolean useProvidedLegend = layer != null && legend.getLayerInfo() != null;

      RenderedImage legendImage = null;
      if (useProvidedLegend) {
        legendImage = getLayerLegend(legend, w, h, transparent, request);
      }

      if (buildRasterLegend) {
        final RasterLayerLegendHelper rasterLegendHelper =
            new RasterLayerLegendHelper(request, gt2Style, ruleName);
        final BufferedImage image = rasterLegendHelper.getLegend();
        if (image != null) {
          if (titleImage != null) {
            layersImages.add(titleImage);
          }
          layersImages.add(image);
        }
      } else if (useProvidedLegend && legendImage != null) {
        if (titleImage != null) {
          layersImages.add(titleImage);
        }
        layersImages.add(legendImage);
      } else {
        final Feature sampleFeature;
        if (layer == null || hasVectorTransformation) {
          sampleFeature = createSampleFeature();
        } else {
          sampleFeature = createSampleFeature(layer);
        }
        final FeatureTypeStyle[] ftStyles =
            gt2Style.featureTypeStyles().toArray(new FeatureTypeStyle[0]);
        final double scaleDenominator = request.getScale();

        final Rule[] applicableRules;

        if (ruleName != null) {
          Rule rule = LegendUtils.getRule(ftStyles, ruleName);
          if (rule == null) {
            throw new ServiceException(
                "Specified style does not contains a rule named " + ruleName);
          }
          applicableRules = new Rule[] {rule};
        } else {
          applicableRules = LegendUtils.getApplicableRules(ftStyles, scaleDenominator);
        }

        final NumberRange<Double> scaleRange =
            NumberRange.create(scaleDenominator, scaleDenominator);
        final int ruleCount = applicableRules.length;

        /**
         * A legend graphic is produced for each applicable rule. They're being held here until the
         * process is done and then painted on a "stack" like legend.
         */
        final List<RenderedImage> legendsStack = new ArrayList<RenderedImage>(ruleCount);

        final SLDStyleFactory styleFactory = new SLDStyleFactory();

        double minimumSymbolSize = MINIMUM_SYMBOL_SIZE;
        // get minSymbolSize from LEGEND_OPTIONS, if defined
        if (request.getLegendOptions().get("minSymbolSize") instanceof String) {
          String minSymbolSizeOpt = (String) request.getLegendOptions().get("minSymbolSize");
          try {
            minimumSymbolSize = Double.parseDouble(minSymbolSizeOpt);
          } catch (NumberFormatException e) {
            throw new IllegalArgumentException("Invalid minSymbolSize value: should be a number");
          }
        }
        // calculate the symbols rescaling factor necessary for them to be
        // drawn inside the icon box
        double symbolScale =
            calcSymbolScale(w, h, layer, sampleFeature, applicableRules, minimumSymbolSize);

        for (int i = 0; i < ruleCount; i++) {

          final RenderedImage image =
              ImageUtils.createImage(w, h, (IndexColorModel) null, transparent);
          final Map<RenderingHints.Key, Object> hintsMap =
              new HashMap<RenderingHints.Key, Object>();
          final Graphics2D graphics =
              ImageUtils.prepareTransparency(
                  transparent, LegendUtils.getBackgroundColor(request), image, hintsMap);
          graphics.setRenderingHint(
              RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

          Feature sample = getSampleFeatureForRule(layer, sampleFeature, applicableRules[i]);

          FilterFactory ff = CommonFactoryFinder.getFilterFactory();
          final Symbolizer[] symbolizers = applicableRules[i].getSymbolizers();
          final GraphicLegend graphic = applicableRules[i].getLegend();

          // If this rule has a legend graphic defined in the SLD, use it
          if (graphic != null) {
            if (this.samplePoint == null) {
              Coordinate coord = new Coordinate(w / 2, h / 2);

              try {
                this.samplePoint = new LiteShape2(geomFac.createPoint(coord), null, null, false);
              } catch (Exception e) {
                this.samplePoint = null;
              }
            }
            shapePainter.paint(graphics, this.samplePoint, graphic, scaleDenominator, false);

          } else {

            for (int sIdx = 0; sIdx < symbolizers.length; sIdx++) {
              Symbolizer symbolizer = symbolizers[sIdx];

              if (symbolizer instanceof RasterSymbolizer) {
                // skip it
              } else {
                // rescale symbols if needed
                if (symbolScale > 1.0 && symbolizer instanceof PointSymbolizer) {
                  PointSymbolizer pointSymbolizer = cloneSymbolizer(symbolizer);
                  if (pointSymbolizer.getGraphic() != null) {
                    double size =
                        getPointSymbolizerSize(sample, pointSymbolizer, Math.min(w, h) - 4);
                    pointSymbolizer
                        .getGraphic()
                        .setSize(ff.literal(size / symbolScale + minimumSymbolSize));

                    symbolizer = pointSymbolizer;
                  }
                }

                Style2D style2d = styleFactory.createStyle(sample, symbolizer, scaleRange);
                LiteShape2 shape = getSampleShape(symbolizer, w, h);

                if (style2d != null) {
                  shapePainter.paint(graphics, shape, style2d, scaleDenominator);
                }
              }
            }
          }
          if (image != null && titleImage != null) {
            layersImages.add(titleImage);
            titleImage = null;
          }
          legendsStack.add(image);
          graphics.dispose();
        }

        // JD: changed legend behavior, see GEOS-812
        // this.legendGraphic = scaleImage(mergeLegends(legendsStack), request);
        BufferedImage image =
            mergeLegends(legendsStack, applicableRules, request, forceLabelsOn, forceLabelsOff);
        if (image != null) {
          layersImages.add(image);
        }
      }
    }
    // all legend graphics are merged if we have a layer group
    BufferedImage finalLegend =
        mergeLegends(layersImages, null, request, forceLabelsOn, forceLabelsOff);
    if (finalLegend == null) {
      throw new IllegalArgumentException("no legend passed");
    }
    return finalLegend;
  }