changeset 450:20a480753ff9

Render labels in vertical cross section charts. gnv-artifacts/trunk@498 c6561f87-3c4e-4783-a992-168aeb5c3f6f
author Sascha L. Teichmann <sascha.teichmann@intevation.de>
date Mon, 04 Jan 2010 02:49:42 +0000
parents c7ca2fce041f
children bc5901bb4525
files gnv-artifacts/ChangeLog gnv-artifacts/src/main/java/de/intevation/gnv/chart/VerticalCrossSectionChart.java gnv-artifacts/src/main/java/de/intevation/gnv/jfreechart/LevelOrderIndices.java gnv-artifacts/src/main/java/de/intevation/gnv/jfreechart/PolygonPlot.java gnv-artifacts/src/main/java/de/intevation/gnv/jfreechart/PolygonRenderer.java gnv-artifacts/src/main/java/de/intevation/gnv/jfreechart/PolygonSeriesLabelGenerator.java
diffstat 6 files changed, 277 insertions(+), 19 deletions(-) [+]
line wrap: on
line diff
--- a/gnv-artifacts/ChangeLog	Sun Jan 03 15:57:02 2010 +0000
+++ b/gnv-artifacts/ChangeLog	Mon Jan 04 02:49:42 2010 +0000
@@ -1,3 +1,41 @@
+2010-01-04	Sascha L. Teichmann	<sascha.teichmann@intevation.de>
+
+	* src/main/java/de/intevation/gnv/jfreechart/PolygonSeriesLabelGenerator.java:
+	  New. Interface to generate labels for polygon series.
+
+	* src/main/java/de/intevation/gnv/jfreechart/PolygonRenderer.java:
+	  Added logic to generate and render labels of polygons.
+	  The implemented layout algorithm is greedy. For all
+	  polygons with labels the label is placed on the center of
+	  the ring, which center in terms of the indices of the vertices.
+	  If the bounding box of the label intersects the bounding box
+	  of a an already placed one alternative places are tried. 
+	  In level order positions at 1/4, 3/4, 1/8, 3/8, 5/8, 7/8 and 
+	  so on are evaluated for non intersections with former placed labels.
+	  This terminates if a free place is found or all index positions
+	  are exhausted. If no free position is found the label is omitted.
+
+	  The visual result is okay but could be improved by a more clever
+	  algorithm e.g. tension reduction in the graph of labels.
+
+	  TODO: Improve clipping against chart borders.
+
+	* src/main/java/de/intevation/gnv/jfreechart/LevelOrderIndices.java:
+	  New. Little helper class to generate the level order index
+	  traversal used in the label layout. Placed in the jfreechart
+	  package to keep it clean from gnv dependencies.
+
+	* src/main/java/de/intevation/gnv/jfreechart/PolygonPlot.java:
+	  Added some methods to access the domain and range axis and
+	  there edge position. Mainly C&P from JFreeCahrts XYPlot
+	  to ease the coordinate transformation between Java2D and
+	  the value spaces of the chart. Call the label generation
+	  of the polygon renderer.
+
+	* src/main/java/de/intevation/gnv/chart/VerticalCrossSectionChart.java:
+	  Added an implementation of PolygonSeriesLabelGenerator to
+	  generate localized labels for the iso lines.
+	  
 2010-01-03	Sascha L. Teichmann	<sascha.teichmann@intevation.de>
 
 	* src/main/java/de/intevation/gnv/chart/VerticalCrossSectionChart.java:
--- a/gnv-artifacts/src/main/java/de/intevation/gnv/chart/VerticalCrossSectionChart.java	Sun Jan 03 15:57:02 2010 +0000
+++ b/gnv-artifacts/src/main/java/de/intevation/gnv/chart/VerticalCrossSectionChart.java	Mon Jan 04 02:49:42 2010 +0000
@@ -2,6 +2,8 @@
 
 import java.util.Locale;
 
+import java.text.NumberFormat;
+
 import java.awt.Color;
 import java.awt.Paint;
 
@@ -54,6 +56,25 @@
         }
     } // class PalettePaintLookup
 
