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