org.eurocarbdb.application.glycoworkbench.plugin.reporting.AnnotationReportCanvas.java Source code

Java tutorial

Introduction

Here is the source code for org.eurocarbdb.application.glycoworkbench.plugin.reporting.AnnotationReportCanvas.java

Source

/*
*   EuroCarbDB, a framework for carbohydrate bioinformatics
*
*   Copyright (c) 2006-2009, Eurocarb project, or third-party contributors as
*   indicated by the @author tags or express copyright attribution
*   statements applied by the authors.  
*
*   This copyrighted material is made available to anyone wishing to use, modify,
*   copy, or redistribute it subject to the terms and conditions of the GNU
*   Lesser General Public License, as published by the Free Software Foundation.
*   A copy of this license accompanies this distribution in the file LICENSE.txt.
*
*   This program is distributed in the hope that it will be useful,
*   but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
*   or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
*   for more details.
*
*   Last commit: $Rev: 1210 $ by $Author: glycoslave $ on $Date:: 2009-06-12 #$  
*/

/**
   @author Alessio Ceroni (a.ceroni@imperial.ac.uk)
*/

package org.eurocarbdb.application.glycoworkbench.plugin.reporting;

import org.eurocarbdb.application.glycoworkbench.plugin.*;
import org.eurocarbdb.application.glycoworkbench.*;
import org.eurocarbdb.application.glycanbuilder.*;

import java.util.*;
import javax.swing.*;
import javax.swing.border.*;
import java.awt.*;
import java.awt.image.*;
import java.awt.geom.*;
import java.awt.event.*;
import java.awt.print.*;
import java.text.DecimalFormat;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.ChartRenderingInfo;
import org.jfree.chart.plot.XYPlot;
import org.jfree.data.Range;
import org.jfree.data.xy.XYDataset;
import org.jfree.data.xy.XYBarDataset;
import org.jfree.data.xy.DefaultXYDataset;
import org.jfree.chart.annotations.XYShapeAnnotation;
import org.jfree.chart.annotations.XYTextAnnotation;
import org.jfree.chart.renderer.xy.StandardXYItemRenderer;
import org.jfree.chart.renderer.xy.XYBarRenderer;

import static org.eurocarbdb.application.glycanbuilder.Geometry.*;