+    public static class LocalizedLabelGenerator
+    extends             PolygonRenderer.DefaultLabelGenerator
+    {
+        protected NumberFormat format;
+
+        public LocalizedLabelGenerator() {
+        }
+
+        public LocalizedLabelGenerator(NumberFormat format) {
+            this.format = format;
+        }
+
+        protected String toString(Object label) {
+            return label instanceof Number
+                ? format.format(((Number)label).doubleValue())
+                : super.toString(label);
+        }
+    } // class LocalizedLabelGenerator
+
     protected JFreeChart chart;
 
     protected AttributedXYColumns columns;
@@ -95,8 +116,13 @@
             }
         }
 
+        NumberFormat format = NumberFormat.getInstance(locale);
+        format.setMinimumFractionDigits(0);
+        format.setMaximumFractionDigits(2);
+
         PolygonRenderer renderer = new PolygonRenderer(
-            new PalettePaintLookup(palette));
+            new PalettePaintLookup(palette),
+            new LocalizedLabelGenerator(format));
 
         ValueAxis domainAxis = new NumberAxis(xAxis);
         ValueAxis rangeAxis  = new NumberAxis(yAxis);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gnv-artifacts/src/main/java/de/intevation/gnv/jfreechart/LevelOrderIndices.java	Mon Jan 04 02:49:42 2010 +0000
@@ -0,0 +1,58 @@
+package de.intevation.gnv.jfreechart;
+
+import java.util.LinkedList;
+
+/**
+ * @author Sascha L. Teichmann (sascha.teichmann@intevation.de)
+ */
+public class LevelOrderIndices
+{
+    public interface Visitor {
+        Object visit(int index);
+    }
+
+    protected int from;
+    protected int to;
+
+    public LevelOrderIndices() {
+    }
+
+    public LevelOrderIndices(int to) {
+        this(0, to);
+    }
+
+    public LevelOrderIndices(int from, int to) {
+        this.from = Math.min(from, to);
+        this.to   = Math.max(from, to);
+    }
+
+    public Object visit(Visitor visitor) {
+        LinkedList<int[]> queue = new LinkedList<int[]>();
+
+        queue.add(new int [] { from, to });
+
+        while (!queue.isEmpty()) {
+            int [] pair = queue.remove();
+
+            int mid = (pair[0] + pair[1]) >> 1;
+
+            Object result = visitor.visit(mid);
+
+            if (result != null) {
+                return result;
+            }
+
+            if (mid-1 >= pair[0]) {
+                queue.add(new int [] { pair[0], mid-1 });
+            }
+
+            if (mid+1 <= pair[1]) {
+                pair[0] = mid+1;
+                queue.add(pair);
+            }
+        }
+
+        return null;
+    }
+}
+// vim:set ts=4 sw=4 si et sta sts=4 fenc=utf-8 :
--- a/gnv-artifacts/src/main/java/de/intevation/gnv/jfreechart/PolygonPlot.java	Sun Jan 03 15:57:02 2010 +0000
+++ b/gnv-artifacts/src/main/java/de/intevation/gnv/jfreechart/PolygonPlot.java	Mon Jan 04 02:49:42 2010 +0000
@@ -123,6 +123,25 @@
         }
     }
 
+    public ValueAxis getDomainAxis() {
+        return getDomainAxis(0);
+    }
+
+    public ValueAxis getDomainAxis(int index) {
+        return index < domainAxes.size()
+            ? (ValueAxis)domainAxes.get(index)
+            : null;
+    }
+
+    public ValueAxis getRangeAxis() {
+        return getRangeAxis(0);
+    }
+
+    public ValueAxis getRangeAxis(int index) {
+        return index < rangeAxes.size()
+            ? (ValueAxis)rangeAxes.get(index)
+            : null;
+    }
 
     public  void configureRangeAxis() {
         // we just have 1 dataset
@@ -191,6 +210,7 @@
         if (!isEmptyOrNull(dataset)) {
             // draw data
             drawPolygons(savedG2, dataArea, info);
+            drawLabels(savedG2, dataArea, info);
         }
 
         g2.setClip(savedClip);
@@ -270,12 +290,20 @@
     }
 
 
