/*
 * File:     TimeSeriesViewer.java
 * Project:  MPI Linguistic Application
 * Date:     03 April 2006
 *
 * Copyright (C) 2001-2006  Max Planck Institute for Psycholinguistics
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package mpi.eudico.client.annotator.viewer;

import mpi.eudico.client.annotator.Constants;
import mpi.eudico.client.annotator.ElanLocale;

import mpi.eudico.client.annotator.timeseries.AbstractTSTrack;
import mpi.eudico.client.annotator.timeseries.ExtractDataMultiStep;
import mpi.eudico.client.annotator.timeseries.TSRulerImpl;
import mpi.eudico.client.annotator.timeseries.TSTrackManager;
import mpi.eudico.client.annotator.timeseries.TSTrackPanelImpl;
import mpi.eudico.client.annotator.timeseries.TimeSeriesChangeEvent;
import mpi.eudico.client.annotator.timeseries.TimeSeriesChangeListener;
import mpi.eudico.client.annotator.timeseries.config.TSTrackConfiguration;

import mpi.eudico.client.mediacontrol.ControllerEvent;

import mpi.eudico.server.corpora.clom.Transcription;

import mpi.eudico.server.corpora.clomimpl.abstr.TranscriptionImpl;

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.image.BufferedImage;

import java.io.File;
import java.io.IOException;

import java.util.ArrayList;
import java.util.List;

import javax.swing.JCheckBoxMenuItem;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JScrollBar;
import javax.swing.SwingConstants;
import javax.swing.UIManager;


/**
 * A viewer that can handle one or more TimeSeriesTrack panels,  adding a
 * horizontal ruler, a crosshair, selection (time interval) etc.
 */
