gernotbelger@9555: /** Copyright (C) 2017 by Bundesanstalt für Gewässerkunde gernotbelger@9555: * Software engineering by gernotbelger@9555: * Björnsen Beratende Ingenieure GmbH gernotbelger@9555: * Dr. Schumacher Ingenieurbüro für Wasser und Umwelt gernotbelger@9555: * gernotbelger@9555: * This file is Free Software under the GNU AGPL (>=v3) gernotbelger@9555: * and comes with ABSOLUTELY NO WARRANTY! Check out the gernotbelger@9555: * documentation coming with Dive4Elements River for details. gernotbelger@9555: */ gernotbelger@9555: package org.dive4elements.river.exports; gernotbelger@9555: gernotbelger@9555: import java.awt.Font; gernotbelger@9555: import java.awt.Paint; gernotbelger@9555: import java.awt.Shape; gernotbelger@9555: import java.awt.Stroke; gernotbelger@9555: import java.util.AbstractMap.SimpleEntry; gernotbelger@9555: import java.util.ArrayList; gernotbelger@9555: import java.util.HashMap; gernotbelger@9555: import java.util.HashSet; gernotbelger@9555: import java.util.IdentityHashMap; gernotbelger@9555: import java.util.Iterator; gernotbelger@9555: import java.util.List; gernotbelger@9555: import java.util.Map; gernotbelger@9555: import java.util.Map.Entry; gernotbelger@9555: import java.util.Set; gernotbelger@9555: gernotbelger@9555: import org.dive4elements.artifacts.CallContext; gernotbelger@9555: import org.dive4elements.river.artifacts.context.RiverContext; gernotbelger@9555: import org.dive4elements.river.artifacts.resources.Resources; gernotbelger@9555: import org.dive4elements.river.jfree.StyledSeries; gernotbelger@9555: import org.dive4elements.river.jfree.StyledXYDataset; gernotbelger@9555: import org.dive4elements.river.themes.Theme; gernotbelger@9555: import org.dive4elements.river.themes.ThemeDocument; gernotbelger@9555: import org.dive4elements.river.themes.ThemeFactory; gernotbelger@9555: import org.jfree.chart.LegendItem; gernotbelger@9555: import org.jfree.chart.LegendItemCollection; gernotbelger@9555: import org.jfree.chart.plot.XYPlot; gernotbelger@9555: import org.jfree.data.general.Dataset; gernotbelger@9555: import org.jfree.data.xy.XYSeries; gernotbelger@9555: import org.jfree.data.xy.XYSeriesCollection; gernotbelger@9555: import org.w3c.dom.Document; gernotbelger@9555: gernotbelger@9555: /** gernotbelger@9555: * Builds and adds legend entries to the plot. gernotbelger@9555: * gernotbelger@9555: * @author Gernot Belger gernotbelger@9555: */ gernotbelger@9555: public class LegendAggregator { gernotbelger@9555: gernotbelger@9555: private static final String NULL_THEME_TYPE = ""; //$NON-NLS-1$ gernotbelger@9555: gernotbelger@9555: private static final String I10N_MERGED = "legend.aggregator.merged"; //$NON-NLS-1$ gernotbelger@9555: gernotbelger@9555: private final List> legendItems = new ArrayList<>(); gernotbelger@9555: gernotbelger@9555: private final Map> itemsPerThemeType = new HashMap<>(); gernotbelger@9555: gernotbelger@9555: private final Font labelFont; gernotbelger@9555: gernotbelger@9555: private final int aggregationThreshold; gernotbelger@9555: gernotbelger@9555: public LegendAggregator(final int aggregationThreshold, final Font labelFont) { gernotbelger@9555: this.aggregationThreshold = aggregationThreshold; gernotbelger@9555: this.labelFont = labelFont; gernotbelger@9555: } gernotbelger@9555: gernotbelger@9555: public void addLegendItem(final String themeType, final LegendItem item) { gernotbelger@9555: gernotbelger@9555: item.setLabelFont(this.labelFont); gernotbelger@9555: gernotbelger@9555: this.legendItems.add(new SimpleEntry<>(item, themeType)); gernotbelger@9555: gernotbelger@9555: final List perThemeType = getItemsPerThemeType(themeType); gernotbelger@9555: perThemeType.add(item); gernotbelger@9555: } gernotbelger@9555: gernotbelger@9555: private List getItemsPerThemeType(final String themeType) { gernotbelger@9555: gernotbelger@9555: final String key = themeType == null ? NULL_THEME_TYPE : themeType; gernotbelger@9555: gernotbelger@9555: if (!this.itemsPerThemeType.containsKey(key)) gernotbelger@9555: this.itemsPerThemeType.put(key, new ArrayList()); gernotbelger@9555: gernotbelger@9555: return this.itemsPerThemeType.get(key); gernotbelger@9555: } gernotbelger@9555: gernotbelger@9555: /** gernotbelger@9555: * Apply the gathered items to the plot. Use only once. gernotbelger@9555: */ gernotbelger@9555: public void apply(final CallContext context, final XYPlot plot) { gernotbelger@9555: gernotbelger@9555: final LegendItemCollection aggregatedItems = new LegendItemCollection(); gernotbelger@9555: gernotbelger@9555: /* marker set for types that are already aggregated */ gernotbelger@9555: final Set aggregatedTypes = new HashSet<>(); gernotbelger@9555: gernotbelger@9555: for (final Entry entry : this.legendItems) { gernotbelger@9555: gernotbelger@9555: final LegendItem item = entry.getKey(); gernotbelger@9555: final String themeType = entry.getValue(); gernotbelger@9555: gernotbelger@9555: /* ignore already aggregated items */ gernotbelger@9555: if (aggregatedTypes.contains(themeType)) gernotbelger@9555: continue; gernotbelger@9555: gernotbelger@9555: /* aggregate known types if count over threshold */ gernotbelger@9555: final Theme legendTheme = getLegendTheme(context, themeType); gernotbelger@9555: gernotbelger@9555: if (legendTheme != null && getItemsPerThemeType(themeType).size() > this.aggregationThreshold) { gernotbelger@9555: gernotbelger@9555: final String labelDescription = Resources.getMsg(context.getMeta(), legendTheme.getDescription()); gernotbelger@9555: final String labelMerged = Resources.getMsg(context.getMeta(), I10N_MERGED); gernotbelger@9555: final String label = String.format("%s (%s)", labelDescription, labelMerged); gernotbelger@9555: gernotbelger@9555: final List items = findDistinctItems(getItemsPerThemeType(themeType)); gernotbelger@9555: /* add items for each distinct shape, only the last one gets the label */ gernotbelger@9555: for (final Iterator iterator = items.iterator(); iterator.hasNext();) { gernotbelger@9555: gernotbelger@9555: final LegendItem legendItem = iterator.next(); gernotbelger@9555: final String itemLabel = iterator.hasNext() ? "," : label; gernotbelger@9555: gernotbelger@9555: /* create and add aggregated item(s) */ gernotbelger@9555: final LegendItem aggregatedItem = createAggregatedItem(legendItem, legendTheme, itemLabel); gernotbelger@9555: aggregatedItems.add(aggregatedItem); gernotbelger@9555: } gernotbelger@9555: gernotbelger@9555: /* mark as handles */ gernotbelger@9555: aggregatedTypes.add(themeType); gernotbelger@9555: gernotbelger@9555: } else { gernotbelger@9555: /* simply add normal items */ gernotbelger@9555: aggregatedItems.add(item); gernotbelger@9555: } gernotbelger@9555: } gernotbelger@9555: gernotbelger@9555: plot.setFixedLegendItems(aggregatedItems); gernotbelger@9555: gernotbelger@9555: this.itemsPerThemeType.clear(); gernotbelger@9555: this.legendItems.clear(); gernotbelger@9555: } gernotbelger@9555: gernotbelger@9555: /** gernotbelger@9555: * Extract distinct items, curently only those that are different regarding their shape gernotbelger@9555: */ gernotbelger@9555: private List findDistinctItems(final List items) { gernotbelger@9555: gernotbelger@9555: // HACKY: we hash by unique shape, because we know that the used shapes are cashed in a static cache. gernotbelger@9555: final Map shapeMap = new IdentityHashMap<>(); gernotbelger@9555: gernotbelger@9555: for (final LegendItem item : items) { gernotbelger@9555: gernotbelger@9555: final Shape shape = item.isShapeVisible() ? item.getShape() : null; gernotbelger@9555: if (!shapeMap.containsKey(shape)) gernotbelger@9555: shapeMap.put(shape, item); gernotbelger@9555: } gernotbelger@9555: gernotbelger@9555: return new ArrayList<>(shapeMap.values()); gernotbelger@9555: } gernotbelger@9555: gernotbelger@9555: private LegendItem createAggregatedItem(final LegendItem item, final Theme legendTheme, final String label) { gernotbelger@9555: /* clone properties from current item */ gernotbelger@9555: final String description = item.getDescription(); gernotbelger@9555: final String tooltipText = item.getToolTipText(); gernotbelger@9555: final String urlText = item.getURLText(); gernotbelger@9555: final boolean shapeVisible = item.isShapeVisible(); gernotbelger@9555: final Shape shape = item.getShape(); gernotbelger@9555: final boolean shapeFilled = item.isShapeFilled(); gernotbelger@9555: final Paint fillPaint = item.getFillPaint(); gernotbelger@9555: final boolean shapeOutlineVisible = item.isShapeOutlineVisible(); gernotbelger@9555: final Paint outlinePaint = item.getOutlinePaint(); gernotbelger@9555: final Stroke outlineStroke = item.getOutlineStroke(); gernotbelger@9555: final boolean lineVisible = item.isLineVisible(); gernotbelger@9555: final Shape line = item.getLine(); gernotbelger@9555: final Stroke lineStroke = item.getLineStroke(); gernotbelger@9555: final Paint linePaint = item.getLinePaint(); gernotbelger@9555: final LegendItem aggregatedItem = new LegendItem(label, description, tooltipText, urlText, shapeVisible, shape, shapeFilled, fillPaint, gernotbelger@9555: shapeOutlineVisible, outlinePaint, outlineStroke, lineVisible, line, lineStroke, linePaint); gernotbelger@9555: gernotbelger@9555: aggregatedItem.setDataset(item.getDataset()); gernotbelger@9555: aggregatedItem.setDatasetIndex(item.getDatasetIndex()); gernotbelger@9555: aggregatedItem.setFillPaintTransformer(item.getFillPaintTransformer()); gernotbelger@9555: aggregatedItem.setLabelFont(item.getLabelFont()); gernotbelger@9555: aggregatedItem.setLabelPaint(item.getLabelPaint()); gernotbelger@9555: aggregatedItem.setSeriesIndex(item.getSeriesIndex()); gernotbelger@9555: gernotbelger@9555: /* let styled dataset apply specific theme configuration */ gernotbelger@9555: applyThemeToLegend(aggregatedItem, legendTheme); gernotbelger@9555: gernotbelger@9555: return aggregatedItem; gernotbelger@9555: } gernotbelger@9555: gernotbelger@9555: private void applyThemeToLegend(final LegendItem item, final Theme legendTheme) { gernotbelger@9555: gernotbelger@9555: final Dataset dataset = item.getDataset(); gernotbelger@9555: if (dataset == null) gernotbelger@9555: return; gernotbelger@9555: gernotbelger@9555: final Document xml = legendTheme.toXML(); gernotbelger@9555: final ThemeDocument themeDocument = new ThemeDocument(xml); gernotbelger@9555: gernotbelger@9555: if (dataset instanceof StyledXYDataset) { gernotbelger@9555: final StyledXYDataset styledDataset = (StyledXYDataset) dataset; gernotbelger@9555: styledDataset.applyAggregatedLegendTheme(item, themeDocument); gernotbelger@9555: return; gernotbelger@9555: } gernotbelger@9555: gernotbelger@9555: if (dataset instanceof XYSeriesCollection) { gernotbelger@9555: gernotbelger@9555: final int seriesIndex = item.getSeriesIndex(); gernotbelger@9555: gernotbelger@9555: final XYSeriesCollection seriesCollection = (XYSeriesCollection) dataset; gernotbelger@9555: if (seriesIndex >= 0 && seriesIndex < seriesCollection.getSeriesCount()) { gernotbelger@9555: final XYSeries series = seriesCollection.getSeries(seriesIndex); gernotbelger@9555: gernotbelger@9555: if (series instanceof StyledSeries) { gernotbelger@9555: ((StyledSeries) series).applyAggregatedLegendTheme(item, themeDocument); gernotbelger@9555: } gernotbelger@9555: } gernotbelger@9555: } gernotbelger@9555: } gernotbelger@9555: gernotbelger@9555: private Theme getLegendTheme(final CallContext context, final String themeType) { gernotbelger@9555: gernotbelger@9555: final RiverContext flysContext = context instanceof RiverContext ? (RiverContext) context : (RiverContext) context.globalContext(); gernotbelger@9555: gernotbelger@9555: return ThemeFactory.getLegendTheme(flysContext, themeType); gernotbelger@9555: } gernotbelger@9555: }