/*
* 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.ext.legend;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import org.geotoolkit.storage.coverage.CoverageReference;
import org.geotoolkit.display.PortrayalException;
import org.geotoolkit.display2d.ext.BackgroundTemplate;
import org.geotoolkit.display2d.ext.BackgroundUtilities;
import org.geotoolkit.display2d.service.DefaultGlyphService;
import org.geotoolkit.map.DefaultCoverageMapLayer;
import org.geotoolkit.map.MapBuilder;
import org.geotoolkit.map.MapContext;
import org.geotoolkit.map.MapItem;
import org.geotoolkit.map.MapLayer;
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.util.ArgumentChecks;
import org.geotoolkit.style.MutableFeatureTypeStyle;
import org.geotoolkit.style.MutableRule;
import org.geotoolkit.style.MutableStyle;
import org.apache.sis.util.logging.Logging;
import org.opengis.util.GenericName;
import org.opengis.parameter.ParameterNotFoundException;
import org.opengis.parameter.ParameterValue;
import org.opengis.style.Description;
import org.opengis.style.Rule;
import org.opengis.util.InternationalString;
/**
* Utility class to render legend using a provided template.
*
* @author Johann Sorel (Geomatys)
* @module
*/
public class J2DLegendUtilities {
private static final Logger LOGGER = Logging.getLogger("org.geotoolkit.display2d.ext.legend");
private static final int GLYPH_SPACE = 5;
private J2DLegendUtilities() {
}
/**
* Paint a legend using Java2D.
*
* @param item : map context, from wich to extract style information
* @param g2d : Graphics2D used for rendering
* @param bounds : Rectangle where the legend must be painted
* @param template : Legend rendering hints.
*/
public static void paintLegend(MapItem item,
Graphics2D g2d,
final Rectangle bounds,
final LegendTemplate template) {
if (item instanceof MapLayer) {
final MapContext context = MapBuilder.createContext();
context.items().add(item);
item = context;
}
g2d = (Graphics2D) g2d.create();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setClip(bounds);
g2d.setStroke(new BasicStroke(1));
if (template == null) {
//no template generate a glyph
paintNoTemplate(item, g2d, bounds);
} else {
float X = bounds.x, Y = bounds.y;
// Will store the get legend graphic results
final Map<GenericName, BufferedImage> legendResults = new HashMap<>();
final Dimension estimation = estimate(g2d, item, template, legendResults, false);
final BackgroundTemplate background = template.getBackground();
if (background != null) {
final Rectangle area = new Rectangle(estimation);
area.x = bounds.x;
area.y = bounds.y;
Insets insets = background.getBackgroundInsets();
area.width += insets.left + insets.right;
area.height += insets.top + insets.bottom;
X += insets.left;
Y += insets.top;
BackgroundUtilities.paint(g2d, area, background);
}
g2d.translate(X, Y);
paintWithTemplate(item, g2d, bounds, template, legendResults);
g2d.translate(-X, -Y);
}
}
/**
* Paint the legend of the given MapItem, without any {@link LegendTemplate}.
* To do so, a simple glyph is generated for each layer of input item.
* @param item The {@link MapItem} to paint legend for.
* @param g2d The {@link Graphics2D} on which we'll paint legend.
* @param bounds The graphic area to paint into.
*/
private static void paintNoTemplate(final MapItem item, final Graphics2D g2d, final Rectangle bounds) {
for (MapItem layer : item.items()) {
if (layer instanceof MapLayer) {
DefaultGlyphService.render(((MapLayer) layer).getStyle(), bounds, g2d, (MapLayer) layer);
} else {
paintNoTemplate(layer, g2d, bounds);
}
}
}
/**
* Draw legend using given {@link LegendTemplate} as rendering hint. At the
* end of the drawning, input {@link Graphics2D} origin is reset at the origin
* it was when given as parameter.
* @param item The map item to draw legend for.
* @param g2d The graphic object to draw legend in.
* @param bounds Drawing authorized rectangle.
* @param template Rendering hints.
* @param legendResults useless. Store drawn images for each coverage layer
* @return The number of lines which have been drawn on y axis
*/
private static int paintWithTemplate(
final MapItem item,
final Graphics2D g2d,
final Rectangle bounds,
final LegendTemplate template,
final Map<GenericName, BufferedImage> legendResults) {
final AffineTransform origin = g2d.getTransform();
final FontMetrics layerFontMetric = g2d.getFontMetrics(template.getLayerFont());
final FontMetrics ruleFontMetric = g2d.getFontMetrics(template.getRuleFont());
final int ruleFontHeight = ruleFontMetric.getHeight();
final float gapSize = template.getGapSize();
final Dimension glyphSize = template.getGlyphSize();
final Rectangle2D rectangle = new Rectangle2D.Float();
float moveY = 0;
final List<MapItem> layers = item.items();
for (int l = 0, n = layers.size(); l < n; l++) {
final MapItem currentItem = layers.get(l);
//check if the given layer is visible, and if we should display invisible layers.
if (template.displayOnlyVisibleLayers() && !isVisible(currentItem)) {
continue;
}
// Check for current item title.
if (template.isLayerVisible()) {
if (l != 0) {
moveY += gapSize;
}
String title = "";
final Description description = currentItem.getDescription();
if (description != null) {
final InternationalString titleTmp = description.getTitle();
if (titleTmp != null) {
title = titleTmp.toString().replace("{}", "");
}
}
if (title.isEmpty()) {
title = currentItem.getName();
}
if (title != null && !title.isEmpty()) {
moveY += layerFontMetric.getLeading() + layerFontMetric.getAscent();
g2d.setFont(template.getLayerFont());
Color layerFontColor = template.getLayerFontColor();
if (layerFontColor != null) {
if (template.getLayerFontOpacity() != null) {
layerFontColor = new Color(layerFontColor.getRed(), layerFontColor.getGreen(),
layerFontColor.getBlue(), template.getLayerFontOpacity());
}
} else {
layerFontColor = Color.BLACK;
}
g2d.setColor(layerFontColor);
g2d.drawString(title, 0, moveY);
moveY += layerFontMetric.getDescent();
moveY += gapSize;
}
}
// If we're not on a leaf, try to display this node children.
if (!(currentItem instanceof MapLayer)) {
// Using doubles allows current position relative translation.
final double nodeInset = template.getBackground().getBackgroundInsets().left;
g2d.translate(nodeInset, moveY);
final int itemDim = paintWithTemplate(currentItem, g2d, bounds, template, legendResults);
g2d.translate(-nodeInset, -moveY);
// Previous function reset graphic position at the top of drawn map item. We Add its size to vertical offset, so next item knows how much pixels it should jump.
moveY += itemDim;
continue;
}
final MapLayer layer = (MapLayer) layers.get(l);
// If we are browsing a coverage map layer, a default generic style has been defined,
// we can use the result of a GetLegendGraphic request instead. It should presents the
// default style defined on the WMS service for this layer
wmscase:
if (layer instanceof DefaultCoverageMapLayer) {
final DefaultCoverageMapLayer covLayer = (DefaultCoverageMapLayer)layer;
// Get the image from the ones previously stored, to not resend a get legend graphic request.
final BufferedImage image = legendResults.get(covLayer.getCoverageReference().getName().tip().toString());
if (image == null) {
break wmscase;
}
if (l != 0) {
moveY += gapSize;
}
g2d.drawImage(image, null, 0, Math.round(moveY));
moveY += image.getHeight();
continue;
}
final MutableStyle style = layer.getStyle();
if (style == null) {
continue;
}
int numElement = 0;
for (final MutableFeatureTypeStyle fts : style.featureTypeStyles()) {
for (final MutableRule rule : fts.rules()) {
if (numElement != 0) {
moveY += gapSize;
}
//calculate the rule text displacement with the glyph size
final float stepRuleTitle;
final float glyphHeight;
final float glyphWidth;
final Dimension preferred = DefaultGlyphService.glyphPreferredSize(rule, null, layer);
if (glyphSize == null) {
//find the best size
glyphHeight = preferred.height;
glyphWidth = preferred.width;
} else {
// Use the biggest size between preferred one and default one.
glyphHeight = Math.max(glyphSize.height, preferred.height);
glyphWidth = Math.max(glyphSize.width, preferred.width);
}
if (glyphHeight > ruleFontHeight) {
stepRuleTitle = ruleFontMetric.getLeading() + ruleFontMetric.getAscent()
+ (glyphHeight - ruleFontHeight) / 2;
} else {
stepRuleTitle = ruleFontMetric.getLeading() + ruleFontMetric.getAscent();
}
rectangle.setRect(0, moveY, glyphWidth, glyphHeight);
DefaultGlyphService.render(rule, rectangle, g2d, layer);
String title = "";
final Description description = rule.getDescription();
if (description != null) {
final InternationalString titleTmp = description.getTitle();
if (titleTmp != null) {
title = titleTmp.toString();
}
}
if (title.isEmpty()) {
moveY += glyphHeight;
} else {
g2d.setFont(template.getRuleFont());
g2d.setColor(Color.BLACK);
g2d.drawString(title, glyphWidth + GLYPH_SPACE, moveY + stepRuleTitle);
moveY += (glyphHeight > ruleFontHeight) ? glyphHeight : ruleFontHeight;
}
numElement++;
}
}
}
g2d.setTransform(origin);
return (int) moveY;
}
/**
* Paint a legend using Java2D.
*
* @param rules : A list of rules to use for legend rendering.
* @param g2d : Graphics2D used to render
* @param bounds : Rectangle where the legend must be painted
* @param template : Legend rendering hints.
* @throws org.geotoolkit.display.exception.PortrayalException
*/
public static void paintLegend(final List<Rule> rules,
Graphics2D g2d,
final Rectangle bounds,
final LegendTemplate template) throws PortrayalException {
g2d = (Graphics2D) g2d.create();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setClip(bounds);
g2d.setStroke(new BasicStroke(1));
final float gapSize = template.getGapSize();
final float glyphHeight = template.getGlyphSize().height;
final float glyphWidth = template.getGlyphSize().width;
final Rectangle2D rectangle = new Rectangle2D.Float();
float X = bounds.x;
float Y = bounds.y;
for (final Rule rule : rules) {
String title = "";
final Description description = rule.getDescription();
if (description != null) {
final InternationalString titleTmp = description.getTitle();
if (titleTmp != null) {
title = titleTmp.toString();
}
}
rectangle.setRect(X, Y, glyphWidth, glyphHeight);
DefaultGlyphService.render(rule, rectangle, g2d, null);
g2d.setFont(template.getRuleFont());
g2d.setColor(Color.BLACK);
g2d.drawString(title, X + glyphWidth + gapSize, Y + glyphHeight);
Y += glyphHeight + gapSize;
}
}
/**
* Compute the optimum size for the given {@link MapItem}, using given configuration.
* @param g The graphic2D in which we want to paint legend.
* @param mapitem The mapItem we want legend for.
* @param template The {@link LegendTemplate} defining legend paniting rules.
* Can be null.
* @param considerBackground A boolean to specify if we must count input
* {@link LegendTemplate} background margins in the result size (true) or
* not (false).
*
* @return An estimation of the size (pixels) needed to display the complete
* legend of the input map item.
*/
public static Dimension estimate(final Graphics2D g, MapItem mapitem, final LegendTemplate template, final boolean considerBackground) {
return estimate(g, mapitem, template, null, considerBackground);
}
/**
* Compute the optimum size for the given {@link MapItem}, using given configuration.
* @param g The graphic2D in which we want to paint legend.
* @param mapitem The mapItem we want legend for.
* @param template The {@link LegendTemplate} defining legend paniting rules.
* Can be null.
* @param images A map in which we'll store eventual legend we generated
* during the process (key = layer name, value = layer legend). Can be null.
* @param considerBackground A boolean to specify if we must count input
* {@link LegendTemplate} background margins in the result size (true) or
* not (false).
*
* @return An estimation of the size (pixels) needed to display the complete
* legend of the input map item.
*/
public static Dimension estimate(final Graphics2D g, MapItem mapitem, final LegendTemplate template,
final Map<GenericName,BufferedImage> images, final boolean considerBackground)
{
final Dimension dim = new Dimension(0, 0);
if (mapitem == null) {
return dim;
}
if (mapitem instanceof MapLayer) {
final MapContext context = MapBuilder.createContext();
context.items().add(mapitem);
mapitem = context;
}
if (template == null) {
//fallback on glyph size
estimateNoTemplate(mapitem, dim);
} else {
estimateWithTemplate(g, mapitem, dim, template, images, considerBackground);
}
checkMinimumSize(dim);
return dim;
}
/**
* Estimate the size (pixels) needed to render the given {@link MapItem} legend
* without using any rendering hint (see {@link LegendTemplate}).
* @param source the map item to paint legend for.
* @param toSet the dimension used to store estimation result.
*/
private static void estimateNoTemplate(MapItem source, Dimension toSet) {
for (MapItem childItem : source.items()) {
if (childItem instanceof MapLayer) {
final MapLayer ml = (MapLayer) childItem;
final Dimension preferred = new Dimension(0, 0);
DefaultGlyphService.glyphPreferredSize(ml.getStyle(), preferred, ml);
if (preferred != null) {
if (preferred.width > toSet.width) {
toSet.width = preferred.width;
}
toSet.height += preferred.height;
}
} else {
estimateNoTemplate(childItem, toSet);
}
}
}
/**
* Estimate the size (pixels) needed to render the given {@link MapItem} legend
* using the input rendering hint (see {@link LegendTemplate}).
* @param g2d The {@link Graphics2D} to paint legend into.
* @param source The source map item to render.
* @param toSet The {@link Dimension} to store estimation result into.
* @param template the {@link LegendTemplate} to use as rendering hint.
* @param images A map in which we'll store eventual legend we generated
* during the process (key = layer name, value = layer legend). Can be null.
* @param considerBackground A boolean to specify if we must count input
* {@link LegendTemplate} background margins in the result size (true) or
* not (false).
*/
private static void estimateWithTemplate(
final Graphics2D g2d,
final MapItem source,
final Dimension toSet,
final LegendTemplate template,
final Map<GenericName,BufferedImage> images,
final boolean considerBackground) {
final FontMetrics layerFontMetric = g2d.getFontMetrics(template.getLayerFont());
final FontMetrics ruleFontMetric = g2d.getFontMetrics(template.getRuleFont());
final int ruleFontHeight = ruleFontMetric.getHeight();
final Dimension glyphSize = template.getGlyphSize();
final List<MapItem> childItems = source.items();
for (int l = 0, n = childItems.size(); l < n; l++) {
/* If legend template asks for visible items only, we have to proceed
* in two steps, because we cannot just check item visibility.
* If we are on a container (not a layer), it must be considered as
* invisible if none of its children is visible.
*/
final MapItem currentItem = childItems.get(l);
if (template.displayOnlyVisibleLayers() && !isVisible(currentItem)) {
continue;
}
if (template.isLayerVisible()) {
if (l != 0) {
toSet.height += template.getGapSize();
}
// Determines space to reserve for title.
final Dimension titleDim = estimateTitle(currentItem, layerFontMetric);
toSet.height += titleDim.height;
if (toSet.width < titleDim.width) {
toSet.width = titleDim.width;
}
if (titleDim.height > 0) {
toSet.height += template.getGapSize();
}
}
if (!(currentItem instanceof MapLayer)) {
estimateWithTemplate(g2d, currentItem, toSet, template, images, considerBackground);
continue;
}
final MapLayer layer = (MapLayer) currentItem;
// Launch a get legend request and take the dimensions from the result
testwms:
if (layer instanceof DefaultCoverageMapLayer) {
final DefaultCoverageMapLayer covLayer = (DefaultCoverageMapLayer)layer;
final CoverageReference covRef = covLayer.getCoverageReference();
if (covRef == null) {
continue;
}
// try first to retrieve the legend directly from the coverage reference.
BufferedImage image;
try {
image = (BufferedImage) covRef.getLegend();
} catch (DataStoreException ex) {
LOGGER.log(Level.FINE, ex.getLocalizedMessage(), ex);
continue;
}
if (image != null) {
toSet.height += image.getHeight();
if (toSet.width < image.getWidth()) {
toSet.width = image.getWidth();
}
if(images != null){
images.put(covLayer.getCoverageReference().getName(), image);
}
continue;
}
}
final MutableStyle style = layer.getStyle();
if (style == null) {
continue;
}
int numElement = 0;
for (final MutableFeatureTypeStyle fts : style.featureTypeStyles()) {
for (final MutableRule rule : fts.rules()) {
if (numElement != 0) {
toSet.height += template.getGapSize();
}
//calculate the text lenght
int textLenght = 0;
final Description description = rule.getDescription();
if (description != null) {
final InternationalString titleTmp = description.getTitle();
if (titleTmp != null) {
final String title = titleTmp.toString();
textLenght = ruleFontMetric.stringWidth(title);
}
}
//calculate the glyph size
final int glyphHeight;
final int glyphWidth;
final Dimension preferred = DefaultGlyphService.glyphPreferredSize(rule, null, layer);
if (glyphSize == null) {
//find the best size
glyphHeight = preferred.height;
glyphWidth = preferred.width;
} else {
//use the defined size
glyphHeight = glyphSize.height > preferred.height ? glyphSize.height : preferred.height;
glyphWidth = glyphSize.width > preferred.width ? glyphSize.width : preferred.width;
}
final int totalWidth = glyphWidth + ((textLenght == 0) ? 0 : (GLYPH_SPACE + textLenght));
final int fh = (textLenght > 0) ? ruleFontHeight : 0;
final int totalHeight = (glyphHeight > fh) ? glyphHeight : fh;
toSet.height += totalHeight;
if (toSet.width < totalWidth) {
toSet.width = totalWidth;
}
numElement++;
}
}
}
if (considerBackground && template.getBackground() != null) {
final Insets insets = template.getBackground().getBackgroundInsets();
toSet.width += insets.left + insets.right;
toSet.height += insets.bottom + insets.top;
}
}
/**
* Ensure that the current map item is visible. For {@link MapLayer}, it means
* that it is visible itself. For MapItems, we check recursively all of its
* children until we find a visible {@link MapLayer}. An item is considered
* invisible if itself or all of its children are marked as not visible.
* @param toCheck The map item to analyse. Cannot be null.
* @return True if input item or at least one of its {@link MapLayer} child
* is visible. False otherwise.
*/
public static boolean isVisible(final MapItem toCheck) {
ArgumentChecks.ensureNonNull("Map item to check visibility on", toCheck);
// If it's a visible container, we must check its children.
if (toCheck.isVisible() && !(toCheck instanceof MapLayer)) {
for (final MapItem child : toCheck.items()) {
if (isVisible(child)) {
return true;
}
}
return false;
} else {
return toCheck.isVisible();
}
}
private static void checkMinimumSize(final Dimension dim) {
if (dim.width == 0) {
dim.width = 1;
}
if (dim.height == 0) {
dim.height = 1;
}
}
private static Dimension estimateTitle(final MapItem source, final FontMetrics fontRules) {
final Dimension dim = new Dimension(0, 0);
String title = "";
final Description description = source.getDescription();
if (description != null) {
final InternationalString titleTmp = description.getTitle();
if (titleTmp != null) {
title = titleTmp.toString().replace("{}", "");
}
if (title.isEmpty() && source.getName() != null) {
title = source.getName();
}
}
if (!title.isEmpty()) {
dim.width = fontRules.stringWidth(title);
dim.height = fontRules.getHeight();
}
return dim;
}
}