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

http://dive4elements.wald.intevation.org