1

I plot a curve with JFreechart. Then the user can draw ranges by dragging the mouse. These I plot using AbstractChartAnnotation to draw a filled Path2D. So far so nice - all aligns perfectly with the curve.

When an area was already annotated the new annotation gets deleted. I use XYPlot.removeAnnotation with the new annotation.

My problem is that sometimes not only the "new" annotation gets removed, but also a second annotation elsewhere in the plot. It doesn't seem random - I kinda found annotations to the "right" side more prone to this happening.

I'm very confused what could cause this. The object that draws/deletes the new annotation is reinstated every time and only holds the current annotation - so how could the other annotation be deleted?

Would be very grateful for any hints, thanks.


As suggested I prepare a sscce example. Unfortunately it's not too short.

import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.*;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.event.MouseInputListener;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.annotations.AbstractXYAnnotation;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.plot.Plot;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.PlotRenderingInfo;
import org.jfree.chart.plot.XYPlot;
import org.jfree.data.time.Millisecond;
import org.jfree.data.time.TimeSeries;
import org.jfree.data.time.TimeSeriesCollection;
import org.jfree.data.time.TimeSeriesDataItem;
import org.jfree.ui.RectangleEdge;

/**
 *
 * @author c.ager
 */
public class IntegrationSSCE {

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        JFrame jFrame = new JFrame();
        jFrame.setLayout(new BorderLayout());
        jFrame.setSize(600, 400);
        jFrame.setDefaultCloseOperation(jFrame.EXIT_ON_CLOSE);
        TimeSeriesCollection timeSeriesCollection = new TimeSeriesCollection();
        TimeSeries timeSeries = new TimeSeries("test");
        for (long i = 0; i < 1000; i++) {
            double val = Math.random() + 3 * Math.exp(-Math.pow(i - 300, 2) / 1000);
            timeSeries.add(new Millisecond(new Date(i)), val);
        }
        timeSeriesCollection.addSeries(timeSeries);

        JFreeChart chart = ChartFactory.createTimeSeriesChart(
                null,
                null, "data", timeSeriesCollection,
                true, true, false);
        ChartPanel chartPanel = new ChartPanel(chart);
        chartPanel.removeMouseListener(chartPanel);

        Set<MyAnnot> annotSet = new TreeSet<MyAnnot>();

        AnnotListener list = new AnnotListener(chartPanel, annotSet, timeSeries);
        chartPanel.addMouseListener(list);
        chartPanel.addMouseMotionListener(list);

        jFrame.add(chartPanel, BorderLayout.CENTER);


        jFrame.setVisible(true);