public class AnnotationReportCanvas extends JComponent implements SVGUtils.Renderable,
        BaseDocument.DocumentChangeListener, MouseListener, MouseMotionListener, Printable {

    public interface SelectionChangeListener {
        public void selectionChanged(SelectionChangeEvent e);
    }

    public static class SelectionChangeEvent {
        private AnnotationReportCanvas src;

        public SelectionChangeEvent(AnnotationReportCanvas _src) {
            src = _src;
        }

        public AnnotationReportCanvas getSource() {
            return src;
        }
    }

    private AnnotationReportDocument theDocument;
    private GlycanRenderer theGlycanRenderer;
    private JScrollPane theScrollPane = null;

    private DefaultXYDataset theDataset;
    private DefaultXYDataset maxIntensityDataset;
    private XYPlot thePlot;
    private JFreeChart theChart;

    private boolean first_time;
    private boolean first_time_init_pos;

    // drawing

    private HashSet<AnnotationObject> selections;
    private HashMap<AnnotationObject, Rectangle> rectangles;
    private HashMap<AnnotationObject, Rectangle> rectangles_text;
    private HashMap<AnnotationObject, Rectangle> rectangles_complete;
    private HashMap<AnnotationObject, Polygon> connections;
    private HashMap<AnnotationObject, Point2D> connections_cp;

    private boolean is_printing;
    private Point mouse_start_point = null;
    private Point mouse_end_point = null;
    private AnnotationObject start_position = null;
    private boolean is_dragndrop = false;
    private boolean is_resizing = false;
    private boolean is_movingcp = false;
    private boolean was_dragged = false;

    private Rectangle draw_area;
    private Rectangle chart_area;
    private Rectangle2D data_area;

    private AnnotationReportOptions theOptions;
    private GraphicOptions theGraphicOptions;

    // 
    private Vector<SelectionChangeListener> listeners;

    // construction

    public AnnotationReportCanvas(AnnotationReportDocument doc, boolean init_pos) {

        theDocument = doc;
        theDocument.addDocumentChangeListener(this);

        theOptions = theDocument.getAnnotationReportOptions();
        theGraphicOptions = theDocument.getGraphicOptions();
        theGlycanRenderer = theDocument.getWorkspace().getGlycanRenderer();

        // create chart
        updateChart();

        // finish setting up
        listeners = new Vector<SelectionChangeListener>();

        is_printing = false;

        // adding data and structures
        setScale(1.);
        resetSelection();

        // add events
        addMouseMotionListener(this);
        addMouseListener(this);

        first_time = true;
        first_time_init_pos = init_pos;
    }

    public void setScrollPane(JScrollPane sp) {
        theScrollPane = sp;
    }

    public AnnotationReportDocument getDocument() {
        return theDocument;
    }

    public BuilderWorkspace getWorkspace() {
        return theDocument.getWorkspace();
    }

    public AnnotationReportOptions getAnnotationReportOptions() {
        return theDocument.getAnnotationReportOptions();
    }

    public GraphicOptions getGraphicOptions() {
        return theDocument.getGraphicOptions();
    }

    // drawing

    public void beforeRendering() {
        is_printing = true;

        // make sure everything is initialized
        updateDrawArea(true);
        updateData();
    }

    public void afterRendering() {
        is_printing = false;
    }

    public Dimension getRenderableSize() {
        return getPreferredSize();
    }

    public void paintRenderable(Graphics2D g2d) {
        paintComponent(g2d);
    }

    protected void paintComponent(Graphics g) {

        // prepare graphic object
        Graphics2D g2d = (Graphics2D) g.create();
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        // set clipping area
        if (is_printing) {
            g2d.translate(-draw_area.x, -draw_area.y);
            g2d.setClip(draw_area);
        }

        //paint canvas background
        if (!is_printing) {
            g2d.setColor(getBackground());
            g2d.fillRect(0, 0, getWidth(), getHeight());
        }

        // paint white background on drawing area    
        g2d.setColor(Color.white);
        g2d.fillRect(draw_area.x, draw_area.y, draw_area.width, draw_area.height);
        if (!is_printing) {
            g2d.setColor(Color.black);
            g2d.draw(draw_area);
        }

        // paint
        paintChart(g2d);
        paintAnnotations(g2d);

        // dispose graphic object
        g2d.dispose();

        if (!is_printing) {
            if (first_time) {
                if (first_time_init_pos)
                    placeStructures(true);
                else
                    theDocument.fireDocumentInit();
                first_time = false;
            } else
                revalidate();
        }

    }

    protected void paintChart(Graphics2D g2d) {
        ChartRenderingInfo cri = new ChartRenderingInfo();
        theChart.draw(g2d, chart_area, cri);
        data_area = cri.getPlotInfo().getDataArea();
    }

    protected Rectangle computeRectangles() {
        return computeRectangles(new PositionManager(), new BBoxManager());
    }

    protected Rectangle computeRectangles(PositionManager pman, BBoxManager bbman) {

        DecimalFormat mz_df = new DecimalFormat("0.0");

        Rectangle all_bbox = null;
        rectangles = new HashMap<AnnotationObject, Rectangle>();
        rectangles_text = new HashMap<AnnotationObject, Rectangle>();
        rectangles_complete = new HashMap<AnnotationObject, Rectangle>();
        for (AnnotationObject a : theDocument.getAnnotations()) {

            // set scale
            theGlycanRenderer.getGraphicOptions().setScale(theOptions.SCALE_GLYCANS * theDocument.getScale(a));

            // compute bbox
            Point2D anchor = dataToScreenCoords(theDocument.getAnchor(a));
            Rectangle bbox = theGlycanRenderer.computeBoundingBoxes(a.getStructures(), false, false, pman, bbman,
                    false);

            int x = (int) anchor.getX() - bbox.width / 2;
            int y = (int) anchor.getY() - bbox.height - theOptions.ANNOTATION_MARGIN
                    - theOptions.ANNOTATION_MZ_SIZE;
            bbman.translate(x - bbox.x, y - bbox.y, a.getStructures());
            bbox.translate(x - bbox.x, y - bbox.y);

            // save bbox
            rectangles.put(a, bbox);

            // compute text bbox        
            String mz_text = mz_df.format(a.getPeakPoint().getX());
            Dimension mz_dim = textBounds(mz_text, theOptions.ANNOTATION_MZ_FONT, theOptions.ANNOTATION_MZ_SIZE);
            Rectangle text_bbox = new Rectangle((int) anchor.getX() - mz_dim.width / 2,
                    (int) anchor.getY() - 2 * theOptions.ANNOTATION_MARGIN / 3 - mz_dim.height, mz_dim.width,
                    mz_dim.height);

            // save text bbox
            rectangles_text.put(a, text_bbox);
            rectangles_complete.put(a, expand(union(bbox, text_bbox), theOptions.ANNOTATION_MARGIN / 2));

            // update all bbox
            all_bbox = union(all_bbox, bbox);
            all_bbox = union(all_bbox, text_bbox);
        }
        if (all_bbox == null)
            return new Rectangle(0, 0, 0, 0);
        return all_bbox;
    }

    public Rectangle2D computeSizeData(AnnotationObject a) {
        // compute bbox
        theGlycanRenderer.getGraphicOptions().setScale(theOptions.SCALE_GLYCANS * theDocument.getScale(a));
        Rectangle bbox = theGlycanRenderer.computeBoundingBoxes(a.getStructures(), false, false,
                new PositionManager(), new BBoxManager(), false);

        // compute text bbox        
        DecimalFormat mz_df = new DecimalFormat("0.0");
        String mz_text = mz_df.format(a.getPeakPoint().getX());
        Dimension mz_dim = textBounds(mz_text, theOptions.ANNOTATION_MZ_FONT, theOptions.ANNOTATION_MZ_SIZE);

        // update bbox
        double width = Math.max(bbox.getWidth(), mz_dim.getWidth());
        double height = bbox.getHeight() + theOptions.ANNOTATION_MARGIN + theOptions.ANNOTATION_MZ_SIZE;
        return new Rectangle2D.Double(0, 0, screenToDataX(width), screenToDataY(height));
    }

    protected Point2D computeAnchor(Rectangle rect, Point2D cp, Point2D peak) {
        Point2D anchor;

        if (peak.getY() > bottom(rect)) {
            if (cp.getY() > bottom(rect))
                anchor = new Point2D.Double(midx(rect), bottom(rect));
            else if (cp.getY() < top(rect))
                anchor = new Point2D.Double(midx(rect), top(rect));
            else if (cp.getX() < left(rect))
                anchor = new Point2D.Double(left(rect), midy(rect));
            else
                anchor = new Point2D.Double(right(rect), midy(rect));
        } else {
            if (peak.getY() < top(rect))
                anchor = new Point2D.Double(midx(rect), top(rect));
            else if (peak.getX() < left(rect))
                anchor = new Point2D.Double(left(rect), midy(rect));
            else
                anchor = new Point2D.Double(right(rect), midy(rect));
        }
        return anchor;
    }

    protected void computeConnections() {

        connections = new HashMap<AnnotationObject, Polygon>();
        connections_cp = new HashMap<AnnotationObject, Point2D>();

        for (AnnotationObject a : theDocument.getAnnotations()) {
            Rectangle rect = rectangles_complete.get(a);
            Point2D cp = dataToScreenCoords(theDocument.getControlPoint(a));
            Point2D peak = dataToScreenCoords(a.getPeakPoint());

            // select anchor
            Point2D anchor = computeAnchor(rect, cp, peak);
            boolean add_cp = (peak.getY() > bottom(rect));

            if (anchor.distance(peak) > 10) {
                // create shape
                Polygon connection = new Polygon();
                connection.addPoint((int) anchor.getX(), (int) anchor.getY());
                if (add_cp)
                    connection.addPoint((int) cp.getX(), (int) cp.getY());
                connection.addPoint((int) peak.getX(), (int) peak.getY());
                if (add_cp)
                    connection.addPoint((int) cp.getX(), (int) cp.getY());

                // save
                connections.put(a, connection);
                if (add_cp)
                    connections_cp.put(a, cp);
            }
        }
    }

    protected void paintAnnotations(Graphics2D g2d) {

        DecimalFormat mz_df = new DecimalFormat("0.0");

        // set font
        Font old_font = g2d.getFont();
        Font new_font = new Font(theOptions.ANNOTATION_MZ_FONT, Font.PLAIN, theOptions.ANNOTATION_MZ_SIZE);

        // compute bboxes
        PositionManager pman = new PositionManager();
        BBoxManager bbman = new BBoxManager();
        computeRectangles(pman, bbman);

        // compute connections
        computeConnections();

        // paint connections
        for (AnnotationObject a : theDocument.getAnnotations()) {
            boolean selected = !is_printing && selections.contains(a);

            // paint arrow        
            Polygon connection = connections.get(a);
            if (connection != null) {
                g2d.setColor(theOptions.CONNECTION_LINES_COLOR);
                g2d.setStroke((selected) ? new BasicStroke((float) (1. + theOptions.ANNOTATION_LINE_WIDTH))
                        : new BasicStroke((float) theOptions.ANNOTATION_LINE_WIDTH));
                g2d.draw(connection);
                g2d.setStroke(new BasicStroke(1));
            }

            // paint control point       
            if (selected) {
                g2d.setColor(Color.black);
                Point2D cp = connections_cp.get(a);
                if (cp != null) {
                    int s = (int) (2 + theOptions.ANNOTATION_LINE_WIDTH);
                    g2d.fill(new Rectangle((int) cp.getX() - s, (int) cp.getY() - s, 2 * s, 2 * s));
                }
            }
        }

        // paint glycans
        for (AnnotationObject a : theDocument.getAnnotations()) {
            boolean highlighted = a.isHighlighted();
            boolean selected = !is_printing && selections.contains(a);

            // set scale
            theGlycanRenderer.getGraphicOptions().setScale(theOptions.SCALE_GLYCANS * theDocument.getScale(a));

            // paint highlighted region
            if (highlighted) {
                Rectangle c_bbox = rectangles_complete.get(a);

                g2d.setColor(theOptions.HIGHLIGHTED_COLOR);
                g2d.setXORMode(Color.white);
                g2d.fill(c_bbox);
                g2d.setPaintMode();

                g2d.setColor(Color.black);
                g2d.draw(c_bbox);
            }

            // paint glycan
            for (Glycan s : a.getStructures())
                theGlycanRenderer.paint(g2d, s, null, null, false, false, pman, bbman);

            // paint MZ text
            g2d.setFont(new_font);
            g2d.setColor(theOptions.MASS_TEXT_COLOR);
            String mz_text = mz_df.format(a.getPeakPoint().getX());
            Rectangle mz_bbox = rectangles_text.get(a);
            g2d.drawString(mz_text, mz_bbox.x, mz_bbox.y + mz_bbox.height);

            // paint selection        
            if (selected) {
                // paint rectangle
                Rectangle c_bbox = rectangles_complete.get(a);

                g2d.setStroke(new BasicStroke(highlighted ? 2 : 1));
                g2d.setColor(Color.black);

                g2d.draw(c_bbox);

                g2d.setStroke(new BasicStroke(1));

                // paint resize points        
                Polygon p1 = new Polygon();
                int cx1 = right(c_bbox);
                int cy1 = top(c_bbox);
                p1.addPoint(cx1, cy1);
                p1.addPoint(cx1 - 2 * theOptions.ANNOTATION_MARGIN / 3, cy1);
                p1.addPoint(cx1, cy1 + 2 * theOptions.ANNOTATION_MARGIN / 3);
                g2d.fill(p1);

                Polygon p2 = new Polygon();
                int cx2 = left(c_bbox);
                int cy2 = top(c_bbox);
                p2.addPoint(cx2, cy2);
                p2.addPoint(cx2 + 2 * theOptions.ANNOTATION_MARGIN / 3, cy2);
                p2.addPoint(cx2, cy2 + 2 * theOptions.ANNOTATION_MARGIN / 3);
                g2d.fill(p2);
            }
        }

        g2d.setFont(old_font);
    }

    private void xorRectangle(Point start_point, Point end_point) {
        Graphics g = getGraphics();
        g.setXORMode(Color.white);
        g.setColor(Color.gray);

        Rectangle rect = makeRectangle(start_point, end_point);
        g.drawRect(rect.x, rect.y, rect.width, rect.height);
    }

    private void xorSelections(Point start_point, Point end_point) {
        Graphics g = getGraphics();
        g.setXORMode(Color.white);
        g.setColor(Color.gray);

        int dx = end_point.x - start_point.x;
        int dy = end_point.y - start_point.y;
        for (AnnotationObject a : selections) {
            Rectangle rect = rectangles_complete.get(a);
            g.drawRect(dx + rect.x, dy + rect.y, rect.width, rect.height);
        }
    }

    private double scaleFactor(AnnotationObject selection, Point start_point, Point end_point) {
        Rectangle rect = rectangles.get(selection);

        double scale = 1.;
        if (start_point.x > midx(rect)) {
            scale = Math.min((double) (end_point.x - start_point.x + rect.width / 2.) / (double) (rect.width / 2.),
                    (double) (start_point.y - end_point.y + rect.height) / (double) (rect.height));
        } else {
            scale = Math.min((double) (start_point.x - end_point.x + rect.width / 2.) / (double) (rect.width / 2.),
                    (double) (start_point.y - end_point.y + rect.height) / (double) (rect.height));
        }

        return Math.max(0., scale);
    }

    private void xorConnection(AnnotationObject selection, Point start_point, Point end_point) {

        Graphics g = getGraphics();
        g.setXORMode(Color.white);
        g.setColor(Color.gray);

        Rectangle rect = rectangles_complete.get(selection);
        Point2D peak = dataToScreenCoords(selection.getPeakPoint());

        // select anchor
        Point2D anchor = computeAnchor(rect, end_point, peak);

        // draw connection
        g.drawLine((int) anchor.getX(), (int) anchor.getY(), (int) end_point.getX(), (int) end_point.getY());
        g.drawLine((int) end_point.getX(), (int) end_point.getY(), (int) peak.getX(), (int) peak.getY());
    }

    private void xorResizing(AnnotationObject selection, Point start_point, Point end_point) {
        Graphics g = getGraphics();
        g.setXORMode(Color.white);
        g.setColor(Color.gray);

        Rectangle rect = rectangles.get(selection);

        double scale = 1.;
        if (start_point.x > midx(rect)) {
            scale = Math.min((double) (end_point.x - start_point.x + rect.width / 2.) / (double) (rect.width / 2.),
                    (double) (start_point.y - end_point.y + rect.height) / (double) (rect.height));
        } else {
            scale = Math.min((double) (start_point.x - end_point.x + rect.width / 2.) / (double) (rect.width / 2.),
                    (double) (start_point.y - end_point.y + rect.height) / (double) (rect.height));
        }
        scale = Math.max(0., scale);

        g.drawRect((int) (midx(rect) - rect.width * scale / 2.), (int) (bottom(rect) - rect.height * scale),
                (int) (rect.width * scale), (int) (rect.height * scale));
    }

    public Dimension getPreferredSize() {
        if (is_printing)
            return draw_area.getSize();
        return theOptions.getViewDimension(draw_area.getSize());
    }

    public Dimension getMinimumSize() {
        return new Dimension(0, 0);
    }

    public double screenToDataX(double length) {
        return length / thePlot.getDomainAxis().lengthToJava2D(1., data_area, thePlot.getDomainAxisEdge());
    }

    public double screenToDataY(double length) {
        return length / thePlot.getRangeAxis().lengthToJava2D(1., data_area, thePlot.getRangeAxisEdge());
    }

    public Point2D screenToDataCoords(Point2D p) {

        double x = thePlot.getDomainAxis().java2DToValue(p.getX(), data_area, thePlot.getDomainAxisEdge());
        double y = thePlot.getRangeAxis().java2DToValue(p.getY(), data_area, thePlot.getRangeAxisEdge());
        return new Point2D.Double(x, y);
    }

    public Point2D dataToScreenCoords(Point2D p) {

        double x = thePlot.getDomainAxis().valueToJava2D(p.getX(), data_area, thePlot.getDomainAxisEdge());
        double y = thePlot.getRangeAxis().valueToJava2D(p.getY(), data_area, thePlot.getRangeAxisEdge());
        return new Point2D.Double(x, y);
    }

    public AnnotationObject getAnnotationAtPoint(Point2D p) {
        for (Map.Entry<AnnotationObject, Rectangle> e : rectangles_complete.entrySet()) {
            if (e.getValue().contains(p))
                return e.getKey();
        }
        return null;
    }

    public AnnotationObject getConnectionAtPoint(Point2D p) {
        for (Map.Entry<AnnotationObject, Polygon> e : connections.entrySet()) {
            if (e.getValue().intersects(p.getX() - 3, p.getY() - 3, 6., 6.))
                return e.getKey();
        }
        return null;
    }

    public AnnotationObject getCPAtPoint(Point2D p) {
        for (Map.Entry<AnnotationObject, Point2D> e : connections_cp.entrySet()) {
            if (e.getValue().distance(p) <= 3)
                return e.getKey();
        }
        return null;
    }

    public Collection<AnnotationObject> getAnnotationsInside(Rectangle r) {
        Vector<AnnotationObject> ret = new Vector<AnnotationObject>();
        if (r != null) {
            for (Map.Entry<AnnotationObject, Rectangle> e : rectangles_complete.entrySet()) {
                if (r.intersects(e.getValue()))
                    ret.add(e.getKey());
            }
        }
        return ret;
    }

    // selection

    public void selectAll() {
        selections.addAll(theDocument.getAnnotations());
        fireUpdatedSelection();
    }

    public void enforceSelection(Point2D p) {
        AnnotationObject a = getAnnotationAtPoint(p);
        if (a == null)
            a = getConnectionAtPoint(p);

        if (!isSelected(a))
            setSelection(a);
    }

    public void resetSelection() {
        selections = new HashSet<AnnotationObject>();
        fireUpdatedSelection();
    }

    public boolean hasSelection() {
        return selections.size() > 0;
    }

    public boolean isSelected(AnnotationObject a) {
        return selections.contains(a);
    }

    public void setSelection(AnnotationObject a) {
        selections.clear();
        if (a != null)
            selections.add(a);
        fireUpdatedSelection();
    }

    public void addSelection(AnnotationObject a) {
        if (a != null) {
            selections.add(a);
            fireUpdatedSelection();
        }
    }

    public void setSelection(Collection<AnnotationObject> toselect) {
        selections.clear();
        selections.addAll(toselect);
        fireUpdatedSelection();
    }

    public void addSelection(Collection<AnnotationObject> toselect) {
        selections.addAll(toselect);
        fireUpdatedSelection();
    }

    // actions

    public void print(PrinterJob job) throws PrinterException {
        // do something before
        is_printing = true;

        job.print();

        // do something after
        is_printing = false;
    }

    public int print(Graphics g, PageFormat pageFormat, int pageIndex) throws PrinterException {
        if (pageIndex > 0) {
            return NO_SUCH_PAGE;
        } else {
            Graphics2D g2d = (Graphics2D) g;
            g2d.setBackground(Color.white);
            g2d.translate(pageFormat.getImageableX(), pageFormat.getImageableY());

            Dimension td = this.getPreferredSize();
            double sx = pageFormat.getImageableWidth() / td.width;
            double sy = pageFormat.getImageableHeight() / td.height;
            double s = Math.min(sx, sy);
            if (s < 1.)
                g2d.scale(s, s);

            RepaintManager.currentManager(this).setDoubleBufferingEnabled(false);
            this.paint(g2d);
            RepaintManager.currentManager(this).setDoubleBufferingEnabled(true);

            return PAGE_EXISTS;
        }
    }

    public void setScale(double scale) {
        theOptions.setScale(scale);
        double scaleg = theGraphicOptions.setScale(theOptions.SCALE_GLYCANS);
        theOptions.setScale(scale * scaleg / theOptions.SCALE_GLYCANS); // set to nearest feasible scale

        updateView();
    }

    public double getScale() {
        return theOptions.SCALE;
    }

    public void moveSelections(int dx, int dy) {
        HashSet<AnnotationObject> old_selections = selections;

        double ddx = screenToDataX(dx);
        double ddy = screenToDataY(dy);
        theDocument.move(selections, ddx, ddy);

        selections = old_selections;
    }

    public void moveControlPointTo(AnnotationObject selection, Point2D p) {
        Point2D dp = screenToDataCoords(p);
        theDocument.moveControlPointTo(selection, dp.getX(), dp.getY());
    }

    public void rescaleSelections(double factor) {
        HashSet<AnnotationObject> old_selections = selections;

        theDocument.rescale(selections, factor);

        selections = old_selections;
    }

    public void resetSelectionsScale() {
        HashSet<AnnotationObject> old_selections = selections;

        theDocument.resetScale(selections);

        selections = old_selections;
    }

    public void highlightSelections() {
        HashSet<AnnotationObject> old_selections = selections;

        boolean all_highlighted = true;
        for (AnnotationObject a : selections)
            all_highlighted = all_highlighted && a.isHighlighted();

        theDocument.setHighlighted(selections, !all_highlighted);

        selections = old_selections;
    }

    public void updateAnnotations(Glycan parent, PeakAnnotationCollection pac, boolean merge) {
        // update document
        Vector<AnnotationObject> added = new Vector<AnnotationObject>();
        boolean changed = theDocument.updateData(parent, pac, added, false, merge);

        if (added.size() > 0) {
            // update canvas
            updateDrawArea(false);
            resetSelection();
            repaint();

            // place new structures
            placeStructures(added, false);
        } else if (changed)
            theDocument.fireDocumentChanged();
    }

    public void cut() {
        copy();
        delete();
    }

    public void copy() {
        Vector<Glycan> sel_structures = new Vector<Glycan>();
        for (AnnotationObject a : selections)
            sel_structures.addAll(a.getStructures());
        ClipUtils.setContents(new GlycanSelection(theGlycanRenderer, sel_structures));
    }

    public void delete() {
        theDocument.remove(selections);
    }

    public void getScreenshot() {
        ClipUtils.setContents(getImage());
    }

    public BufferedImage getImage() {
        // Create an image that supports transparent pixels
        Dimension d = getPreferredSize();
        BufferedImage img = GraphicUtils.createCompatibleImage(d.width, d.height, false);

        // prepare graphics context
        Graphics2D g2d = img.createGraphics();
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
        g2d.setBackground(new Color(255, 255, 255, 0));

        // paint
        is_printing = true;
        this.paint(g2d);
        is_printing = false;

        return img;
    }

    public void placeStructures() {
        placeStructures(theDocument.getAnnotations(), false);
    }

    private void placeStructures(boolean init) {
        placeStructures(theDocument.getAnnotations(), init);
    }

    private void placeStructures(Vector<AnnotationObject> annotations, boolean init) {

        // get starting point
        double y = thePlot.getRangeAxis().getRange().getUpperBound();
        double cur_x = thePlot.getDomainAxis().getRange().getLowerBound();

        double all_width = 0.;
        for (AnnotationObject a : annotations) {
            if (a.hasAnnotations())
                all_width += screenToDataX(rectangles_complete.get(a).width);
        }

        double min_pp_x = annotations.firstElement().getPeakPoint().getX();
        double max_pp_x = annotations.lastElement().getPeakPoint().getX();
        double center_pp_x = (max_pp_x + min_pp_x) / 2;

        cur_x = Math.max(cur_x, center_pp_x - all_width / 2);

        // place annotations
        for (AnnotationObject a : annotations) {
            Point2D pp = a.getPeakPoint();
            if (a.hasAnnotations()) {
                double cur_width = screenToDataX(rectangles_complete.get(a).width);
                double x = cur_x + cur_width / 2.;
                theDocument.getAnchor(a).setLocation(x, y);
                theDocument.getControlPoints().put(a,
                        theDocument.computeControlPoint(new Point2D.Double(x, y), pp));

                cur_x += cur_width;
            } else {
                theDocument.getAnchor(a).setLocation(pp.getX(), pp.getY());
                theDocument.getControlPoints().put(a, theDocument.computeControlPoint(pp, pp));
            }
        }

        // refine control points
        for (int i = 0; i < annotations.size(); i++) {
            AnnotationObject ai = annotations.get(i);
            Point2D aai = theDocument.getAnchor(ai);
            Point2D cpi = theDocument.getControlPoint(ai);

            if (aai.getX() < cpi.getX()) {
                for (int l = i + 1; l < annotations.size(); l++) {
                    AnnotationObject al = annotations.get(l);
                    Point2D aal = theDocument.getAnchor(al);
                    Point2D cpl = theDocument.getControlPoint(al);

                    if (aal.getX() > cpi.getX())
                        break;
                    if (cpl.getY() < cpi.getY()) {
                        cpl.setLocation(cpl.getX(), cpi.getY());
                        ai = al;
                        aai = aal;
                        cpi = cpl;
                    } else
                        break;
                }
            } else {
                for (int l = i - 1; l >= 0; l--) {
                    AnnotationObject al = annotations.get(l);
                    Point2D aal = theDocument.getAnchor(al);
                    Point2D cpl = theDocument.getControlPoint(al);

                    if (aal.getX() < cpi.getX())
                        break;
                    if (cpl.getY() < cpi.getY()) {
                        cpl.setLocation(cpl.getX(), cpi.getY());
                        ai = al;
                        aai = aal;
                        cpi = cpl;
                    } else
                        break;
                }
            }
        }

        // fire events
        if (init)
            theDocument.fireDocumentInit();
        else
            theDocument.fireDocumentChanged();
    }

    public boolean canGroupSelections() {
        return theDocument.canGroup(selections);
    }

    public void groupSelections() {
        theDocument.group(selections);
    }

    public void ungroupSelections() {
        theDocument.ungroup(selections, this);
    }

    // events
    public void documentInit(BaseDocument.DocumentChangeEvent e) {
        updateChart();
        //updateDrawArea(false);
        resetSelection();
        updateView();
        updateView();
    }

    public void documentChanged(BaseDocument.DocumentChangeEvent e) {
        updateDrawArea(false);
        resetSelection();
        thePlot.getRenderer().setPaint(theOptions.SPECTRUM_COLOR);
        repaint();
    }

    public void addSelectionChangeListener(SelectionChangeListener l) {
        if (l != null)
            listeners.add(l);
    }

    public void removeSelectionChangeListener(SelectionChangeListener l) {
        if (l != null)
            listeners.remove(l);
    }

    public void fireUpdatedSelection() {
        for (Iterator<SelectionChangeListener> i = listeners.iterator(); i.hasNext();)
            i.next().selectionChanged(new SelectionChangeEvent(this));
        repaint();
    }

    public void updateView() {
        updateDrawArea(true);
        updateData();
        repaint();
    }

    private void updateChart() {
        String x_label = "m/z ratio";
        String y_label = (theDocument.isShowRelativeIntensities()) ? "Intensity %" : "Intensity";
        theDataset = new DefaultXYDataset();

        if (theDocument.getPeakData() != null) {
            theChart = org.jfree.chart.ChartFactory.createScatterPlot(null, x_label, y_label, theDataset,
                    org.jfree.chart.plot.PlotOrientation.VERTICAL, false, false, false);
            thePlot = (XYPlot) theChart.getPlot();
            thePlot.setRenderer(new StandardXYItemRenderer(StandardXYItemRenderer.LINES));
        } else {
            theChart = org.jfree.chart.ChartFactory.createScatterPlot(null, x_label, y_label,
                    new XYBarDataset(theDataset, 0.001), org.jfree.chart.plot.PlotOrientation.VERTICAL, false,
                    false, false);
            thePlot = (XYPlot) theChart.getPlot();
            thePlot.setRenderer(new XYBarRenderer());
        }

        if (theDocument.isShowRelativeIntensities() && theOptions.SHOW_MAX_INTENSITY) {
            // set second axis
            ValueAxis second_axis = new NumberAxis("");
            thePlot.setRangeAxis(1, second_axis);

            // set dataset
            maxIntensityDataset = new DefaultXYDataset();
            thePlot.setDataset(1, maxIntensityDataset);
            thePlot.mapDatasetToRangeAxis(1, 1);

            // set invisible renderer
            StandardXYItemRenderer r = new StandardXYItemRenderer(StandardXYItemRenderer.SHAPES);
            r.setBaseShapesVisible(false);
            thePlot.setRenderer(1, r);
        }

        theChart.setBackgroundPaint(Color.white);
        theChart.setBorderVisible(false);
        thePlot.getRenderer().setPaint(theOptions.SPECTRUM_COLOR);
        thePlot.setOutlinePaint(null);
        thePlot.setDomainGridlinesVisible(false);
        thePlot.setRangeGridlinesVisible(false);
    }

    private void updateData() {

        double[][] data = theDocument.getData(data_area, false);

        if (theDocument.isShowRelativeIntensities() && data != null && data[0].length > 0) {
            // get max intensity
            double min_int = data[1][0];
            double max_int = data[1][0];
            for (int i = 1; i < data[1].length; i++) {
                max_int = Math.max(max_int, data[1][i]);
                min_int = Math.min(min_int, data[1][i]);
            }

            // normalize
            for (int i = 0; i < data[1].length; i++)
                data[1][i] = 100. * data[1][i] / max_int;

            // set max intensity data
            if (maxIntensityDataset != null) {
                double[][] s = new double[2][];
                s[0] = new double[] { data[0][0], data[0][data[0].length - 1] };
                s[1] = new double[] { min_int, max_int };
                maxIntensityDataset.removeSeries("max");
                maxIntensityDataset.addSeries("max", s);

                ((NumberAxis) thePlot.getRangeAxis(1))
                        .setTickUnit(new org.jfree.chart.axis.NumberTickUnit(max_int));
            }
        }

        // set peak data
        theDataset.removeSeries("intensities");
        theDataset.addSeries("intensities", data);

    }

    protected void updateDrawArea(boolean update_chart) {

        // update data area
        if (update_chart) {
            draw_area = theOptions.getDefaultDrawArea();
            chart_area = theOptions.getDefaultChartArea();
            data_area = null;

            paintChart(GraphicUtils.createImage(theOptions.getDefaultViewDimension(), true).createGraphics());
        }

        // update glycan bboxes
        Rectangle all_bbox = computeRectangles();

        // update draw area
        int add_left = Math.max(0, chart_area.x - all_bbox.x);
        int add_top = Math.max(0, chart_area.y - all_bbox.y);
        int add_right = Math.max(0, all_bbox.x + all_bbox.width - chart_area.x - chart_area.width);
        int add_bottom = Math.max(0, all_bbox.y + all_bbox.height - chart_area.y - chart_area.height);

        draw_area.width = chart_area.width + add_left + add_right + 2 * theOptions.CHART_X_MARGIN;
        draw_area.height = chart_area.height + add_top + add_bottom + 2 * theOptions.CHART_Y_MARGIN;

        chart_area.x = draw_area.x + add_left + theOptions.CHART_X_MARGIN;
        chart_area.y = draw_area.y + add_top + theOptions.CHART_Y_MARGIN;
    }

    public void mouseEntered(MouseEvent e) {
    }

    public void mouseExited(MouseEvent e) {
    }

    public boolean isInResizeCorner(AnnotationObject selection, Point p) {
        Rectangle rect = rectangles_complete.get(selection);

        int size = 2 * theOptions.ANNOTATION_MARGIN / 3;

        Rectangle corner1 = new Rectangle(left(rect), top(rect), size, size);
        if (corner1.contains(p))
            return true;

        Rectangle corner2 = new Rectangle(right(rect) - size, top(rect), size, size);
        if (corner2.contains(p))
            return true;

        return false;
    }

    public void mousePressed(MouseEvent e) {
        if (MouseUtils.isPushTrigger(e) || MouseUtils.isCtrlPushTrigger(e)) {
            start_position = getAnnotationAtPoint(e.getPoint());
            mouse_start_point = e.getPoint();
            mouse_end_point = null;

            if (start_position != null) {
                if (isInResizeCorner(start_position, mouse_start_point)) {
                    // start resizing
                    setSelection(start_position);
                    is_resizing = true;
                } else {
                    // start DnD
                    if (!isSelected(start_position))
                        setSelection(start_position);
                    is_dragndrop = true;
                }
            } else {
                start_position = getCPAtPoint(e.getPoint());
                if (start_position != null) {
                    // start moving cp
                    setSelection(start_position);
                    is_movingcp = true;
                } else {
                    // start selection
                    is_dragndrop = false;
                }
            }
        }
        was_dragged = false;
    }

    public void mouseMoved(MouseEvent e) {
    }

    public void mouseDragged(MouseEvent e) {
        was_dragged = true;

        // if is dragging don't update selection
        if (is_dragndrop) {
            if (mouse_end_point != null)
                xorSelections(mouse_start_point, mouse_end_point);
            mouse_end_point = e.getPoint();
            xorSelections(mouse_start_point, mouse_end_point);
        } else if (is_resizing) {
            if (mouse_end_point != null)
                xorResizing(start_position, mouse_start_point, mouse_end_point);
            mouse_end_point = e.getPoint();
            xorResizing(start_position, mouse_start_point, mouse_end_point);
        } else if (is_movingcp) {
            if (mouse_end_point != null)
                xorConnection(start_position, mouse_start_point, mouse_end_point);
            mouse_end_point = e.getPoint();
            xorConnection(start_position, mouse_start_point, mouse_end_point);
        } else if (mouse_start_point != null) {
            if (mouse_end_point != null)
                xorRectangle(mouse_start_point, mouse_end_point);
            mouse_end_point = e.getPoint();
            xorRectangle(mouse_start_point, mouse_end_point);
        }

        dragAndScroll(e);
    }

    public void mouseReleased(MouseEvent e) {

        // Drag and drop
        if (is_dragndrop && was_dragged) {
            if (mouse_end_point != null)
                xorSelections(mouse_start_point, mouse_end_point);

            moveSelections(e.getPoint().x - mouse_start_point.x, e.getPoint().y - mouse_start_point.y);
        } else if (is_resizing && was_dragged) {
            if (mouse_end_point != null)
                xorResizing(start_position, mouse_start_point, mouse_end_point);
            rescaleSelections(scaleFactor(start_position, mouse_start_point, e.getPoint()));
        } else if (is_movingcp && was_dragged) {
            if (mouse_end_point != null)
                xorConnection(start_position, mouse_start_point, mouse_end_point);
            moveControlPointTo(start_position, e.getPoint());
        } else if (mouse_start_point != null) {
            if (mouse_end_point != null)
                xorRectangle(mouse_start_point, mouse_end_point);

            Rectangle mouse_rect = makeRectangle(mouse_start_point, e.getPoint());
            if (MouseUtils.isNothingPressed(e))
                setSelection(getAnnotationsInside(mouse_rect));
            else if (MouseUtils.isCtrlPressed(e))
                addSelection(getAnnotationsInside(mouse_rect));
        }

        // reset
        start_position = null;
        is_resizing = false;
        is_movingcp = false;
        is_dragndrop = false;
        was_dragged = false;
        mouse_start_point = null;
        mouse_end_point = null;
        repaint();
    }

    public void mouseClicked(MouseEvent e) {
        AnnotationObject a = getAnnotationAtPoint(e.getPoint());
        if (a == null)
            a = getConnectionAtPoint(e.getPoint());

        if (a != null) {
            if (MouseUtils.isAddSelectTrigger(e) || MouseUtils.isSelectAllTrigger(e))
                addSelection(a);
            else if (MouseUtils.isSelectTrigger(e))
                setSelection(a);
        } else
            resetSelection();
    }

    private void dragAndScroll(MouseEvent e) {
        // move view if near borders                
        Point point = e.getPoint();
        JViewport view = theScrollPane.getViewport();
        Rectangle inner = view.getViewRect();
        inner.grow(-10, -10);

        if (!inner.contains(point)) {
            Point orig = view.getViewPosition();
            if (point.x < inner.x)
                orig.x -= 10;
            else if (point.x > (inner.x + inner.width))
                orig.x += 10;
            if (point.y < inner.y)
                orig.y -= 10;
            else if (point.y > (inner.y + inner.height))
                orig.y += 10;

            int maxx = getBounds().width - view.getViewRect().width;
            int maxy = getBounds().height - view.getViewRect().height;
            if (orig.x < 0)
                orig.x = 0;
            if (orig.x > maxx)
                orig.x = maxx;
            if (orig.y < 0)
                orig.y = 0;
            if (orig.y > maxy)
                orig.y = maxy;

            view.setViewPosition(orig);
        }
    }

}