+    private void drawLabels(
+        Graphics2D        g2,
+        Rectangle2D       area,
+        PlotRenderingInfo info
+    ) {
+        renderer.drawLabels(g2, this, area, dataset);
+    }
+
     private void drawPolygons(
         Graphics2D        g2,
         Rectangle2D       area,
         PlotRenderingInfo info
     ) {
-        renderer.draw(g2, area, dataset);
+        renderer.drawPolygons(g2, area, dataset);
     }
 
 
@@ -330,14 +358,14 @@
         return space;
     }
 
-    private RectangleEdge getDomainAxisEdge() {
+    public RectangleEdge getDomainAxisEdge() {
         return Plot.resolveDomainAxisLocation(
             getDomainAxisLocation(), orientation
         );
     }
 
 
-    private RectangleEdge getDomainAxisEdge(int idx) {
+    public RectangleEdge getDomainAxisEdge(int idx) {
         AxisLocation  location = getDomainAxisLocation(idx);
         RectangleEdge result   = Plot.resolveDomainAxisLocation(
             location, orientation
@@ -350,14 +378,14 @@
     }
 
 
-    private RectangleEdge getRangeAxisEdge() {
+    public RectangleEdge getRangeAxisEdge() {
         return Plot.resolveRangeAxisLocation(
             getRangeAxisLocation(), orientation
         );
     }
 
 
-    private RectangleEdge getRangeAxisEdge(int idx) {
+    public RectangleEdge getRangeAxisEdge(int idx) {
         AxisLocation  location = getRangeAxisLocation(idx);
         RectangleEdge result   = Plot.resolveRangeAxisLocation(
             location,
--- a/gnv-artifacts/src/main/java/de/intevation/gnv/jfreechart/PolygonRenderer.java	Sun Jan 03 15:57:02 2010 +0000
+++ b/gnv-artifacts/src/main/java/de/intevation/gnv/jfreechart/PolygonRenderer.java	Mon Jan 04 02:49:42 2010 +0000
@@ -1,54 +1,83 @@
 package de.intevation.gnv.jfreechart;
 
+import java.util.ArrayList;
+
 import java.awt.Color;
 import java.awt.Graphics2D;
 import java.awt.Paint;
 import java.awt.Shape;
 import java.awt.BasicStroke;
+import java.awt.FontMetrics;
+import java.awt.Font;
 
 import java.awt.geom.GeneralPath;
 import java.awt.geom.Rectangle2D;
 import java.awt.geom.Rectangle2D.Double;
+import java.awt.geom.AffineTransform;
 
 import org.jfree.data.Range;
 
+import org.jfree.chart.axis.ValueAxis;
+
+import org.jfree.ui.RectangleEdge;
+
 import org.apache.log4j.Logger;
 
+import org.jfree.text.TextUtilities;
+
+
 /**
- * @author Ingo Weinzierl <ingo.weinzierl@intevation.de>
+ * @author Ingo Weinzierl      (ingo.weinzierl@intevation.de)
+ * @author Sascha L. Teichmann (sascha.teichmann@intevation.de)
  */
 public class PolygonRenderer
 {
 	private static Logger log = Logger.getLogger(
 		PolygonRenderer.class);
 
-    public static final int AREA           = 1;
-    public static final int LINES          = 2;
-    public static final int AREA_AND_LINES = AREA | LINES;
-
     public interface PaintLookup {
 
         Paint getPaint(int index);
 
     } // interface PaintLookup
 
-    protected int type;
+    public static class DefaultLabelGenerator
+    implements          PolygonSeriesLabelGenerator
+    {
+        public DefaultLabelGenerator() {
+        }
 
-    protected PaintLookup lookup;
+        public String generateLabel(PolygonSeries series) {
+            Object label = series.getAttribute("label");
+            return label != null
+                ? toString(label)
+                : null;
+        }
 
-    protected PolygonPlot plot;
+        protected String toString(Object label) {
+            return label.toString();
+        }
+    } // class DefaultLabelGenerator
 
+    public static final PolygonSeriesLabelGenerator 
+        DEFAULT_LABEL_GENERATOR_INSTANCE = new DefaultLabelGenerator();
+
+    protected PaintLookup                 lookup;
+    protected PolygonSeriesLabelGenerator labelGenerator;
 
     public PolygonRenderer(PaintLookup lookup) {
-        this(lookup, AREA);
+        this(lookup, null);
     }
 
-    public PolygonRenderer(PaintLookup lookup, int type) {
-        this.lookup = lookup;
-        this.type   = type;
+    public PolygonRenderer(
+        PaintLookup                 lookup, 
+        PolygonSeriesLabelGenerator labelGenerator
+    ) {
+        this.lookup         = lookup;
+        this.labelGenerator = labelGenerator;
     }
 
-    public void draw(
+    public void drawPolygons(
         Graphics2D     graphics,
         Rectangle2D    rectangle,
         PolygonDataset dataset
@@ -66,6 +95,8 @@
             sy = -sy; // mirror
         }
 
+        AffineTransform xform = graphics.getTransform();
+
         graphics.translate(tx, ty);
         graphics.scale(sx, sy);
 
@@ -88,6 +119,73 @@
                 graphics.draw(constructShape(series, false));
             }
         }
+
+        graphics.setTransform(xform);
+    }
+
+    public void drawLabels(
+        final Graphics2D  graphics,
+        final PolygonPlot plot,
+        final Rectangle2D area,
+        PolygonDataset    dataset
+    ) {
+        if (labelGenerator == null) {
+            return;
+        }
+
+        final ArrayList<Rectangle2D> bboxes = new ArrayList<Rectangle2D>();
+
+        Font font = graphics.getFont();
+        font = font.deriveFont(Font.PLAIN, Math.max(8, font.getSize()-3));
+        graphics.setFont(font);
+        FontMetrics metrics = graphics.getFontMetrics(font);
+
+        for (int i = dataset.getSeriesCount()-1; i >= 0; --i) {
+            PolygonSeries series = dataset.getSeries(i);
+
+            String label = labelGenerator.generateLabel(series);
+            if (label == null) {
+                continue;
+            }
+
+            final Rectangle2D box = TextUtilities.getTextBounds(
+                label, graphics, metrics);
+
+            for (int j = series.getItemCount()-1; j >= 0; --j) {
+                final CompactXYItems ring = series.getItem(j);
+                LevelOrderIndices loi = new LevelOrderIndices(ring.size()-1);
+                Rectangle2D r = (Rectangle2D)loi.visit(
+                    new LevelOrderIndices.Visitor() 
+                {
+                    ValueAxis          da = plot.getDomainAxis();
+                    ValueAxis          ra = plot.getRangeAxis();
+                    RectangleEdge      de = plot.getDomainAxisEdge();
+                    RectangleEdge      re = plot.getRangeAxisEdge();
+                    Rectangle2D.Double r  = new Rectangle2D.Double(
+                        0d, 0d, box.getWidth(), box.getHeight());
+
+                    public Object visit(int index) {
+                        r.x = da.valueToJava2D(ring.getX(index), area, de) 
+                            - 0.5*box.getWidth();
+                        r.y = ra.valueToJava2D(ring.getY(index), area, re)
+                            + 0.5*box.getHeight();
+
+                        for (Rectangle2D b: bboxes) {
+                            if (b.intersects(r)) {
+                                return null;
+                            }
+                        }
+                        return r;
+                    }
+                });
+
+                if (r != null) {
+                    bboxes.add(r);
+                    graphics.drawString(
+                        label, (float)r.getX(), (float)r.getY());
+                }
+            } // for all items in series
+        } // for all series
     }
 
     protected Shape constructShape(PolygonSeries series, boolean close) {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gnv-artifacts/src/main/java/de/intevation/gnv/jfreechart/PolygonSeriesLabelGenerator.java	Mon Jan 04 02:49:42 2010 +0000
@@ -0,0 +1,10 @@
+package de.intevation.gnv.jfreechart;
+
+/**
+ * @author Sascha L. Teichmann (sascha.teichmann@intevation.de)
+ */
+public interface PolygonSeriesLabelGenerator
+{
+    String generateLabel(PolygonSeries series);
+}
+// vim:set ts=4 sw=4 si et sta sts=4 fenc=utf-8 :

http://dive4elements.wald.intevation.org