changeset 3162:0d8146989012

Added labeling for Q/W points FixingsKMChartService. flys-artifacts/trunk@4774 c6561f87-3c4e-4783-a992-168aeb5c3f6f
author Sascha L. Teichmann <sascha.teichmann@intevation.de>
date Mon, 25 Jun 2012 11:38:33 +0000
parents cd8d81b2824d
children dad513d5ce37
files flys-artifacts/ChangeLog flys-artifacts/src/main/java/de/intevation/flys/artifacts/services/FixingsKMChartService.java flys-artifacts/src/main/java/de/intevation/flys/artifacts/services/QWSeriesCollection.java flys-artifacts/src/main/java/de/intevation/flys/java2d/ShapeUtils.java flys-artifacts/src/main/java/de/intevation/flys/jfree/ShapeRenderer.java flys-artifacts/src/main/java/de/intevation/flys/utils/Formatter.java
diffstat 6 files changed, 733 insertions(+), 26 deletions(-) [+]
line wrap: on
line diff
--- a/flys-artifacts/ChangeLog	Mon Jun 25 07:59:22 2012 +0000
+++ b/flys-artifacts/ChangeLog	Mon Jun 25 11:38:33 2012 +0000
@@ -1,3 +1,21 @@
+2012-06-25	Sascha L. Teichmann	<sascha.teichmann@intevation.de>
+
+	* src/main/java/de/intevation/flys/artifacts/services/FixingsKMChartService.java:
+	  Label the points in diagram and show if they are interpolated or not.
+
+	* src/main/java/de/intevation/flys/artifacts/services/QWSeriesCollection.java:
+	  New. Extended XYSeriesCollection to cope with QWs
+
+	* src/main/java/de/intevation/flys/utils/Formatter.java: Added formatters
+	  to be fetched only over CallMeta. CallContext are not present in services.
+	  
+	* src/main/java/de/intevation/flys/java2d/ShapeUtils.java: New. Some code
+	  to handle Shapes.
+
+	* src/main/java/de/intevation/flys/jfree/ShapeRenderer.java: New. Shape
+	  renderer. This is a simplified version of the shape renderer 
+	  from fixings analysis in desktop FLYS.
+	
 2012-06-25	Sascha L. Teichmann	<sascha.teichmann@intevation.de>
 
 	* src/main/java/de/intevation/flys/artifacts/model/fixings/FixCalculation.java:
--- a/flys-artifacts/src/main/java/de/intevation/flys/artifacts/services/FixingsKMChartService.java	Mon Jun 25 07:59:22 2012 +0000
+++ b/flys-artifacts/src/main/java/de/intevation/flys/artifacts/services/FixingsKMChartService.java	Mon Jun 25 11:38:33 2012 +0000
@@ -18,17 +18,20 @@
 import de.intevation.flys.artifacts.model.GaugeFinderFactory;
 import de.intevation.flys.artifacts.model.GaugeRange;
 
+import de.intevation.flys.artifacts.model.fixings.QW;
+
 import de.intevation.flys.backend.SessionHolder;
 
+import de.intevation.flys.utils.Formatter;
 import de.intevation.flys.utils.Pair;
 
-import gnu.trove.TDoubleArrayList;
-
 import java.awt.BasicStroke;
 import java.awt.Color;
 import java.awt.Dimension;
 import java.awt.Transparency;
 
+import java.awt.geom.Rectangle2D;
+
 import java.awt.image.BufferedImage;
 
 import java.io.ByteArrayOutputStream;
@@ -44,13 +47,16 @@
 import org.jfree.chart.ChartFactory;
 import org.jfree.chart.ChartUtilities;
 import org.jfree.chart.JFreeChart;
+import org.jfree.chart.LegendItemCollection;
+
+import org.jfree.chart.axis.NumberAxis;
 
 import org.jfree.chart.plot.Marker;
 import org.jfree.chart.plot.PlotOrientation;
 import org.jfree.chart.plot.ValueMarker;
 import org.jfree.chart.plot.XYPlot;
 
