1 package org.opentrafficsim.swing.gui;
2
3 import java.awt.FontMetrics;
4 import java.awt.Graphics;
5 import java.awt.Graphics2D;
6 import java.awt.RenderingHints;
7 import java.awt.geom.Rectangle2D;
8 import java.util.ArrayList;
9 import java.util.Hashtable;
10 import java.util.LinkedHashMap;
11 import java.util.LinkedHashSet;
12 import java.util.List;
13 import java.util.Objects;
14 import java.util.function.BiFunction;
15 import java.util.stream.IntStream;
16
17 import javax.swing.JLabel;
18 import javax.swing.event.ChangeEvent;
19 import javax.swing.event.ChangeListener;
20
21 import org.djutils.exceptions.Throw;
22 import org.djutils.swing.multislider.LinearMultiSlider;
23
24
25
26
27
28
29
30
31
32
33
34
35 public class ProbabilityDistributionEditor<T> extends LinearMultiSlider<Double>
36 {
37
38
39 private static final long serialVersionUID = 20250916L;
40
41
42 private final int valuesPerPercent;
43
44
45 private final List<T> categories;
46
47
48 private BiFunction<T, Double, String> labelFunction = (t, p) -> String.format("%s: %.1f%%", t, p * 100.0);
49
50
51 private float categoryFontSize = 10.0f;
52
53
54
55
56
57
58
59 public ProbabilityDistributionEditor(final List<T> categories, final double[] probabilities, final int valuesPerPercent)
60 {
61 super(0.0, 1.0, 100 * valuesPerPercent + 1, checkValues(probabilities));
62 Throw.whenNull(categories, "categories");
63 Throw.whenNull(probabilities, "probabilities");
64 Throw.when(categories.size() != new LinkedHashSet<>(categories).size(), IllegalArgumentException.class,
65 "The categories are not unique.");
66 Throw.when(valuesPerPercent < 1, IllegalArgumentException.class, "valuesPerPercent should be at least 1.");
67 this.categories = new ArrayList<>(categories);
68 this.valuesPerPercent = 100 * valuesPerPercent;
69
70 setLabelTable(new Hashtable<Integer, JLabel>(IntStream.range(0, 11).collect(() -> new LinkedHashMap<>(),
71 (m, i) -> m.put(i * 10 * valuesPerPercent, new JLabel((i * 10) + "%")), (m1, m2) -> m1.putAll(m2))));
72 setMajorTickSpacing(5 * valuesPerPercent);
73 setPaintTicks(true);
74 setOverlap(true);
75 setPaintTrack(false);
76 this.addChangeListener(new ChangeListener()
77 {
78 @Override
79 public void stateChanged(final ChangeEvent e)
80 {
81
82 repaint();
83 }
84 });
85 }
86
87
88
89
90
91
92 private static Double[] checkValues(final double[] probabilities)
93 {
94 Double[] out = new Double[probabilities.length - 1];
95 double cumul = 0.0;
96 for (int i = 0; i < probabilities.length - 1; i++)
97 {
98 Throw.when(probabilities[i] < 0.0, IllegalArgumentException.class, "Probabilities should not be negative.");
99 cumul += probabilities[i];
100 out[i] = cumul;
101 }
102 Throw.when(Math.abs(cumul + probabilities[probabilities.length - 1] - 1.0) > 1e-9, IllegalArgumentException.class,
103 "Probabilities do not add up to one.");
104 return out;
105 }
106
107 @Override
108 protected Double mapIndexToValue(final int index)
109 {
110 return ((double) index) / this.valuesPerPercent;
111 }
112
113
114
115
116
117 public void setCategoryLabelFunction(final BiFunction<T, Double, String> labelfunction)
118 {
119 Throw.whenNull(labelfunction, "labelfunction");
120 this.labelFunction = labelfunction;
121 }
122
123
124
125
126
127 public void setCategoryFontSize(final float categoryFontSize)
128 {
129 Throw.when(categoryFontSize <= 0.0, IllegalArgumentException.class, "Category font size should be larger than 0.");
130 this.categoryFontSize = categoryFontSize;
131 }
132
133
134 @Override
135 public void paint(final Graphics g)
136 {
137 super.paint(g);
138 Graphics2D g2 = (Graphics2D) g;
139
140
141 int[] edges = new int[getNumberOfThumbs() + 2];
142 edges[0] = getTrackSizeLoPx();
143 for (int i = 0; i < getNumberOfThumbs(); i++)
144 {
145 edges[i + 1] = thumbPositionPx(i);
146 }
147 edges[edges.length - 1] = getTrackSizeLoPx() + trackSize();
148
149
150 g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
151 g2.setFont(g2.getFont().deriveFont(this.categoryFontSize));
152 FontMetrics fontMetric = g2.getFontMetrics();
153 for (int i = 0; i < edges.length - 1; i++)
154 {
155 int x = (edges[i] + edges[i + 1]) / 2;
156 String categoryLabel;
157 try
158 {
159 categoryLabel = this.labelFunction.apply(this.categories.get(i), getProbability(i));
160 }
161 catch (Exception e)
162 {
163 categoryLabel = this.categories.get(i).toString();
164 }
165 Rectangle2D d = fontMetric.getStringBounds(categoryLabel, g2);
166 int left = (int) (x - d.getWidth() / 2.0);
167 if (left < 0)
168 {
169 g2.drawString(categoryLabel, 0, fontMetric.getHeight());
170 }
171 else if (left + d.getWidth() > getWidth())
172 {
173 g2.drawString(categoryLabel, (int) (getWidth() - d.getWidth()), fontMetric.getHeight());
174 }
175 else
176 {
177 g2.drawString(categoryLabel, left, fontMetric.getHeight());
178 }
179 }
180 }
181
182
183
184
185
186 public double[] getProbabilities()
187 {
188 double[] result = new double[this.categories.size()];
189 for (int i = 0; i < this.categories.size(); i++)
190 {
191 result[i] = getProbability(i);
192 }
193 return result;
194 }
195
196
197
198
199
200
201
202 public double getProbability(final T t)
203 {
204 Throw.when(!this.categories.contains(t), IllegalArgumentException.class, "Category {} is not part of the distribution.",
205 t);
206 return getProbability(this.categories.indexOf(t));
207 }
208
209
210
211
212
213
214
215 public double getProbability(final int i)
216 {
217 Objects.checkIndex(i, this.categories.size());
218 if (i == 0)
219 {
220 return getValue(0);
221 }
222 else if (i == this.categories.size() - 1)
223 {
224 return 1.0 - getValue(this.categories.size() - 2);
225 }
226 return getValue(i) - getValue(i - 1);
227 }
228
229 }