public class TimeSeriesViewer extends DefaultTimeScaleBasedViewer
    implements AdjustmentListener, TimeSeriesChangeListener {
    private TranscriptionImpl transcription;
    private TSTrackManager trackManager;
    private final String ADD_PR = "add-";
    private final String REM_PR = "rem-";
    private final String RANGE = "ran-";
    private final int DEF_PANEL_HEIGHT = 300;

    //private BufferedImage bi;
    //private Graphics2D big2d;
    // a separate buf img for the hor. ruler
    private BufferedImage ri;
    private Graphics2D rug2d;

    //private int imageWidth;
    //private int imageHeight;
    //private Font font;
    //private FontMetrics metrics;
    //private int rulerHeight;
    private int vertRulerWidth;

    //private TimeRuler ruler;
    private int verticalScrollOffset;

    //private AffineTransform identity;
    //private long crossHairTime;
    //private int crossHairPos;
    //private long intervalBeginTime;
    //private long intervalEndTime;
    //private long selectionBeginTime;
    //private long selectionEndTime;
    //private int selectionBeginPos;
    //private int selectionEndPos;
    //private AlphaComposite alpha04;
    //private AlphaComposite alpha07;
    //private float msPerPixel;
    // vertical scrolling
    private JScrollBar scrollBar;

    /** default scrollbar width */
    private final int defBarWidth;

    /** a list of trackpanels */
    private List trackPanels;
    private int trackPanelHeight;
    private Insets panelMargins;
    private TSTrackPanelImpl selTrackPanel;
    private boolean autoFitVertical = true;

    /** menu's and menu items */
    private JCheckBoxMenuItem fitVerticalMI;
    private JMenu trPanelMenu;
    private JMenuItem addPanelMI;
    private JMenuItem removePanelMI;
    private JMenu setRangeMenu;
    private JMenu addTrackMenu;
    private JMenu removeTrackMenu;
    private JMenuItem extractDataMI;
    private JMenuItem configureTrMI;

    /**
     * Constructor.
     */
    public TimeSeriesViewer() {
        super();
        initViewer();
        defBarWidth = getDefaultBarWidth();

        paintBuffer();
        addMouseWheelListener(this);
        addEmptyTrackPanel();
    }

    /**
     * Constructor. The specified transcription is used as a document   within
     * ELAN this viewer is associated with.
     *
     * @param transcription the transcription
     */
    public TimeSeriesViewer(Transcription transcription) {
        this();
        this.transcription = (TranscriptionImpl) transcription;

        //testTrack();
    }

    /**
     * Constructor. The specified transcription is used as a document   within
     * ELAN this viewer is associated with.
     *
     * @param transcription the transcription
     * @param trackManager the track manager
     */
    public TimeSeriesViewer(Transcription transcription,
        TSTrackManager trackManager) {
        this();
        this.transcription = (TranscriptionImpl) transcription;
        this.trackManager = trackManager;

        if (trackManager != null) {
            // register as a listener
            trackManager.addTimeSeriesChangeListener(this);
        }

        //testTrack();
    }

    /**
     * Returns the track manager.
     *
     * @return the track manager
     */
    public TSTrackManager getTrackManager() {
        return trackManager;
    }

    /**
     * Sets the track manager.
     *
     * @param manager the track manager
     */
    public void setTrackManager(TSTrackManager manager) {
        trackManager = manager;

        if (trackManager != null) {
            // register as a listener
            trackManager.addTimeSeriesChangeListener(this);
        }
    }

    /**
     * Performs the initialization of fields and sets up the viewer.<br>
     */
    protected void initViewer() {
        super.initViewer();
        trackPanels = new ArrayList(4);

        //font = Constants.DEFAULTFONT;
        //setFont(font);
        //metrics = getFontMetrics(font);
        //ruler = new TimeRuler(font, TimeFormatter.toString(0));
        //rulerHeight = ruler.getHeight();
        vertRulerWidth = 43;

        //msPerPixel = 10f;
        //crossHairTime = 0L;
        //crossHairPos = 100;
        //selectionBeginTime = 0L;
        //selectionEndTime = 0L;
        //selectionBeginPos = 150;
        //selectionEndPos = 250;
        //alpha04 = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.4f);
        //alpha07 = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.7f);
        //dragStartTime = 0;
        //imageWidth = 0;
        //imageHeight = 0;
        trackPanelHeight = DEF_PANEL_HEIGHT;
        panelMargins = new Insets(3, 3, 3, 0);

        verticalScrollOffset = 0;
        scrollBar = new JScrollBar(JScrollBar.VERTICAL, 0, 50, 0, 200);
        scrollBar.setBlockIncrement(trackPanelHeight);

        //scrollBar.setUnitIncrement(5);
        scrollBar.addAdjustmentListener(this);

        //setLayout(null);
        add(scrollBar);
    }

    /**
     * Retrieves the default, platform specific width of a scrollbar.
     *
     * @return the default width, or 20 when not found
     */
    private int getDefaultBarWidth() {
        int width = 20;

        if (UIManager.getDefaults().get("ScrollBar.width") != null) {
            width = ((Integer) (UIManager.getDefaults().get("ScrollBar.width"))).intValue();
        }

        return width;
    }

    /**
     * Override <code>JComponent</code>'s paintComponent to paint:<br>
     * - a BufferedImage with a ruler and the tags<br>
     * - the cursor / crosshair
     *
     * @param g the graphics object
     */
    public void paintComponent(Graphics g) {
        super.paintComponent(g);

        Graphics2D g2d = (Graphics2D) g;
        int h = getHeight();

        if (h > imageHeight) {
            g2d.setColor(Color.WHITE);
            g2d.fillRect(0, 0, imageWidth, h);

            if (bi != null) {
                // keeps the image at the bottom, with the hor. ruler
                g2d.drawImage(bi, 0, h - imageHeight - verticalScrollOffset,
                    null);
            }
        } else {
            if (bi != null) {
                g2d.drawImage(bi, 0, -verticalScrollOffset, null);
            }
        }

        if (ri != null) {
            g2d.drawImage(ri, 0, h - rulerHeight, null);
        }

        // paint selection: here or in paintBuffer
        // paint selection, not in the vertical ruler
        if ((selectionBeginPos != selectionEndPos) &&
                (selectionEndPos > vertRulerWidth)) {
            g2d.setColor(Constants.SELECTIONCOLOR);
            g2d.setComposite(alpha05);

            if (selectionBeginPos < vertRulerWidth) {
                selectionBeginPos = vertRulerWidth;
            }

            g2d.fillRect(selectionBeginPos, h - rulerHeight,
                (selectionEndPos - selectionBeginPos), rulerHeight);

            //g2d.setComposite(alpha07);
            g2d.fillRect(selectionBeginPos, 0,
                (selectionEndPos - selectionBeginPos), h - rulerHeight);
            g2d.setComposite(AlphaComposite.Src);
        }

        if ((crossHairPos >= vertRulerWidth) && (crossHairPos <= imageWidth)) {
            // prevents drawing outside the component on Mac
            g2d.setColor(Constants.CROSSHAIRCOLOR);
            g2d.drawLine(crossHairPos, 0, crossHairPos, h);
        }
    }

    /**
     * Paint to a buffer.<br>
     * First paint the top ruler, next the annotations of the current tier.
     */
    private void paintBuffer() {
        if ((getWidth() <= 0) || (getHeight() <= 0)) {
            return;
        }

        if (imageWidth != (getWidth() - defBarWidth)) {
            imageWidth = getWidth() - defBarWidth;
        }

        int h = (trackPanels.size() * trackPanelHeight) + rulerHeight;

        if (imageHeight != h) {
            imageHeight = (getHeight() > h) ? getHeight() : h;
        }

        intervalEndTime = intervalBeginTime +
            (int) (intervalWidth * msPerPixel);

        if ((bi == null) || (bi.getWidth() < imageWidth) ||
                (bi.getHeight() < imageHeight)) {
            bi = new BufferedImage(imageWidth, imageHeight,
                    BufferedImage.TYPE_INT_RGB);
            big2d = bi.createGraphics();
        }

        big2d.setColor(Color.WHITE);
        big2d.fillRect(0, 0, imageWidth, bi.getHeight());

        TSTrackPanelImpl panel;
        int y = imageHeight - rulerHeight;

        for (int i = 0; i < trackPanels.size(); i++) {
            panel = (TSTrackPanelImpl) trackPanels.get(i);
            y -= panel.getHeight();
            big2d.translate(0, y);
            panel.paint(big2d, intervalBeginTime);
            big2d.translate(0, -y);

            //big2d.setTransform(identity);		
        }

        big2d.setTransform(identity); //reset transform

        /*paint time ruler */
        if ((ri == null) || (ri.getWidth() < imageWidth) ||
                (ri.getHeight() != rulerHeight)) {
            ri = new BufferedImage(imageWidth, rulerHeight,
                    BufferedImage.TYPE_INT_RGB);
            rug2d = ri.createGraphics();
        }

        rug2d.setColor(Constants.DEFAULTBACKGROUNDCOLOR);
        rug2d.fillRect(0, 0, imageWidth, rulerHeight);
        rug2d.setColor(Color.DARK_GRAY);
        rug2d.drawLine(0, 0, imageWidth, 0);
        rug2d.setColor(Constants.DEFAULTFOREGROUNDCOLOR);
        rug2d.translate(-(intervalBeginTime / msPerPixel) + vertRulerWidth, 0.0);

        //rug2d.translate(vertRulerWidth, 0);
        //big2d.translate(vertRulerWidth, y);
        ruler.paint(rug2d, intervalBeginTime, imageWidth, msPerPixel,
            SwingConstants.TOP);
        rug2d.setTransform(identity); //reset transform

        //big2d.setTransform(identity); //reset transform
        repaint();
    }

    /**
     * Adds a trackpanel to the viewer. The panel's settings are forced to
     * comply to the viewer's settings.
     *
     * @param trackPanel the trackpanel to add
     */
    public void addTSTrackPanel(TSTrackPanelImpl trackPanel) {
        trackPanel.setHeight(trackPanelHeight);
        trackPanel.setWidth(getWidth() - defBarWidth);
        trackPanel.setMargin(panelMargins);
        trackPanel.setRulerWidth(vertRulerWidth - panelMargins.left);
        trackPanel.setMsPerPixel(msPerPixel);
        trackPanels.add(trackPanel);
        adjustPanelHeight();
        paintBuffer();
        updateScrollBar();
    }

    /**
     * Adds an empty default track panel.
     */
    public void addEmptyTrackPanel() {
        TSTrackPanelImpl tsp = new TSTrackPanelImpl();
        TSRulerImpl ruler = new TSRulerImpl();
        ruler.setFont(Constants.SMALLFONT);
        tsp.setRuler(ruler);
        addTSTrackPanel(tsp);
    }

    /**
     * Removes a trackpanel from the viewer
     *
     * @param trackPanel the trackpanel to remove
     */
    public void removeTSTrackPanel(TSTrackPanelImpl trackPanel) {
        trackPanels.remove(trackPanel);
        adjustPanelHeight();
        paintBuffer();
        updateScrollBar();
    }

    /**
     * Returns the width for the vertical ruler.
     *
     * @return the width for the vertical ruler
     */
    public int getVerticalRulerWidth() {
        return vertRulerWidth;
    }

    /**
     * Sets the width for the vertical ruler.
     *
     * @param rulerWidth the width for the vertical ruler
     */
    public void setVerticalRulerWidth(int rulerWidth) {
        vertRulerWidth = rulerWidth;
    }

    /**
     * Returns the vertical scroll offset.
     *
     * @return the vertical scroll offset
     */
    public int getVerticalScrollOffset() {
        return verticalScrollOffset;
    }

    /**
     * Sets the vertical scroll offset on this component.
     *
     * @param offset the new vertical scroll offset
     */
    public void setVerticalScrollOffset(int offset) {
        verticalScrollOffset = offset;
        repaint();
    }

    /**
     * Returns the current number of trackpanels.
     *
     * @return the number of trackpanels
     */
    public int getNumberOfTrackPanels() {
        return trackPanels.size();
    }

    /**
     * Sets the number of trackpanels. Adds a number of empty trackpanels
     * untill there are the given number of panels. If there are already
     * more panels nothing happens.
     *
     * @param num the number of panels
     */
    public void setNumberOfTrackPanels(int num) {
        int curSize = trackPanels.size();

        for (int i = 0; i < (num - curSize); i++) {
            addEmptyTrackPanel();
        }
    }

    /**
     * Returns the names of the tracks of the trackpanel at the specified index.
     *
     * @param panelIndex index of the panel
     *
     * @return an array of track names
     */
    public String[] getTracksForPanel(int panelIndex) {
        if ((panelIndex < 0) || (panelIndex >= trackPanels.size())) {
            return null; // rather than ArrayIndexOutOfBoundsException
        }

        TSTrackPanelImpl panel = (TSTrackPanelImpl) trackPanels.get(panelIndex);
        List tracks = panel.getTracks();
        AbstractTSTrack tr;
        String[] names = new String[tracks.size()];

        for (int i = 0; i < tracks.size(); i++) {
            tr = (AbstractTSTrack) tracks.get(i);
            names[i] = tr.getName();
        }

        return names;
    }

    /**
     * Tells the viewer to add the given predefined tracks to the panel
     * at index <code>panelIndex</code>.
     *
     * @param panelIndex the index of the panel
     * @param trackNames the names of the tracks to add
     */
    public void setTracksForPanel(int panelIndex, String[] trackNames) {
        if ((panelIndex < 0) || (panelIndex >= trackPanels.size()) ||
                (trackNames == null)) {
            return; // rather than ArrayIndexOutOfBoundsException
        }

        TSTrackPanelImpl panel = (TSTrackPanelImpl) trackPanels.get(panelIndex);
        String name;
        AbstractTSTrack tr;

        for (int i = 0; i < trackNames.length; i++) {
            name = trackNames[i];

            if (name != null) {
                tr = trackManager.getTrack(name);

                if (tr != null) {
                    panel.addTrack(tr);
                    panel.getRuler().setRange(tr.getRange());
                    panel.getRuler().setUnitString(tr.getUnitString());
                }
            }
        }

        paintBuffer();
    }

    /**
     * Update the values of the scrollbar.<br>
     * Called after a change in the number of visible tracks.
     */
    private void updateScrollBar() {
        int value = scrollBar.getValue();
        int max = (imageHeight > getHeight()) ? imageHeight : getHeight();

        // before changing scrollbar values do a setValue(0), otherwise
        // setMaxiimum and/or setVisibleAmount will not be accurate
        scrollBar.setValue(0);
        scrollBar.setMaximum(max);
        scrollBar.setVisibleAmount(getHeight());

        if ((value + getHeight()) > max) {
            value = max - getHeight();
        }

        scrollBar.setValue(value);
        scrollBar.revalidate();
    }

    /**
     * @see mpi.eudico.client.annotator.viewer.DefaultTimeScaleBasedViewer#getLeftMargin()
     */
    public int getLeftMargin() {
        return vertRulerWidth;
    }

    /**
     * @see mpi.eudico.client.annotator.viewer.DefaultTimeScaleBasedViewer#getRightMargin()
     */
    public int getRightMargin() {
        return scrollBar.getWidth();
    }

    /**
     * @see mpi.eudico.client.annotator.viewer.DefaultTimeScaleBasedViewer#createPopupMenu()
     */
    protected void createPopupMenu() {
        super.createPopupMenu();
        timeScaleConMI.setEnabled(false);
        fitVerticalMI = new JCheckBoxMenuItem(ElanLocale.getString(
                    "TimeSeriesViewer.TrackPanel.FitVertically"));
        fitVerticalMI.setSelected(autoFitVertical);
        fitVerticalMI.addActionListener(this);
        popup.add(fitVerticalMI);
        popup.addSeparator();

        addPanelMI = new JMenuItem(ElanLocale.getString(
                    "TimeSeriesViewer.TrackPanel.AddPanel"));
        addPanelMI.addActionListener(this);
        popup.add(addPanelMI);
        removePanelMI = new JMenuItem(ElanLocale.getString(
                    "TimeSeriesViewer.TrackPanel.RemovePanel"));
        removePanelMI.addActionListener(this);
        popup.add(removePanelMI);
        popup.addSeparator();

        trPanelMenu = new JMenu(ElanLocale.getString(
                    "TimeSeriesViewer.TrackPanel"));
        popup.add(trPanelMenu);
        setRangeMenu = new JMenu(ElanLocale.getString(
                    "TimeSeriesViewer.TrackPanel.SetRange"));
        trPanelMenu.add(setRangeMenu);
        addTrackMenu = new JMenu(ElanLocale.getString(
                    "TimeSeriesViewer.Track.Add"));
        trPanelMenu.add(addTrackMenu);
        removeTrackMenu = new JMenu(ElanLocale.getString(
                    "TimeSeriesViewer.Track.Remove"));
        trPanelMenu.add(removeTrackMenu);
        popup.addSeparator();
        extractDataMI = new JMenuItem(ElanLocale.getString(
                    "TimeSeriesViewer.Extract"));
        extractDataMI.addActionListener(this);
        popup.add(extractDataMI);
        configureTrMI = new JMenuItem(ElanLocale.getString(
                    "TimeSeriesViewer.Tracks.Configure"));
        configureTrMI.addActionListener(this);
        popup.add(configureTrMI);
    }

    /**
     * @see mpi.eudico.client.annotator.viewer.DefaultTimeScaleBasedViewer#updatePopup(java.awt.Point)
     */
    protected void updatePopup(Point p) {
        selTrackPanel = null;
        removePanelMI.setEnabled(false);
        trPanelMenu.setEnabled(false);

        if (!pointInHorizontalRuler(p.y)) {
            Point inverse = new Point(p);
            inverse.y += verticalScrollOffset;
            selTrackPanel = getPanelAtY(inverse.y);

            if (selTrackPanel != null) {
                removePanelMI.setEnabled(true);
                trPanelMenu.setEnabled(true);
            }
        }

        addTrackMenu.removeAll();
        removeTrackMenu.removeAll();
        setRangeMenu.removeAll();

        if (selTrackPanel == null) {
            return;
        }

        if (trackManager != null) {
            ArrayList trs = trackManager.getRegisteredTracks();

            if ((trs.size() == 0) || (transcription.getTiers().size() == 0)) {
                extractDataMI.setEnabled(false);
            } else {
                extractDataMI.setEnabled(true);
            }

            AbstractTSTrack tra;

            for (int i = 0; i < trs.size(); i++) {
                tra = (AbstractTSTrack) trs.get(i);

                if ((tra != null) && !selTrackPanel.getTracks().contains(tra)) {
                    JMenuItem it = new JMenuItem(tra.getName());
                    it.setActionCommand(ADD_PR + tra.getName());
                    it.addActionListener(this);
                    addTrackMenu.add(it);
                }
            }

            List curTracks = selTrackPanel.getTracks();

            for (int i = 0; i < curTracks.size(); i++) {
                tra = (AbstractTSTrack) curTracks.get(i);

                if (tra != null) {
                    JMenuItem it = new JMenuItem(tra.getName());
                    it.setActionCommand(REM_PR + tra.getName());
                    it.addActionListener(this);
                    removeTrackMenu.add(it);

                    JMenuItem is = new JMenuItem(tra.getName());
                    is.setActionCommand(RANGE + tra.getName());
                    is.addActionListener(this);
                    setRangeMenu.add(is);
                }
            }
        }
    }

    /**
     * Integrate the vertical ruler's width in calculation.
     *
     * @see mpi.eudico.client.annotator.viewer.DefaultTimeScaleBasedViewer#timeAt(int)
     */
    public long timeAt(int x) {
        return intervalBeginTime + (int) ((x - vertRulerWidth) * msPerPixel);
    }

    /**
     * Integrate the vertical ruler's width in calculation.
     *
     * @see mpi.eudico.client.annotator.viewer.DefaultTimeScaleBasedViewer#xAt(long)
     */
    public int xAt(long t) {
        return super.xAt(t) + vertRulerWidth;
    }

    /**
     * @see mpi.eudico.client.annotator.viewer.TimeScaleBasedViewer#updateTimeScale()
     */

    //public void updateTimeScale() {
    //}

    /**
     * To be implemented by each extending class.
     *
     * @param begin the interval begintime
     */
    protected void setLocalTimeScaleIntervalBeginTime(long begin) {
        if (begin == intervalBeginTime) {
            return;
        }

        intervalBeginTime = begin;
        intervalEndTime = intervalBeginTime +
            (long) (intervalWidth * msPerPixel);
        crossHairPos = xAt(crossHairTime);
        selectionBeginPos = xAt(getSelectionBeginTime());
        selectionEndPos = xAt(getSelectionEndTime());
        paintBuffer();
    }

    /**
     * To be implemented by each extending class.
     *
     * @param step new msPerPixel value
     */
    protected void setLocalTimeScaleMsPerPixel(float step) {
        if (msPerPixel == step) {
            return;
        }

        msPerPixel = step;

        /*stop the player if necessary*/
        boolean playing = playerIsPlaying();

        if (playing) {
            stopPlayer();
        }

        long mediaTime = getMediaTime();
        int oldScreenPos = crossHairPos;
        long newMediaX = (long) (mediaTime / msPerPixel);
        int numScreens;

        if (intervalWidth > 0) {
            numScreens = (int) (mediaTime / (intervalWidth * msPerPixel));
        } else {
            numScreens = 0;
        }

        int newScreenPos = (int) newMediaX - (numScreens * intervalWidth);
        int diff = oldScreenPos - newScreenPos;

        //new values
        intervalBeginTime = (long) (((numScreens * intervalWidth) - diff) * msPerPixel);

        if (intervalBeginTime < 0) {
            intervalBeginTime = 0;
        }

        intervalEndTime = intervalBeginTime +
            (long) (intervalWidth * msPerPixel);

        crossHairPos = xAt(mediaTime);
        selectionBeginPos = xAt(getSelectionBeginTime());
        selectionEndPos = xAt(getSelectionEndTime());

        TSTrackPanelImpl panel;

        for (int i = 0; i < trackPanels.size(); i++) {
            panel = (TSTrackPanelImpl) trackPanels.get(i);
            panel.setMsPerPixel(msPerPixel);
        }

        paintBuffer();

        if (playing) {
            startPlayer();
        }

        int zoom = (int) (100f * (10f / msPerPixel));

        if (zoom <= 0) {
            zoom = 100;
        }

        updateZoomPopup(zoom);
    }

    /**
     * Sets new media offset. Should only be called if all tracks in all panels
     * in the viewer should have the same offset.
     *
     * @param offset the new media offset in ms
     */
    public void setMediaTimeOffset(long offset) {
        if (offset != mediaTimeOffset) {
            super.setMediaTimeOffset(offset);

            TSTrackPanelImpl panel;
            List tracks;
            AbstractTSTrack track;

            for (int i = 0; i < trackPanels.size(); i++) {
                panel = (TSTrackPanelImpl) trackPanels.get(i);
                tracks = panel.getTracks();

                for (int j = 0; j < tracks.size(); j++) {
                    track = (AbstractTSTrack) tracks.get(j);

                    if (track != null) {
                        track.setTimeOffset((int) mediaTimeOffset);
                    }
                }
            }
        }
    }

    /**
     * @see DefaultTimeScaleBasedViewer#pointInHorizontalRuler(int)
     */
    protected boolean pointInHorizontalRuler(int yPos) {
        int h = getHeight();

        return (yPos <= h) && (yPos >= (h - rulerHeight));
    }

    /**
     * Finds and returns the track panel at the specified y-coordinate.
     *
     * @param y the y coordinate
     *
     * @return the panel at that point or null
     */
    private TSTrackPanelImpl getPanelAtY(int y) {
        TSTrackPanelImpl panel = null;
        int h = Math.max(getHeight(), imageHeight);
        int ymin = h;
        int ymax = h - rulerHeight;

        for (int i = 0; i < trackPanels.size(); i++) {
            panel = (TSTrackPanelImpl) trackPanels.get(i);
            ymin = ymax - panel.getHeight();

            if ((ymax >= y) && (ymin <= y)) {
                return panel;
            }

            ymax -= panel.getHeight();
        }

        return null;
    }

    /**
     * Change the height of the trackpanels depending on the auto fit
     * vertically flag.
     */
    private void adjustPanelHeight() {
        int numPan = trackPanels.size();

        if (numPan > 0) {
            TSTrackPanelImpl panel;

            if (autoFitVertical) {
                trackPanelHeight = (getHeight() - rulerHeight) / numPan;
            } else {
                trackPanelHeight = DEF_PANEL_HEIGHT;
            }

            for (int i = 0; i < trackPanels.size(); i++) {
                panel = (TSTrackPanelImpl) trackPanels.get(i);
                panel.setHeight(trackPanelHeight);
            }
        } else {
            if (autoFitVertical) {
                trackPanelHeight = getHeight();
            }
        }
    }

    /**
     * Create a wizard to extract data from a track to a tier, based on
     * annotation intervals of a parent tier.
     */
    private void extractTrackData() {
        // shows a modal dialog
        new ExtractDataMultiStep(transcription, trackManager);
    }

    /**
     * @see mpi.eudico.client.annotator.viewer.AbstractViewer#controllerUpdate(mpi.eudico.client.mediacontrol.ControllerEvent)
     */
    public void controllerUpdate(ControllerEvent event) {
        super.controllerUpdate(event);
    }

    /**
     * @see mpi.eudico.client.annotator.viewer.AbstractViewer#updateSelection()
     */
    public void updateSelection() {
        selectionBeginPos = xAt(getSelectionBeginTime());

        if (selectionBeginPos < vertRulerWidth) {
            selectionBeginPos = vertRulerWidth;
        }

        selectionEndPos = xAt(getSelectionEndTime());

        if (selectionEndPos < selectionBeginPos) {
            selectionEndPos = selectionBeginPos;
        }

        repaint();
    }

    /**
     * @see mpi.eudico.client.annotator.viewer.AbstractViewer#updateLocale()
     */
    public void updateLocale() {
        super.updateLocale();

        if (popup != null) {
            fitVerticalMI.setText(ElanLocale.getString(
                    "TimeSeriesViewer.TrackPanel.FitVertically"));
            addPanelMI.setText(ElanLocale.getString(
                    "TimeSeriesViewer.TrackPanel.AddPanel"));
            removePanelMI.setText(ElanLocale.getString(
                    "TimeSeriesViewer.TrackPanel.RemovePanel"));
            trPanelMenu.setText(ElanLocale.getString(
                    "TimeSeriesViewer.TrackPanel"));
            setRangeMenu.setText(ElanLocale.getString(
                    "TimeSeriesViewer.TrackPanel.SetRange"));
            addTrackMenu.setText(ElanLocale.getString(
                    "TimeSeriesViewer.Track.Add"));
            removeTrackMenu.setText(ElanLocale.getString(
                    "TimeSeriesViewer.Track.Remove"));
            extractDataMI.setText(ElanLocale.getString(
                    "TimeSeriesViewer.Extract"));
            configureTrMI.setText(ElanLocale.getString(
                    "TimeSeriesViewer.Tracks.Configure"));
        }
    }

    /**
     * @see java.awt.event.ComponentListener#componentResized(java.awt.event.ComponentEvent)
     */
    public void componentResized(ComponentEvent e) {
        TSTrackPanelImpl panel;
        intervalWidth = getWidth() - vertRulerWidth - defBarWidth;

        for (int i = 0; i < trackPanels.size(); i++) {
            panel = (TSTrackPanelImpl) trackPanels.get(i);
            panel.setWidth(getWidth() - defBarWidth);
        }

        adjustPanelHeight();

        paintBuffer();

        scrollBar.setBounds(getWidth() - defBarWidth, 0, defBarWidth,
            getHeight());
        scrollBar.revalidate();
        updateScrollBar();
    }

    /**
     * @see java.awt.event.ComponentListener#componentShown(java.awt.event.ComponentEvent)
     */
    public void componentShown(ComponentEvent e) {
        paintBuffer();

        scrollBar.setBounds(getWidth() - defBarWidth, 0, defBarWidth,
            getHeight());
        scrollBar.revalidate();
        updateScrollBar();
    }

    /**
     * Handle scrolling of the viewer image.
     *
     * @param e the event
     */
    public void adjustmentValueChanged(AdjustmentEvent e) {
        int value = e.getValue();

        setVerticalScrollOffset(value);
    }

    /**
     * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
     */
    public void actionPerformed(ActionEvent e) {
        if (e.getSource() == fitVerticalMI) {
            autoFitVertical = fitVerticalMI.isSelected();
            adjustPanelHeight();
            paintBuffer();
            updateScrollBar();

            return;
        }

        if (e.getSource() == addPanelMI) {
            addEmptyTrackPanel();

            return;
        }

        if (e.getSource() == removePanelMI) {
            if (selTrackPanel != null) {
                removeTSTrackPanel(selTrackPanel);
            }

            return;
        }

        if (e.getSource() == configureTrMI) {
            if (trackManager != null) {
                trackManager.configureTracks(this);
            }

            return;
        }

        if (e.getSource() == extractDataMI) {
            extractTrackData();

            return;
        }

        String command = e.getActionCommand();

        if ((command != null) && (command.indexOf(ADD_PR) > -1)) {
            // add Track
            String trackName = command.substring(4);

            if ((trackManager != null) && (selTrackPanel != null)) {
                AbstractTSTrack tr = trackManager.getTrack(trackName);

                if (tr != null) {
                    selTrackPanel.addTrack(tr);
                    selTrackPanel.getRuler().setRange(tr.getRange());
                    selTrackPanel.getRuler().setUnitString(tr.getUnitString());
                    paintBuffer();
                }
            }

            return;
        } else if ((command != null) && (command.indexOf(REM_PR) > -1)) {
            // remove track
            String trackName = command.substring(4);

            if (selTrackPanel != null) {
                List tracks = selTrackPanel.getTracks();
                AbstractTSTrack tr;

                for (int i = 0; i < tracks.size(); i++) {
                    tr = (AbstractTSTrack) tracks.get(i);

                    if (trackName.equals(tr.getName())) {
                        selTrackPanel.removeTrack(tr);
                        paintBuffer();
                    }
                }
            }

            return;
        } else if ((command != null) && (command.indexOf(RANGE) > -1)) {
            // setRange and unit string
            String trackName = command.substring(4);

            if (selTrackPanel != null) {
                List tracks = selTrackPanel.getTracks();
                AbstractTSTrack tr;

                for (int i = 0; i < tracks.size(); i++) {
                    tr = (AbstractTSTrack) tracks.get(i);

                    if (trackName.equals(tr.getName())) {
                        selTrackPanel.getRuler().setRange(tr.getRange());
                        selTrackPanel.getRuler().setUnitString(tr.getUnitString());
                        paintBuffer();
                    }
                }
            }

            return;
        }

        // if we get here the super implementation (zoom) will be called
        super.actionPerformed(e);
    }

    /**
     * The use of a mousewheel needs Java 1.4!<br>
     * The scroll amount of the mousewheel is the height of a tier.
     *
     * @param e the event
     */
    public void mouseWheelMoved(MouseWheelEvent e) {
        if (e.getUnitsToScroll() > 0) {
            scrollBar.setValue(scrollBar.getValue() + 25);
        } else {
            scrollBar.setValue(scrollBar.getValue() - 25);
        }
    }

    //####################################################################################
    // testing
    private void testTrack() {
        //String fileName = "D:\\MPI\\ELAN docs\\dataglove\\onno.log";
        String fileName = System.getProperty("user.dir") + File.separator +
            "glove.log";
        mpi.eudico.client.annotator.timeseries.glove.DataGloveFileReader reader = new mpi.eudico.client.annotator.timeseries.glove.DataGloveFileReader(fileName);

        try {
            int sampleRate = reader.detectSampleFrequency();
            float[] data = (float[]) reader.readTrack(10, 5);
            mpi.eudico.client.annotator.timeseries.ContinuousRateTSTrack track = new mpi.eudico.client.annotator.timeseries.ContinuousRateTSTrack();
            track.setType(mpi.eudico.client.annotator.timeseries.TimeSeriesTrack.VALUES_FLOAT_ARRAY);
            track.setRange(new float[] { -180, 180 });
            track.setColor(new Color(128, 0, 128));
            track.setSampleRate(sampleRate);
            track.setData(data);

            float[] data2 = (float[]) reader.readTrack(14, 5);
            mpi.eudico.client.annotator.timeseries.ContinuousRateTSTrack track2 = new mpi.eudico.client.annotator.timeseries.ContinuousRateTSTrack();
            track2.setType(mpi.eudico.client.annotator.timeseries.TimeSeriesTrack.VALUES_FLOAT_ARRAY);
            track2.setRange(new float[] { -180, 180 });
            track2.setColor(new Color(0, 128, 0));
            track2.setSampleRate(sampleRate);
            track2.setData(data2);

            if (trackPanels.size() > 0) {
                TSTrackPanelImpl tsp = (TSTrackPanelImpl) trackPanels.get(0);
                tsp.getRuler().setRange(track.getRange());
                tsp.addTrack(track);
                tsp.addTrack(track2);
            }
        } catch (IOException ioe) {
            System.out.println("Read error: " + ioe.getMessage());
        }
    }

    /**
     * Notification of changes in tracks and / or track sources (files).
     * Especially change and delete events should be handled here.
     *
     * @param event Dthe event
     */
    public void timeSeriesChanged(TimeSeriesChangeEvent event) {
        if (event.getEditSourceType() == TimeSeriesChangeEvent.TRACK) {
            if (event.getSource() instanceof TSTrackConfiguration ||
                    event.getSource() instanceof AbstractTSTrack) {
                AbstractTSTrack tr = null;

                if (event.getSource() instanceof TSTrackConfiguration) {
                    TSTrackConfiguration trc = (TSTrackConfiguration) event.getSource();
                    tr = (AbstractTSTrack) trc.getObject(trc.getTrackName());
                } else {
                    tr = (AbstractTSTrack) event.getSource();
                }

                AbstractTSTrack tr2;

                // find the panel 
                TSTrackPanelImpl pan;

                for (int i = 0; i < trackPanels.size(); i++) {
                    pan = (TSTrackPanelImpl) trackPanels.get(i);

                    List tracks = pan.getTracks();

                    for (int j = 0; j < tracks.size(); j++) {
                        tr2 = (AbstractTSTrack) tracks.get(j);

                        if (tr2 == tr) {
                            if (event.getEditType() == TimeSeriesChangeEvent.CHANGE) {
                                // make it the active track ??
                                pan.getRuler().setUnitString(tr.getUnitString());
                                pan.getRuler().setRange(tr2.getRange());
                                paintBuffer();

                                break;
                            } else if (event.getEditType() == TimeSeriesChangeEvent.DELETE) {
                                // check
                                boolean active = false;

                                if ((pan.getRuler().getRange()[0] == tr.getRange()[0]) &&
                                        (pan.getRuler().getRange()[1] == tr.getRange()[1]) &&
                                        ((pan.getRuler().getUnitString() != null) &&
                                        pan.getRuler().getUnitString().equals(tr.getUnitString()))) {
                                    // assume it is the active track
                                    active = true;
                                }

                                pan.removeTrack(tr2);

                                if (active && (pan.getTracks().size() > 0)) {
                                    pan.getRuler().setRange(((AbstractTSTrack) tracks.get(
                                            0)).getRange());
                                    pan.getRuler().setUnitString(((AbstractTSTrack) tracks.get(
                                            0)).getUnitString());
                                }

                                paintBuffer();

                                break;
                            }
                        }
                    }
                }
            }
        }
    }
}