-import org.jfree.data.xy.DefaultXYDataset;
+import org.jfree.data.Range;
 
 import org.jfree.ui.RectangleAnchor;
 import org.jfree.ui.TextAnchor;
@@ -177,7 +183,7 @@
             }
         }
 
-        JFreeChart chart = createChart(cols, river, km);
+        JFreeChart chart = createChart(cols, river, km, callMeta);
 
         return encode(chart, extent, format);
     }
@@ -207,42 +213,66 @@
 
     protected static JFreeChart createChart(
         List<Pair<Fixing.Column, FixingsColumn>> cols,
-        String river,
-        double km
+        String      river,
+        double      km,
+        CallMeta    callMeta
     ) {
-
-        TDoubleArrayList ws = new TDoubleArrayList(cols.size());
-        TDoubleArrayList qs = new TDoubleArrayList(cols.size());
+        // TODO: I18N
+        QWSeriesCollection dataset = new QWSeriesCollection();
 
         double [] w = new double[1];
         for (Pair<Fixing.Column, FixingsColumn> col: cols) {
-            boolean interpolated = col.getB().getW(km, w);
-            // TODO: Do something special with the interpolated values.
+            boolean interpolated = !col.getB().getW(km, w);
             double q = col.getB().getQ(km);
             if (!Double.isNaN(w[0]) && !Double.isNaN(q)) {
-                ws.add(w[0]);
-                qs.add(q);
-                // TODO: Generate labels depending on sectors.
+                QW qw = new QW(
+                    q, w[0],
+                    col.getA().getDescription(),
+                    col.getA().getStartTime(),
+                    interpolated);
+                dataset.add(qw);
             }
         }
 
-        DefaultXYDataset dataset = new DefaultXYDataset();
-
-        dataset.addSeries(
-            "Fixierungen", // TODO: i18n
-            new double [][] { qs.toNativeArray(), ws.toNativeArray() });
-
-        JFreeChart chart = ChartFactory.createScatterPlot(
+        JFreeChart chart = ChartFactory.createXYLineChart(
             "Fixierungen " + river + ": km " + km, // TODO: i18n
-            "Q", // TODO: i18n
-            "W", // TODO: i18n
-            dataset,
+            "Q [m\u00b3/s]",
+            "W [NN + m]",
+            null,
             PlotOrientation.VERTICAL,
             true,
-            false,
+            true,
             false);
 
-        applyQSectorMarkers(chart.getXYPlot(), river, km);
+        XYPlot plot = (XYPlot)chart.getPlot();
+
+        NumberAxis qA = (NumberAxis)plot.getDomainAxis();
+        qA.setNumberFormatOverride(Formatter.getWaterlevelQ(callMeta));
+
+        NumberAxis wA = (NumberAxis)plot.getRangeAxis();
+        wA.setNumberFormatOverride(Formatter.getWaterlevelW(callMeta));
+
+        plot.setRenderer(0, dataset.createRenderer());
+        plot.setDataset(0, dataset);
+
+        Rectangle2D area = dataset.getArea();
+
+        if (area != null) {
+            double wInset = area.getHeight() * 0.25d;
+            log.debug("width: " + area.getWidth());
+            log.debug("height: "+ area.getHeight());
+
+            wA.setAutoRangeIncludesZero(false);
+            wA.setRange(new Range(
+                area.getMinY() - wInset,
+                area.getMaxY() + wInset));
+        }
+
+        LegendItemCollection lic = plot.getLegendItems();
+        dataset.addLegendItems(lic);
+        plot.setFixedLegendItems(lic);
+
+        applyQSectorMarkers(plot, river, km);
 
         ChartUtilities.applyCurrentTheme(chart);
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/flys-artifacts/src/main/java/de/intevation/flys/artifacts/services/QWSeriesCollection.java	Mon Jun 25 11:38:33 2012 +0000
@@ -0,0 +1,222 @@
+package de.intevation.flys.artifacts.services;
+
+import de.intevation.flys.artifacts.model.fixings.QW;
+
+import de.intevation.flys.java2d.ShapeUtils;
+
+import de.intevation.flys.jfree.ShapeRenderer;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Paint;
+import java.awt.Shape;
+
+import java.awt.geom.Rectangle2D;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.jfree.chart.LegendItem;
+import org.jfree.chart.LegendItemCollection;
+
+import org.jfree.chart.labels.XYItemLabelGenerator;
+
+import org.jfree.chart.renderer.xy.StandardXYItemRenderer;
+
+import org.jfree.data.xy.XYDataset;
+import org.jfree.data.xy.XYSeries;
+import org.jfree.data.xy.XYSeriesCollection;
+
+public class QWSeriesCollection
+extends      XYSeriesCollection
+implements   XYItemLabelGenerator
+{
+    public interface LabelGenerator {
+        String createLabel(QW qw);
+    } // interface LabelGenerator
+
+    public static class DateFormatLabelGenerator
+    implements          LabelGenerator
+    {
+        protected DateFormat format;
+
+        public DateFormatLabelGenerator() {
+            this(new SimpleDateFormat("dd.MM.yyyy"));
+        }
+
+        public DateFormatLabelGenerator(DateFormat format) {
+            this.format = format;
+        }
+
+        @Override
+        public String createLabel(QW qw) {
+            Date date = qw.getDate();
+            return date != null ? format.format(date) : "";
+        }
+    } // class DateFormatLabelGenerator
+
+    public static final LabelGenerator SIMPLE_GENERATOR =
+        new DateFormatLabelGenerator();
+
+    protected Date minDate;
+    protected Date maxDate;
+
+    protected List<List<QW>> labels;
+
+    protected Rectangle2D area;
+
+    protected LabelGenerator labelGenerator;
+
+    protected Map<ShapeRenderer.Entry, Integer> knownShapes =
+        new HashMap<ShapeRenderer.Entry, Integer>();
+
+    public QWSeriesCollection() {
+        labels = new ArrayList<List<QW>>();
+        labelGenerator = SIMPLE_GENERATOR;
+    }
+
+    public QWSeriesCollection(LabelGenerator labelGenerator) {
+        this();
+        this.labelGenerator = labelGenerator;
+    }
+
+    protected static ShapeRenderer.Entry classify(QW qw) {
+        Shape shape = qw.getInterpolated()
+            ? ShapeUtils.INTERPOLATED_SHAPE
+            : ShapeUtils.MEASURED_SHAPE;
+
+        boolean filled = !qw.getInterpolated();
+
+        return new ShapeRenderer.Entry(
+            shape, Color.black, filled);
+    }
+
+    public void add(QW qw) {
+
+        ShapeRenderer.Entry key = classify(qw);
+
+        Integer seriesNo = knownShapes.get(key);
+
+        XYSeries series;
+
+        if (seriesNo == null) {
+            seriesNo = Integer.valueOf(getSeriesCount());
+            knownShapes.put(key, seriesNo);
+            series = new XYSeries(seriesNo, false);
+            addSeries(series);
+            labels.add(new ArrayList<QW>());
+        }
+        else {
+            series = getSeries(seriesNo);
+        }
+
+        series.add(qw.getQ(), qw.getW());
+
+        labels.get(seriesNo).add(qw);
+
+        extendDateRange(qw);
+        extendArea(qw);
+    }
+
+    protected void extendDateRange(QW qw) {
+        Date date = qw.getDate();
+        if (date != null) {
+            if (minDate == null) {
+                minDate = maxDate = date;
+            }
+            else {
+                if (date.compareTo(minDate) < 0) {
+                    minDate = date;
+                }
+                if (date.compareTo(maxDate) > 0) {
+                    maxDate = date;
+                }
+            }
+        }
+    }
+
+    protected void extendArea(QW qw) {
+        if (area == null) {
+            area = new Rectangle2D.Double(
+                qw.getQ(), qw.getW(), 0d, 0d);
+        }
+        else {
+            area.add(qw.getQ(), qw.getW());
+        }
+    }
+
+    public Rectangle2D getArea() {
+        return area;
+    }
+
+    public Date getMinDate() {
+        return minDate;
+    }
+
+    public Date getMaxDate() {
+        return maxDate;
+    }
+
+    public LabelGenerator getLabelGenerator() {
+        return labelGenerator;
+    }
+
+    @Override
+    public String generateLabel(XYDataset dataset, int series, int item) {
+        return labelGenerator.createLabel(labels.get(series).get(item));
+    }
+
+    public StandardXYItemRenderer createRenderer() {
+        StandardXYItemRenderer renderer = new ShapeRenderer(knownShapes);
+        renderer.setBaseItemLabelGenerator(this);
+        renderer.setBaseSeriesVisibleInLegend(false);
+        renderer.setBaseItemLabelsVisible(true);
+        return renderer;
+    }
+
+    public static final LegendItem legendItem(
+        String  label,
+        Paint   paint,
+        Shape   shape,
+        boolean filled
+    ) {
+        BasicStroke stroke = new BasicStroke();
+        return new LegendItem(
+            label,  // label
+            null,   // description
+            null,   // tooltip
+            null,   // url
+            true,   // shape visible
+            shape,  // shape
+            filled, // shape filled
+            filled ? paint : Color.white, // fill paint
+            true,   // shape outline
+            paint,  // outline paint
+            stroke, // outline stroke
+            false,  // line visible
+            shape,  // line
+            stroke, // line stroke
+            Color.white);
+    }
+
+    public void addLegendItems(LegendItemCollection lic) {
+        for (ShapeRenderer.Entry entry: knownShapes.keySet()) {
+            // TODO: i18n
+            String label = entry.getFilled()
+                ? "gemessene Werte"
+                : "interpolierte Werte";
+            lic.add(legendItem(
+                label,
+                entry.getPaint(),
+                entry.getShape(),
+                entry.getFilled()));
+        }
+    }
+}
+// vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 :
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/flys-artifacts/src/main/java/de/intevation/flys/java2d/ShapeUtils.java	Mon Jun 25 11:38:33 2012 +0000
@@ -0,0 +1,83 @@
+package de.intevation.flys.java2d;
+
+import java.awt.Shape;
+
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Ellipse2D;
+import java.awt.geom.GeneralPath;
+import java.awt.geom.Rectangle2D;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class ShapeUtils
+{
+    // TODO: Use enum
+    public static final int MEASURED     = 0;
+    public static final int DIGITIZED    = 1;
+    public static final int INTERPOLATED = 2;
+
+    public static final boolean DIGITIZED_FILL    = false;
+    public static final boolean MEASURED_FILL     = true;
+    public static final boolean INTERPOLATED_FILL = false;
+
+   public static final Shape DIGITIZED_SHAPE =
+        createCross(4f);
+
+    public static final Shape MEASURED_SHAPE =
+        new Rectangle2D.Double(-2, -2, 4, 4);
+
+    public static final Shape INTERPOLATED_SHAPE =
+        new Ellipse2D.Double(-2, -2, 4, 4);
+
+    protected static Map<Long, Shape> scaledShapesCache =
+        new HashMap<Long, Shape>();
+
+    public static final Shape createCross(float size) {
+        float half = size * 0.5f;
+        GeneralPath p = new GeneralPath();
+        p.moveTo(-half, -half);
+        p.lineTo(half, half);
+        p.closePath();
+        p.moveTo(-half, half);
+        p.lineTo(half, -half);
+        p.closePath();
+        return p;
+    }
+
+    public static Shape scale(Shape shape, float factor) {
+        if (factor == 1f) {
+            return shape;
+        }
+        AffineTransform xform =
+            AffineTransform.getScaleInstance(factor, factor);
+
+        GeneralPath gp = new GeneralPath(shape);
+        return gp.createTransformedShape(xform);
+    }
+
+    public static synchronized Shape getScaledShape(int type, float size) {
+
+        Long hash = Long.valueOf(
+            (((long)type) << 32) | Float.floatToIntBits(size));
+
+        Shape shape = scaledShapesCache.get(hash);
+
+        if (shape == null) {
+            switch (type) {
+                case MEASURED:
+                    shape = MEASURED_SHAPE;
+                    break;
+                case DIGITIZED:
+                    shape = DIGITIZED_SHAPE;
+                    break;
+                default:
+                    shape = INTERPOLATED_SHAPE;
+            }
+            scaledShapesCache.put(hash, shape = scale(shape, size));
+        }
+
+        return shape;
+    }
+}
+// vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 :
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/flys-artifacts/src/main/java/de/intevation/flys/jfree/ShapeRenderer.java	Mon Jun 25 11:38:33 2012 +0000
@@ -0,0 +1,342 @@
+package de.intevation.flys.jfree;
+
+/**
+ * Copyright (c) 2006, 2012 by Intevation GmbH
+ *
+ * @author Sascha L. Teichmann (teichmann@intevation.de)
+ *
+ * This program is free software under the LGPL (&gt;=v2.1)
+ * Read the file LGPL coming with FLYS for details.
+ */
+
+import java.awt.Font;
+import java.awt.Graphics2D;
+import java.awt.Paint;
+import java.awt.Shape;
+
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.jfree.chart.axis.ValueAxis;
+
+import org.jfree.chart.labels.ItemLabelPosition;
+import org.jfree.chart.labels.XYItemLabelGenerator;
+
+import org.jfree.chart.plot.CrosshairState;
+import org.jfree.chart.plot.PlotOrientation;
+import org.jfree.chart.plot.PlotRenderingInfo;
+import org.jfree.chart.plot.XYPlot;
+
+import org.jfree.chart.renderer.xy.StandardXYItemRenderer;
+import org.jfree.chart.renderer.xy.XYItemRendererState;
+
+import org.jfree.data.xy.XYDataset;
+
+import org.jfree.text.TextUtilities;
+
+import org.jfree.ui.RectangleEdge;
+
+public class ShapeRenderer
+extends      StandardXYItemRenderer {
+
+    public static class Entry {
+        protected Shape   shape;
+        protected Shape   frame;
+        protected Paint   paint;
+        protected boolean filled;
+
+        public Entry(
+            Shape shape,
+            Paint paint,
+            boolean filled
+        ) {
+            this.shape = shape;
+            this.paint = paint;
+            this.filled = filled;
+        }
+
+        public Entry(
+            Shape   shape,
+            Shape   frame,
+            Paint   paint,
+            boolean filled
+        ) {
+            this.shape  = shape;
+            this.frame  = frame;
+            this.paint  = paint;
+            this.filled = filled;
+        }
+
+        public Shape getShape() {
+            return shape;
+        }
+
+        public void setShape(Shape shape) {
+            this.shape = shape;
+        }
+
+
+        public Paint getPaint() {
+            return paint;
+        }
+
+        public void setPaint(Paint paint) {
+            this.paint = paint;
+        }
+
+        public boolean getFilled() {
+            return filled;
+        }
+
+        public void setFilled(boolean filled) {
+            this.filled = filled;
+        }
+
+        public boolean equals(Object other) {
+            Entry entry = (Entry)other;
+            return filled == entry.filled
+                   &&   paint.equals(entry.paint)
+                   &&   shape.equals(entry.shape);
+        }
+
+        public int hashCode() {
+            return
+                shape.hashCode() ^
+                paint.hashCode() ^
+                (filled ? 1231 : 1237);
+        }
+    } // class Entry
+
+    protected Entry []  entries;
+
+    protected List<Rectangle2D> labelBoundingBoxes;
+
+    protected Rectangle2D area;
+
+    public ShapeRenderer() {
+        this(SHAPES);
+    }
+
+    public ShapeRenderer(int type) {
+        super(type);
+    }
+
+    public ShapeRenderer(Map<Entry, Integer> map) {
+        super(SHAPES);
+        setEntries(map);
+    }
+
+    public void setEntries(Entry [] entries) {
+        this.entries = entries;
+    }
+
+    public void setEntries(Map<Entry, Integer> map) {
+        Entry [] entries = new Entry[map.size()];
+
+        for (Map.Entry<Entry, Integer> entry: map.entrySet()) {
+            entries[entry.getValue()] = entry.getKey();
+        }
+
+        setEntries(entries);
+    }
+
+    @Override
+    public Shape getSeriesShape(int series) {
+        return entries[series].shape;
+    }
+
+    public Shape getSeriesFrame(int series) {
+        return entries[series].frame;
+    }
+
+    @Override
+    public Paint getSeriesPaint(int series) {
+        return entries[series].paint;
+    }
+
+    @Override
+    public boolean getItemShapeFilled(int series, int item) {
+        return entries[series].filled;
+    }
+
+    @Override
+    public XYItemRendererState initialise(
+        Graphics2D        g2,
+        Rectangle2D       dataArea,
+        XYPlot            plot,
+        XYDataset         data,
+        PlotRenderingInfo info
+    ) {
+        if (labelBoundingBoxes == null) {
+            labelBoundingBoxes = new ArrayList<Rectangle2D>(32);
+        }
+        else {
+            labelBoundingBoxes.clear();
+        }
+
+        area = dataArea;
+
+        return super.initialise(g2, dataArea, plot, data, info);
+    }
+
+    @Override
+    public void drawItem(
+        Graphics2D          g2,
+        XYItemRendererState state,
+        Rectangle2D         dataArea,
+        PlotRenderingInfo   info,
+        XYPlot              plot,
+        ValueAxis           domainAxis,
+        ValueAxis           rangeAxis,
+        XYDataset           dataset,
+        int                 series,
+        int                 item,
+        CrosshairState      crosshairState,
+        int                 pass
+    ) {
+        if (!getItemVisible(series, item)) {
+            return;
+        }
+
+        // get the data point...
+        double x1 = dataset.getXValue(series, item);
+        double y1 = dataset.getYValue(series, item);
+        if (Double.isNaN(x1) || Double.isNaN(y1)) {
+            return;
+        }
+
+        RectangleEdge xAxisLocation = plot.getDomainAxisEdge();
+        RectangleEdge yAxisLocation = plot.getRangeAxisEdge();
+        double x = domainAxis.valueToJava2D(x1, dataArea, xAxisLocation);
+        double y = rangeAxis.valueToJava2D(y1, dataArea, yAxisLocation);
+
+        if (dataArea.contains(x, y))
+            super.drawItem(
+                g2,
+                state,
+                dataArea,
+                info,
+                plot,
+                domainAxis,
+                rangeAxis,
+                dataset,
+                series,
+                item,
+                crosshairState,
+                pass);
+    }
+
+    protected Point2D shiftBox(Rectangle2D box) {
+
+        double cx1 = area.getX();
+        double cy1 = area.getY();
+        double cx2 = cx1 + area.getWidth();
+        double cy2 = cy1 + area.getHeight();
+
+        double bx1 = box.getX();
+        double by1 = box.getY();
+        double bx2 = bx1 + box.getWidth();
+        double by2 = by1 + box.getHeight();
+
+        double dx;
+        double dy;
+
+        if (bx1 < cx1) {
+            dx = cx1 - bx1;
+        }
+        else if (bx2 > cx2) {
+            dx = cx2 - bx2;
+        }
+        else {
+            dx = 0d;
+        }
+
+        if (by1 < cy1) {
+            dy = cy1 - by1;
+        }
+        else if (by2 > cy2) {
+            dy = cy2 - by2;
+        }
+        else {
+            dy = 0d;
+        }
+
+        return new Point2D.Double(dx, dy);
+    }
+
+    @Override
+    protected void drawItemLabel(
+        Graphics2D      g2,
+        PlotOrientation orientation,
+        XYDataset       dataset,
+        int             series,
+        int             item,
+        double          x,
+        double          y,
+        boolean         negative
+    ) {
+        XYItemLabelGenerator generator = getItemLabelGenerator(series, item);
+        if (generator == null) {
+            return;
+        }
+
+        Font labelFont = getItemLabelFont(series, item);
+
+        Paint paint = getItemLabelPaint(series, item);
+
+        g2.setFont(labelFont);
+        g2.setPaint(paint);
+
+        String label = generator.generateLabel(dataset, series, item);
+
+        // get the label position..
+        ItemLabelPosition position = null;
+        if (!negative) {
+            position = getPositiveItemLabelPosition(series, item);
+        }
+        else {
+            position = getNegativeItemLabelPosition(series, item);
+        }
+
+        // work out the label anchor point...
+        Point2D anchorPoint = calculateLabelAnchorPoint(
+            position.getItemLabelAnchor(), x, y, orientation);
+
+        Shape labelShape = TextUtilities.calculateRotatedStringBounds(
+            label, g2,
+            (float)anchorPoint.getX(), (float)anchorPoint.getY(),
+            position.getTextAnchor(), position.getAngle(),
+            position.getRotationAnchor());
+
+        Rectangle2D bbox = labelShape.getBounds2D();
+
+        Point2D shift = shiftBox(bbox);
+
+        bbox = new Rectangle2D.Double(
+            bbox.getX() + shift.getX(),
+            bbox.getY() + shift.getY(),
+            bbox.getWidth(),
+            bbox.getHeight());
+
+        if (labelBoundingBoxes != null) {
+            for (Rectangle2D old: labelBoundingBoxes) {
+                if (old.intersects(bbox)) {
+                    return;
+                }
+            }
+            labelBoundingBoxes.add(bbox);
+        }
+
+        TextUtilities.drawRotatedString(
+            label, g2,
+            (float)(anchorPoint.getX() + shift.getX()),
+            (float)(anchorPoint.getY() + shift.getY()),
+            position.getTextAnchor(), position.getAngle(),
+            position.getRotationAnchor());
+    }
+}
+// vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 :
--- a/flys-artifacts/src/main/java/de/intevation/flys/utils/Formatter.java	Mon Jun 25 07:59:22 2012 +0000
+++ b/flys-artifacts/src/main/java/de/intevation/flys/utils/Formatter.java	Mon Jun 25 11:38:33 2012 +0000
@@ -142,6 +142,12 @@
             WATERLEVEL_KM_MAX_DIGITS);
     }
 
+    public static NumberFormat getWaterlevelW(CallMeta meta) {
+        return getFormatter(
+            meta,
+            WATERLEVEL_W_MIN_DIGITS,
+            WATERLEVEL_W_MAX_DIGITS);
+    }
 
     /**
      * Returns the number formatter for W values in waterlevel exports.
@@ -168,6 +174,12 @@
             WATERLEVEL_Q_MAX_DIGITS);
     }
 
+    public static NumberFormat getWaterlevelQ(CallMeta meta) {
+        return getFormatter(
+            meta,
+            WATERLEVEL_Q_MIN_DIGITS,
+            WATERLEVEL_Q_MAX_DIGITS);
+    }
 
     /**
      * Returns the number formatter for W values in exports of computed

http://dive4elements.wald.intevation.org