view 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 source
/** 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);
    }
}

http://dive4elements.wald.intevation.org