comparison 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
comparison
equal deleted inserted replaced
9554:33ce8eba9806 9555:ef5754ba5573
1 /** Copyright (C) 2017 by Bundesanstalt für Gewässerkunde
2 * Software engineering by
3 * Björnsen Beratende Ingenieure GmbH
4 * Dr. Schumacher Ingenieurbüro für Wasser und Umwelt
5 *
6 * This file is Free Software under the GNU AGPL (>=v3)
7 * and comes with ABSOLUTELY NO WARRANTY! Check out the
8 * documentation coming with Dive4Elements River for details.
9 */
10 package org.dive4elements.river.exports;
11
12 import java.awt.Font;
13 import java.awt.Paint;
14 import java.awt.Shape;
15 import java.awt.Stroke;
16 import java.util.AbstractMap.SimpleEntry;
17 import java.util.ArrayList;
18 import java.util.HashMap;
19 import java.util.HashSet;
20 import java.util.IdentityHashMap;
21 import java.util.Iterator;
22 import java.util.List;
23 import java.util.Map;
24 import java.util.Map.Entry;
25 import java.util.Set;
26
27 import org.dive4elements.artifacts.CallContext;
28 import org.dive4elements.river.artifacts.context.RiverContext;
29 import org.dive4elements.river.artifacts.resources.Resources;
30 import org.dive4elements.river.jfree.StyledSeries;
31 import org.dive4elements.river.jfree.StyledXYDataset;
32 import org.dive4elements.river.themes.Theme;
33 import org.dive4elements.river.themes.ThemeDocument;
34 import org.dive4elements.river.themes.ThemeFactory;
35 import org.jfree.chart.LegendItem;
36 import org.jfree.chart.LegendItemCollection;
37 import org.jfree.chart.plot.XYPlot;
38 import org.jfree.data.general.Dataset;
39 import org.jfree.data.xy.XYSeries;
40 import org.jfree.data.xy.XYSeriesCollection;
41 import org.w3c.dom.Document;
42
43 /**
44 * Builds and adds legend entries to the plot.
45 *
46 * @author Gernot Belger
47 */
48 public class LegendAggregator {
49
50 private static final String NULL_THEME_TYPE = ""; //$NON-NLS-1$
51
52 private static final String I10N_MERGED = "legend.aggregator.merged"; //$NON-NLS-1$
53
54 private final List<Map.Entry<LegendItem, String>> legendItems = new ArrayList<>();
55
56 private final Map<String, List<LegendItem>> itemsPerThemeType = new HashMap<>();
57
58 private final Font labelFont;
59
60 private final int aggregationThreshold;
61
62 public LegendAggregator(final int aggregationThreshold, final Font labelFont) {
63 this.aggregationThreshold = aggregationThreshold;
64 this.labelFont = labelFont;
65 }
66
67 public void addLegendItem(final String themeType, final LegendItem item) {
68
69 item.setLabelFont(this.labelFont);
70
71 this.legendItems.add(new SimpleEntry<>(item, themeType));
72
73 final List<LegendItem> perThemeType = getItemsPerThemeType(themeType);
74 perThemeType.add(item);
75 }
76
77 private List<LegendItem> getItemsPerThemeType(final String themeType) {
78
79 final String key = themeType == null ? NULL_THEME_TYPE : themeType;
80
81 if (!this.itemsPerThemeType.containsKey(key))
82 this.itemsPerThemeType.put(key, new ArrayList<LegendItem>());
83
84 return this.itemsPerThemeType.get(key);
85 }
86
87 /**
88 * Apply the gathered items to the plot. Use only once.
89 */
90 public void apply(final CallContext context, final XYPlot plot) {
91
92 final LegendItemCollection aggregatedItems = new LegendItemCollection();
93
94 /* marker set for types that are already aggregated */
95 final Set<String> aggregatedTypes = new HashSet<>();
96
97 for (final Entry<LegendItem, String> entry : this.legendItems) {
98
99 final LegendItem item = entry.getKey();
100 final String themeType = entry.getValue();
101
102 /* ignore already aggregated items */
103 if (aggregatedTypes.contains(themeType))
104 continue;
105
106 /* aggregate known types if count over threshold */
107 final Theme legendTheme = getLegendTheme(context, themeType);
108
109 if (legendTheme != null && getItemsPerThemeType(themeType).size() > this.aggregationThreshold) {
110
111 final String labelDescription = Resources.getMsg(context.getMeta(), legendTheme.getDescription());
112 final String labelMerged = Resources.getMsg(context.getMeta(), I10N_MERGED);
113 final String label = String.format("%s (%s)", labelDescription, labelMerged);
114
115 final List<LegendItem> items = findDistinctItems(getItemsPerThemeType(themeType));
116 /* add items for each distinct shape, only the last one gets the label */
117 for (final Iterator<LegendItem> iterator = items.iterator(); iterator.hasNext();) {
118
119 final LegendItem legendItem = iterator.next();
120 final String itemLabel = iterator.hasNext() ? "," : label;
121
122 /* create and add aggregated item(s) */
123 final LegendItem aggregatedItem = createAggregatedItem(legendItem, legendTheme, itemLabel);
124 aggregatedItems.add(aggregatedItem);
125 }
126
127 /* mark as handles */
128 aggregatedTypes.add(themeType);
129
130 } else {
131 /* simply add normal items */
132 aggregatedItems.add(item);
133 }
134 }
135
136 plot.setFixedLegendItems(aggregatedItems);
137
138 this.itemsPerThemeType.clear();
139 this.legendItems.clear();
140 }
141
142 /**
143 * Extract distinct items, curently only those that are different regarding their shape
144 */
145 private List<LegendItem> findDistinctItems(final List<LegendItem> items) {
146
147 // HACKY: we hash by unique shape, because we know that the used shapes are cashed in a static cache.
148 final Map<Shape, LegendItem> shapeMap = new IdentityHashMap<>();
149
150 for (final LegendItem item : items) {
151
152 final Shape shape = item.isShapeVisible() ? item.getShape() : null;
153 if (!shapeMap.containsKey(shape))
154 shapeMap.put(shape, item);
155 }
156
157 return new ArrayList<>(shapeMap.values());
158 }
159
160 private LegendItem createAggregatedItem(final LegendItem item, final Theme legendTheme, final String label) {
161 /* clone properties from current item */
162 final String description = item.getDescription();
163 final String tooltipText = item.getToolTipText();
164 final String urlText = item.getURLText();
165 final boolean shapeVisible = item.isShapeVisible();
166 final Shape shape = item.getShape();
167 final boolean shapeFilled = item.isShapeFilled();
168 final Paint fillPaint = item.getFillPaint();
169 final boolean shapeOutlineVisible = item.isShapeOutlineVisible();
170 final Paint outlinePaint = item.getOutlinePaint();
171 final Stroke outlineStroke = item.getOutlineStroke();
172 final boolean lineVisible = item.isLineVisible();
173 final Shape line = item.getLine();
174 final Stroke lineStroke = item.getLineStroke();
175 final Paint linePaint = item.getLinePaint();
176 final LegendItem aggregatedItem = new LegendItem(label, description, tooltipText, urlText, shapeVisible, shape, shapeFilled, fillPaint,
177 shapeOutlineVisible, outlinePaint, outlineStroke, lineVisible, line, lineStroke, linePaint);
178
179 aggregatedItem.setDataset(item.getDataset());
180 aggregatedItem.setDatasetIndex(item.getDatasetIndex());
181 aggregatedItem.setFillPaintTransformer(item.getFillPaintTransformer());
182 aggregatedItem.setLabelFont(item.getLabelFont());
183 aggregatedItem.setLabelPaint(item.getLabelPaint());
184 aggregatedItem.setSeriesIndex(item.getSeriesIndex());
185
186 /* let styled dataset apply specific theme configuration */
187 applyThemeToLegend(aggregatedItem, legendTheme);
188
189 return aggregatedItem;
190 }
191
192 private void applyThemeToLegend(final LegendItem item, final Theme legendTheme) {
193
194 final Dataset dataset = item.getDataset();
195 if (dataset == null)
196 return;
197
198 final Document xml = legendTheme.toXML();
199 final ThemeDocument themeDocument = new ThemeDocument(xml);
200
201 if (dataset instanceof StyledXYDataset) {
202 final StyledXYDataset styledDataset = (StyledXYDataset) dataset;
203 styledDataset.applyAggregatedLegendTheme(item, themeDocument);
204 return;
205 }
206
207 if (dataset instanceof XYSeriesCollection) {
208
209 final int seriesIndex = item.getSeriesIndex();
210
211 final XYSeriesCollection seriesCollection = (XYSeriesCollection) dataset;
212 if (seriesIndex >= 0 && seriesIndex < seriesCollection.getSeriesCount()) {
213 final XYSeries series = seriesCollection.getSeries(seriesIndex);
214
215 if (series instanceof StyledSeries) {
216 ((StyledSeries) series).applyAggregatedLegendTheme(item, themeDocument);
217 }
218 }
219 }
220 }
221
222 private Theme getLegendTheme(final CallContext context, final String themeType) {
223
224 final RiverContext flysContext = context instanceof RiverContext ? (RiverContext) context : (RiverContext) context.globalContext();
225
226 return ThemeFactory.getLegendTheme(flysContext, themeType);
227 }
228 }

http://dive4elements.wald.intevation.org