        // TODO code application logic here
    }

    private static class AnnotListener implements MouseInputListener {

        Point2D start, end;
        MyAnnot currAnnot;
        final Set<MyAnnot> annotSet;
        final ChartPanel myChart;
        final TimeSeries timeSeries;

        public AnnotListener(ChartPanel myChart, Set<MyAnnot> annotSet, TimeSeries timeSeries) {
            this.myChart = myChart;
            this.annotSet = annotSet;
            this.timeSeries = timeSeries;
        }

        @Override
        public void mousePressed(MouseEvent e) {
            start = convertScreePoint2DataPoint(e.getPoint());
            currAnnot = new MyAnnot(start, timeSeries, myChart.getChart().getXYPlot());
            myChart.getChart().getXYPlot().addAnnotation(currAnnot);
        }

        @Override
        public void mouseDragged(MouseEvent e) {
            end = convertScreePoint2DataPoint(e.getPoint());
            currAnnot.updateEnd(end);
        }

        @Override
        public void mouseReleased(MouseEvent e) {
            boolean test = annotSet.add(currAnnot);
            if (!test) {
                myChart.getChart().getXYPlot().removeAnnotation(currAnnot);
            }
        }

        @Override
        public void mouseEntered(MouseEvent e) {
        }

        @Override
        public void mouseClicked(MouseEvent e) {
        }

        @Override
        public void mouseExited(MouseEvent e) {
        }

        @Override
        public void mouseMoved(MouseEvent e) {
        }

        protected Point2D convertScreePoint2DataPoint(Point in) {
            Rectangle2D plotArea = myChart.getScreenDataArea();
            XYPlot plot = (XYPlot) myChart.getChart().getPlot();
            double x = plot.getDomainAxis().java2DToValue(in.getX(), plotArea, plot.getDomainAxisEdge());
            double y = plot.getRangeAxis().java2DToValue(in.getY(), plotArea, plot.getRangeAxisEdge());
            return new Point2D.Double(x, y);
        }
    }

    private static class MyAnnot extends AbstractXYAnnotation implements Comparable<MyAnnot> {

        Long max;
        Line2D line;
        final TimeSeries timeSeries;
        final XYPlot plot;
        final Stroke stroke  = new BasicStroke(1.5f);

        public MyAnnot(Point2D start, TimeSeries timeSeries, XYPlot plot) {
            this.plot = plot;
            this.timeSeries = timeSeries;

            line = new Line2D.Double(start, start);
            findMax();
        }

        public void updateEnd(Point2D end) {
            line.setLine(line.getP1(), end);
            findMax();
            fireAnnotationChanged();
        }

        @Override
        public void draw(Graphics2D gd, XYPlot xyplot, Rectangle2D rd, ValueAxis va, ValueAxis va1, int i, PlotRenderingInfo pri) {
            PlotOrientation orientation = plot.getOrientation();
            RectangleEdge domainEdge = Plot.resolveDomainAxisLocation(
                    plot.getDomainAxisLocation(), orientation);
            RectangleEdge rangeEdge = Plot.resolveRangeAxisLocation(
                    plot.getRangeAxisLocation(), orientation);

            double m02 = va.valueToJava2D(0, rd, domainEdge);
            // y-axis translation
            double m12 = va1.valueToJava2D(0, rd, rangeEdge);
            // x-axis scale
            double m00 = va.valueToJava2D(1, rd, domainEdge) - m02;
            // y-axis scale
            double m11 = va1.valueToJava2D(1, rd, rangeEdge) - m12;

            Shape s = null;
            if (orientation == PlotOrientation.HORIZONTAL) {
                AffineTransform t1 = new AffineTransform(
                        0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f);
                AffineTransform t2 = new AffineTransform(
                        m11, 0.0f, 0.0f, m00, m12, m02);
                s = t1.createTransformedShape(line);
                s = t2.createTransformedShape(s);
            } else if (orientation == PlotOrientation.VERTICAL) {
                AffineTransform t = new AffineTransform(m00, 0, 0, m11, m02, m12);
                s = t.createTransformedShape(line);
            }
            gd.setStroke(stroke);
            gd.setPaint(Color.BLUE);
            gd.draw(s);
            addEntity(pri, s.getBounds2D(), i, getToolTipText(), getURL());
        }

        @Override
        public int compareTo(MyAnnot o) {
            return max.compareTo(o.max);
        }

        private void findMax() {
            max = (long) line.getP1().getX();

            Point2D left, right;
            if (line.getP1().getX() < line.getP2().getX()) {
                left = line.getP1();
                right = line.getP2();
            } else {
                left = line.getP2();
                right = line.getP1();
            }
            Double maxVal = left.getY();
            List<TimeSeriesDataItem> items = timeSeries.getItems();
            for (Iterator<TimeSeriesDataItem> it = items.iterator(); it.hasNext();) {
                TimeSeriesDataItem dataItem = it.next();
                if (dataItem.getPeriod().getFirstMillisecond() < left.getX()) {
                    continue;
                }
                if (dataItem.getPeriod().getFirstMillisecond() > right.getX()) {
                    break;
                }
                double curVal = dataItem.getValue().doubleValue();
                if (curVal > maxVal) {
                    maxVal = curVal;
                    max = dataItem.getPeriod().getFirstMillisecond();
                }
            }
        }
    }
}

Here is the problematic behaviour. Note that images 2 and 4 were taken while the mouse button was pressed.

  1. select a few non-overlapping lines - no problem as it should be select a few non-overlapping lines enter image description here enter image description here enter image description here enter image description here

I have just been looking at it in the debugger - could it be that ArrayList.remove(Object o) removes the WRONG element? Seems very unlikely to me...

bdecaf
  • 4,652
  • 23
  • 44

1 Answers1

2

You might look at the Layer to which the annotation is being added. There's an example here. Naturally, an sscce that exhibits the problem you describe would help clarify the source of the problem.

Addendum: One potential problem is that your implementation of Comparable is not consistent with equals(), as the latter relies (implicitly) on the super-class implementation. A consistent implementation is required for use with a sorted Set such as TreeSet. You'll need to override hashCode(), too. Class Value is an example.

Community
  • 1
  • 1
trashgod
  • 203,806
  • 29
  • 246
  • 1,045
  • Ok created a sscce. But can't find the layer. Can you point me? – bdecaf Jan 25 '12 at 11:14
  • If you have access to [_Effective Java_](http://java.sun.com/docs/books/effective/), see items 9 & 12. – trashgod Jan 25 '12 at 19:13
  • ah - we are getting somewhere! overriding hashCode alone didn't help - but I got a message I should also override `equals` and now I don't see the "weird" behavior in the example. Unfortunately I don't have access to the book you suggest. What do the chapters you suggest treat? – bdecaf Jan 25 '12 at 19:32
  • Just as note - in my application I didn't implement `Comparable` in my annotation. I was just relying that the "default" method would work. Turns out I was wrong. – bdecaf Jan 25 '12 at 19:35
  • I habitually & defensively override all three: `compareTo()`, `equals()` & `hashCode()`. Edited to clarify. – trashgod Jan 25 '12 at 23:49
  • Seems a good advice. You nailed it on first try - thanks a lot :D – bdecaf Jan 26 '12 at 09:10