Mercurial > dive4elements > river
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 } |