comparison artifacts/src/main/java/org/dive4elements/river/jfree/StableXYDifferenceRenderer.java @ 9186:eec4df8165a1

Implemented 'ShowLineLabel' for area themes.
author gernotbelger
date Thu, 28 Jun 2018 10:47:04 +0200
parents 77eb4553245b
children 6b2496d71936
comparison
equal deleted inserted replaced
9185:4778333ea2cd 9186:eec4df8165a1
120 import org.jfree.chart.renderer.xy.AbstractXYItemRenderer; 120 import org.jfree.chart.renderer.xy.AbstractXYItemRenderer;
121 import org.jfree.chart.renderer.xy.XYItemRendererState; 121 import org.jfree.chart.renderer.xy.XYItemRendererState;
122 import org.jfree.chart.urls.XYURLGenerator; 122 import org.jfree.chart.urls.XYURLGenerator;
123 import org.jfree.data.xy.DefaultXYDataset; 123 import org.jfree.data.xy.DefaultXYDataset;
124 import org.jfree.data.xy.XYDataset; 124 import org.jfree.data.xy.XYDataset;
125 import org.jfree.data.xy.XYSeries;
126 import org.jfree.data.xy.XYSeriesCollection;
125 import org.jfree.io.SerialUtilities; 127 import org.jfree.io.SerialUtilities;
126 import org.jfree.ui.RectangleEdge; 128 import org.jfree.ui.RectangleEdge;
127 import org.jfree.util.PaintUtilities; 129 import org.jfree.util.PaintUtilities;
128 import org.jfree.util.PublicCloneable; 130 import org.jfree.util.PublicCloneable;
129 import org.jfree.util.ShapeUtilities; 131 import org.jfree.util.ShapeUtilities;
174 /** The shape to display in the legend item. */ 176 /** The shape to display in the legend item. */
175 private transient Shape legendShape; 177 private transient Shape legendShape;
176 178
177 private final boolean drawOriginalSeries; 179 private final boolean drawOriginalSeries;
178 180
181 /** NumberFormat to use for area. */
182 private NumberFormat areaLabelNumberFormat;
183
184 private int areaCalculationMode;
185
186 private double positiveArea;
187
188 private double negativeArea;
189
179 /** The color of the label showing the calculated area. */ 190 /** The color of the label showing the calculated area. */
180 private Color labelColor; 191 private Color labelColor;
181 192
182 /** The background color of the label showing the calculated area. */ 193 /** The background color of the label showing the calculated area. */
183 private Color labelBGColor; 194 private Color labelBGColor;
184 195
185 /** Font to draw label of calculated area with. */ 196 /** Font to draw label of calculated area with. */
186 private Font labelFont; 197 private Font labelFont;
187 198
199 /** Whether or not to draw a label that shows the title of the theme. */
200 private boolean drawTitleLabel = false;
201
202 /** Whether or not to draw a label that shows the area of the polygon. */
203 private boolean drawAreaLabel = false;
204
188 /** Template to create i18ned label for area. */ 205 /** Template to create i18ned label for area. */
189 private String areaLabelTamplate; 206 private String areaLabelTamplate;
190
191 /** NumberFormat to use for area. */
192 private NumberFormat areaLabelNumberFormat;
193
194 private int areaCalculationMode;
195
196 private double positiveArea;
197
198 private double negativeArea;
199
200 /** Whether or not to draw a label in the area. */
201 private boolean labelArea = true;
202 207
203 /** Arithmetic centroid of drawn polygons. */ 208 /** Arithmetic centroid of drawn polygons. */
204 private Point2D.Double centroid; 209 private Point2D.Double centroid;
205 210
206 /** Number of points that contributed to the centroid. */ 211 /** Number of points that contributed to the centroid. */
214 * vector graphics formats such as SVG and PDF). 219 * vector graphics formats such as SVG and PDF).
215 * 220 *
216 * @since 1.0.4 221 * @since 1.0.4
217 */ 222 */
218 private final boolean roundXCoordinates; 223 private final boolean roundXCoordinates;
224
225 /** Holds the minimal x value in screen coordinates, will updated during the draw operation */
226 private transient double minimumScreenX = Double.POSITIVE_INFINITY;
227
228 /** Holds the y value in screen coordinates at the minimum x value **/
229 private transient double minimumScreenX_Y = Double.POSITIVE_INFINITY;
219 230
220 /** 231 /**
221 * Creates a new renderer with default attributes. 232 * Creates a new renderer with default attributes.
222 */ 233 */
223 public StableXYDifferenceRenderer() { 234 public StableXYDifferenceRenderer() {
268 279
269 public void setAreaLabelNumberFormat(final NumberFormat nf) { 280 public void setAreaLabelNumberFormat(final NumberFormat nf) {
270 this.areaLabelNumberFormat = nf; 281 this.areaLabelNumberFormat = nf;
271 } 282 }
272 283
273 public void setLabelArea(final boolean label) { 284 public void setShowAreaLabel(final boolean doDrawAreaLabel) {
274 this.labelArea = label; 285 this.drawAreaLabel = doDrawAreaLabel;
286 }
287
288 public void setShowTitleLabel(final boolean doDrawTitleLabel) {
289 this.drawTitleLabel = doDrawTitleLabel;
275 } 290 }
276 291
277 /** Set font to paint label with. */ 292 /** Set font to paint label with. */
278 public void setLabelFont(final Font font) { 293 public void setLabelFont(final Font font) {
279 this.labelFont = font; 294 this.labelFont = font;
809 */ 824 */
810 @Override 825 @Override
811 public void drawItem(final Graphics2D g2, final XYItemRendererState state, final Rectangle2D dataArea, final PlotRenderingInfo info, final XYPlot plot, 826 public void drawItem(final Graphics2D g2, final XYItemRendererState state, final Rectangle2D dataArea, final PlotRenderingInfo info, final XYPlot plot,
812 final ValueAxis domainAxis, final ValueAxis rangeAxis, final XYDataset dataset, final int series, final int item, 827 final ValueAxis domainAxis, final ValueAxis rangeAxis, final XYDataset dataset, final int series, final int item,
813 final CrosshairState crosshairState, final int pass) { 828 final CrosshairState crosshairState, final int pass) {
829
814 switch (pass) { 830 switch (pass) {
815 case 0: 831 case 0:
816 for (final XYDataset ds : splitByNaNs(dataset)) { 832 for (final XYDataset ds : splitByNaNs(dataset)) {
817 drawItemPass0(g2, dataArea, info, plot, domainAxis, rangeAxis, ds, series, item, crosshairState); 833 drawItemPass0(g2, dataArea, info, plot, domainAxis, rangeAxis, ds, series, item, crosshairState);
818 } 834 }
819 break; 835 break;
820 case 1: 836 case 1:
821 drawItemPass1(g2, dataArea, info, plot, domainAxis, rangeAxis, dataset, series, item, crosshairState); 837 drawItemPass1(g2, dataArea, info, plot, domainAxis, rangeAxis, dataset, series, item, crosshairState);
822 } 838 break;
823
824 // Find geometric middle, calculate area and paint
825 // a string with it here.
826 if (pass == 1 && this.labelArea && this.areaLabelNumberFormat != null && this.areaLabelTamplate != null) {
827 double center_x = this.centroid.getX();
828 double center_y = this.centroid.getY();
829 center_x = domainAxis.valueToJava2D(center_x, dataArea, plot.getDomainAxisEdge());
830 center_y = rangeAxis.valueToJava2D(center_y, dataArea, plot.getRangeAxisEdge());
831
832 // Respect text-extend if text should appear really centered.
833
834 float area = 0f;
835 if (this.areaCalculationMode == CALCULATE_POSITIVE_AREA || this.areaCalculationMode == CALCULATE_ALL_AREA) {
836 area += Math.abs(this.positiveArea);
837 }
838 if (this.areaCalculationMode == CALCULATE_NEGATIVE_AREA || this.areaCalculationMode == CALCULATE_ALL_AREA) {
839 area += Math.abs(this.negativeArea);
840 }
841 if (area != 0f) {
842 final Color oldColor = g2.getColor();
843 final Font oldFont = g2.getFont();
844 g2.setFont(this.labelFont);
845 final String labelText = String.format(this.areaLabelTamplate, this.areaLabelNumberFormat.format(area));
846 if (this.labelBGColor != null) {
847 EnhancedLineAndShapeRenderer.drawTextBox(g2, labelText, (float) center_x, (float) center_y, this.labelBGColor);
848 }
849 g2.setColor(this.labelColor);
850 g2.drawString(labelText, (float) center_x, (float) center_y);
851 g2.setFont(oldFont);
852 g2.setColor(oldColor);
853 }
854 } 839 }
855 } 840 }
856 841
857 /** 842 /**
858 * Draws the visual representation of a single data item, first pass. 843 * Draws the visual representation of a single data item, first pass.
882 */ 867 */
883 private void drawItemPass0(final Graphics2D x_graphics, final Rectangle2D x_dataArea, final PlotRenderingInfo x_info, final XYPlot x_plot, 868 private void drawItemPass0(final Graphics2D x_graphics, final Rectangle2D x_dataArea, final PlotRenderingInfo x_info, final XYPlot x_plot,
884 final ValueAxis x_domainAxis, final ValueAxis x_rangeAxis, final XYDataset x_dataset, final int x_series, final int x_item, 869 final ValueAxis x_domainAxis, final ValueAxis x_rangeAxis, final XYDataset x_dataset, final int x_series, final int x_item,
885 final CrosshairState x_crosshairState) { 870 final CrosshairState x_crosshairState) {
886 871
887 if (!((0 == x_series) && (0 == x_item))) { 872 if (x_series != 0 || x_item != 0)
888 return; 873 return;
889 }
890 874
891 final boolean b_impliedZeroSubtrahend = (1 == x_dataset.getSeriesCount()); 875 final boolean b_impliedZeroSubtrahend = (1 == x_dataset.getSeriesCount());
892 876
893 // check if either series is a degenerate case (i.e. less than 2 points) 877 // check if either series is a degenerate case (i.e. less than 2 points)
894 if (isEitherSeriesDegenerate(x_dataset, b_impliedZeroSubtrahend)) { 878 if (isEitherSeriesDegenerate(x_dataset, b_impliedZeroSubtrahend))
895 return; 879 return;
896 }
897 880
898 // check if series are disjoint (i.e. domain-spans do not overlap) 881 // check if series are disjoint (i.e. domain-spans do not overlap)
899 if (!b_impliedZeroSubtrahend && areSeriesDisjoint(x_dataset)) { 882 if (!b_impliedZeroSubtrahend && areSeriesDisjoint(x_dataset))
900 return; 883 return;
901 }
902 884
903 // polygon definitions 885 // polygon definitions
904 final List<Double> l_minuendXs = new LinkedList<>(); 886 final List<Double> l_minuendXs = new LinkedList<>();
905 final List<Double> l_minuendYs = new LinkedList<>(); 887 final List<Double> l_minuendYs = new LinkedList<>();
906 final List<Double> l_subtrahendXs = new LinkedList<>(); 888 final List<Double> l_subtrahendXs = new LinkedList<>();
1264 l_polygonYs.addAll(l_subtrahendYs); 1246 l_polygonYs.addAll(l_subtrahendYs);
1265 1247
1266 // create an actual polygon 1248 // create an actual polygon
1267 b_positive = (l_subtrahendMaxY <= l_minuendMaxY) && (l_subtrahendMinY <= l_minuendMinY); 1249 b_positive = (l_subtrahendMaxY <= l_minuendMaxY) && (l_subtrahendMinY <= l_minuendMinY);
1268 createPolygon(x_graphics, x_dataArea, x_plot, x_domainAxis, x_rangeAxis, b_positive, l_polygonXs, l_polygonYs); 1250 createPolygon(x_graphics, x_dataArea, x_plot, x_domainAxis, x_rangeAxis, b_positive, l_polygonXs, l_polygonYs);
1251 }
1252
1253 private void drawItemPass1(final Graphics2D g2, final Rectangle2D dataArea, final PlotRenderingInfo info, final XYPlot plot, final ValueAxis domainAxis,
1254 final ValueAxis rangeAxis, final XYDataset dataset, final int series, final int item, final CrosshairState crosshairState) {
1255
1256 doDrawItemPass1(g2, dataArea, info, plot, domainAxis, rangeAxis, dataset, series, item, crosshairState);
1257
1258 final int lastSeries = dataset.getSeriesCount() - 1;
1259 final int lastItem = dataset.getItemCount(series) - 1;
1260 if (series == lastSeries && item == lastItem) {
1261 // draw labels: only once per theme!
1262 drawAreaLabel(g2, dataArea, plot, domainAxis, rangeAxis);
1263 drawTitleLabel(g2, dataArea, info.getOwner().getEntityCollection(), dataset);
1264 }
1265 }
1266
1267 private void drawAreaLabel(final Graphics2D g2, final Rectangle2D dataArea, final XYPlot plot, final ValueAxis domainAxis, final ValueAxis rangeAxis) {
1268
1269 if (!this.drawAreaLabel)
1270 return;
1271
1272 if (this.areaLabelNumberFormat == null || this.areaLabelTamplate == null || this.centroid == null)
1273 return;
1274
1275 // Respect text-extend if text should appear really centered.
1276 float area = 0f;
1277 if (this.areaCalculationMode == CALCULATE_POSITIVE_AREA || this.areaCalculationMode == CALCULATE_ALL_AREA)
1278 area += Math.abs(this.positiveArea);
1279 if (this.areaCalculationMode == CALCULATE_NEGATIVE_AREA || this.areaCalculationMode == CALCULATE_ALL_AREA)
1280 area += Math.abs(this.negativeArea);
1281
1282 if (area != 0f) {
1283
1284 final String labelText = String.format(this.areaLabelTamplate, this.areaLabelNumberFormat.format(area));
1285
1286 final double center_x = this.centroid.getX();
1287 final double center_y = this.centroid.getY();
1288
1289 final double screenX = domainAxis.valueToJava2D(center_x, dataArea, plot.getDomainAxisEdge());
1290 final double screenY = rangeAxis.valueToJava2D(center_y, dataArea, plot.getRangeAxisEdge());
1291
1292 drawLabel(g2, labelText, screenX, screenY);
1293 }
1294 }
1295
1296 private void drawTitleLabel(final Graphics2D g2, final Rectangle2D dataArea, final EntityCollection entities, final XYDataset dataset) {
1297
1298 if (!this.drawTitleLabel)
1299 return;
1300
1301 if (Double.isInfinite(this.minimumScreenX))
1302 return;
1303
1304 if (dataset instanceof XYSeriesCollection) {
1305 final XYSeries xYSeries = ((XYSeriesCollection) dataset).getSeries(0);
1306 final String label = (xYSeries instanceof HasLabel) ? ((HasLabel) xYSeries).getLabel() : xYSeries.getKey().toString();
1307
1308 EnhancedLineAndShapeRenderer.drawLineLabel(g2, dataArea, entities, this.minimumScreenX, this.minimumScreenX_Y, this.labelFont, this.labelColor,
1309 this.labelBGColor != null, this.labelBGColor, label);
1310 }
1269 } 1311 }
1270 1312
1271 /** 1313 /**
1272 * Draws the visual representation of a single data item, second pass. In 1314 * Draws the visual representation of a single data item, second pass. In
1273 * the second pass, the renderer draws the lines and shapes for the 1315 * the second pass, the renderer draws the lines and shapes for the
1294 * the item index (zero-based). 1336 * the item index (zero-based).
1295 * @param x_crosshairState 1337 * @param x_crosshairState
1296 * crosshair information for the plot 1338 * crosshair information for the plot
1297 * (<code>null</code> permitted). 1339 * (<code>null</code> permitted).
1298 */ 1340 */
1299 private void drawItemPass1(final Graphics2D x_graphics, final Rectangle2D x_dataArea, final PlotRenderingInfo x_info, final XYPlot x_plot, 1341 private void doDrawItemPass1(final Graphics2D x_graphics, final Rectangle2D x_dataArea, final PlotRenderingInfo x_info, final XYPlot x_plot,
1300 final ValueAxis x_domainAxis, final ValueAxis x_rangeAxis, final XYDataset x_dataset, final int x_series, final int x_item, 1342 final ValueAxis x_domainAxis, final ValueAxis x_rangeAxis, final XYDataset x_dataset, final int x_series, final int x_item,
1301 final CrosshairState x_crosshairState) { 1343 final CrosshairState x_crosshairState) {
1302 1344
1303 Shape l_entityArea = null; 1345 Shape l_entityArea = null;
1304 EntityCollection l_entities = null; 1346 EntityCollection l_entities = null;
1305 if (null != x_info) { 1347 if (x_info != null)
1306 l_entities = x_info.getOwner().getEntityCollection(); 1348 l_entities = x_info.getOwner().getEntityCollection();
1307 }
1308 1349
1309 final Paint l_seriesPaint = getItemPaint(x_series, x_item); 1350 final Paint l_seriesPaint = getItemPaint(x_series, x_item);
1310 final Stroke l_seriesStroke = getItemStroke(x_series, x_item); 1351 final Stroke l_seriesStroke = getItemStroke(x_series, x_item);
1311 x_graphics.setPaint(l_seriesPaint); 1352 x_graphics.setPaint(l_seriesPaint);
1312 x_graphics.setStroke(l_seriesStroke); 1353 x_graphics.setStroke(l_seriesStroke);
1317 1358
1318 final double l_x0 = x_dataset.getXValue(x_series, x_item); 1359 final double l_x0 = x_dataset.getXValue(x_series, x_item);
1319 final double l_y0 = x_dataset.getYValue(x_series, x_item); 1360 final double l_y0 = x_dataset.getYValue(x_series, x_item);
1320 final double l_x1 = x_domainAxis.valueToJava2D(l_x0, x_dataArea, l_domainAxisLocation); 1361 final double l_x1 = x_domainAxis.valueToJava2D(l_x0, x_dataArea, l_domainAxisLocation);
1321 final double l_y1 = x_rangeAxis.valueToJava2D(l_y0, x_dataArea, l_rangeAxisLocation); 1362 final double l_y1 = x_rangeAxis.valueToJava2D(l_y0, x_dataArea, l_rangeAxisLocation);
1363
1364 /* update minimumScreenX -> used to position title */
1365 // REMARK: ignore points with y == 0.0 --> else the label sticks on the zero-line, becaue most area themes start at 0.0
1366 if (l_x1 < this.minimumScreenX && Math.abs(l_y1) > 0.01) {
1367 this.minimumScreenX = l_x1;
1368 this.minimumScreenX_Y = l_y1;
1369 }
1322 1370
1323 // These are the shapes of the series items. 1371 // These are the shapes of the series items.
1324 if (getShapesVisible()) { 1372 if (getShapesVisible()) {
1325 Shape l_shape = getItemShape(x_series, x_item); 1373 Shape l_shape = getItemShape(x_series, x_item);
1326 if (l_orientation == PlotOrientation.HORIZONTAL) { 1374 if (l_orientation == PlotOrientation.HORIZONTAL) {
1342 } 1390 }
1343 l_entityArea = l_shape; 1391 l_entityArea = l_shape;
1344 } // if (getShapesVisible()) 1392 } // if (getShapesVisible())
1345 1393
1346 // add an entity for the item... 1394 // add an entity for the item...
1347 if (null != l_entities) { 1395 if (l_entities != null) {
1348 if (null == l_entityArea) { 1396 if (l_entityArea == null)
1349 l_entityArea = new Rectangle2D.Double((l_x1 - 2), (l_y1 - 2), 4, 4); 1397 l_entityArea = new Rectangle2D.Double((l_x1 - 2), (l_y1 - 2), 4, 4);
1350 } 1398
1351 String l_tip = null; 1399 String l_tip = null;
1352 final XYToolTipGenerator l_tipGenerator = getToolTipGenerator(x_series, x_item); 1400 final XYToolTipGenerator l_tipGenerator = getToolTipGenerator(x_series, x_item);
1353 if (null != l_tipGenerator) { 1401 if (null != l_tipGenerator) {
1354 l_tip = l_tipGenerator.generateToolTip(x_dataset, x_series, x_item); 1402 l_tip = l_tipGenerator.generateToolTip(x_dataset, x_series, x_item);
1355 } 1403 }
1361 final XYItemEntity l_entity = new XYItemEntity(l_entityArea, x_dataset, x_series, x_item, l_tip, l_url); 1409 final XYItemEntity l_entity = new XYItemEntity(l_entityArea, x_dataset, x_series, x_item, l_tip, l_url);
1362 l_entities.add(l_entity); 1410 l_entities.add(l_entity);
1363 } 1411 }
1364 1412
1365 // draw the item label if there is one... 1413 // draw the item label if there is one...
1366 if (isItemLabelVisible(x_series, x_item)) { 1414 if (isItemLabelVisible(x_series, x_item))
1367 drawItemLabel(x_graphics, l_orientation, x_dataset, x_series, x_item, l_x1, l_y1, (l_y1 < 0.0)); 1415 drawItemLabel(x_graphics, l_orientation, x_dataset, x_series, x_item, l_x1, l_y1, (l_y1 < 0.0));
1368 }
1369 1416
1370 final int l_domainAxisIndex = x_plot.getDomainAxisIndex(x_domainAxis); 1417 final int l_domainAxisIndex = x_plot.getDomainAxisIndex(x_domainAxis);
1371 final int l_rangeAxisIndex = x_plot.getRangeAxisIndex(x_rangeAxis); 1418 final int l_rangeAxisIndex = x_plot.getRangeAxisIndex(x_rangeAxis);
1372 updateCrosshairValues(x_crosshairState, l_x0, l_y0, l_domainAxisIndex, l_rangeAxisIndex, l_x1, l_y1, l_orientation); 1419 updateCrosshairValues(x_crosshairState, l_x0, l_y0, l_domainAxisIndex, l_rangeAxisIndex, l_x1, l_y1, l_orientation);
1373 1420
1377 1424
1378 final double l_x2 = x_domainAxis.valueToJava2D(x_dataset.getXValue(x_series, (x_item - 1)), x_dataArea, l_domainAxisLocation); 1425 final double l_x2 = x_domainAxis.valueToJava2D(x_dataset.getXValue(x_series, (x_item - 1)), x_dataArea, l_domainAxisLocation);
1379 final double l_y2 = x_rangeAxis.valueToJava2D(x_dataset.getYValue(x_series, (x_item - 1)), x_dataArea, l_rangeAxisLocation); 1426 final double l_y2 = x_rangeAxis.valueToJava2D(x_dataset.getYValue(x_series, (x_item - 1)), x_dataArea, l_rangeAxisLocation);
1380 1427
1381 Line2D l_line = null; 1428 Line2D l_line = null;
1382 if (PlotOrientation.HORIZONTAL == l_orientation) { 1429 if (PlotOrientation.HORIZONTAL == l_orientation)
1383 l_line = new Line2D.Double(l_y1, l_x1, l_y2, l_x2); 1430 l_line = new Line2D.Double(l_y1, l_x1, l_y2, l_x2);
1384 } else if (PlotOrientation.VERTICAL == l_orientation) { 1431 else if (PlotOrientation.VERTICAL == l_orientation)
1385 l_line = new Line2D.Double(l_x1, l_y1, l_x2, l_y2); 1432 l_line = new Line2D.Double(l_x1, l_y1, l_x2, l_y2);
1386 }
1387 1433
1388 if ((null != l_line) && l_line.intersects(x_dataArea)) { 1434 if ((null != l_line) && l_line.intersects(x_dataArea)) {
1389 x_graphics.setPaint(getItemPaint(x_series, x_item)); 1435 x_graphics.setPaint(getItemPaint(x_series, x_item));
1390 x_graphics.setStroke(getItemStroke(x_series, x_item)); 1436 x_graphics.setStroke(getItemStroke(x_series, x_item));
1391 if (this.drawOriginalSeries) { 1437 if (this.drawOriginalSeries) {
1718 stream.defaultReadObject(); 1764 stream.defaultReadObject();
1719 this.positivePaint = SerialUtilities.readPaint(stream); 1765 this.positivePaint = SerialUtilities.readPaint(stream);
1720 this.negativePaint = SerialUtilities.readPaint(stream); 1766 this.negativePaint = SerialUtilities.readPaint(stream);
1721 this.legendShape = SerialUtilities.readShape(stream); 1767 this.legendShape = SerialUtilities.readShape(stream);
1722 } 1768 }
1769
1770 private void drawLabel(final Graphics2D g2, final String labelText, final double screenX, final double screenY) {
1771
1772 final Color oldColor = g2.getColor();
1773 final Font oldFont = g2.getFont();
1774
1775 g2.setFont(this.labelFont);
1776 if (this.labelBGColor != null)
1777 EnhancedLineAndShapeRenderer.drawTextBox(g2, labelText, (float) screenX, (float) screenY, this.labelBGColor);
1778
1779 g2.setColor(this.labelColor);
1780 g2.drawString(labelText, (float) screenX, (float) screenY);
1781
1782 g2.setFont(oldFont);
1783 g2.setColor(oldColor);
1784 }
1723 } 1785 }

http://dive4elements.wald.intevation.org