// **********************************************************************
//
// Copyright (c) 2003-2016 ZeroC, Inc. All rights reserved.
//
// This copy of Ice is licensed to you under the terms described in the
// ICE_LICENSE file included in this distribution.
//
// **********************************************************************

package IceGridGUI.LiveDeployment;

import java.util.Map;
import java.util.List;
import java.util.Stack;
import java.util.HashMap;
import java.util.Date;
import java.util.TimeZone;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.Color;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.event.KeyEvent;

import javax.swing.AbstractAction;
import javax.swing.AbstractCellEditor;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.border.Border;
import javax.swing.DefaultCellEditor;
import javax.swing.DefaultListCellRenderer;
import javax.swing.event.TableModelEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JColorChooser;
import javax.swing.colorchooser.AbstractColorChooserPanel;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSpinner;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.JToolBar;
import javax.swing.JOptionPane;
import javax.swing.KeyStroke;
import javax.swing.ListCellRenderer;
import javax.swing.SwingUtilities;
import javax.swing.SpinnerNumberModel;
import javax.swing.JSplitPane;
import javax.swing.WindowConstants;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;

import java.text.DecimalFormat;

import javafx.application.Platform;
import javafx.embed.swing.JFXPanel;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.event.EventHandler;
import javafx.scene.input.MouseButton;
import javafx.scene.input.DragEvent;
import javafx.scene.input.TransferMode;
import javafx.scene.input.DataFormat;
import javafx.scene.input.Dragboard;
import javafx.util.StringConverter;

import com.jgoodies.forms.layout.FormLayout;
import com.jgoodies.forms.builder.DefaultFormBuilder;
import com.jgoodies.forms.factories.Borders;
import com.jgoodies.looks.Options;
import com.jgoodies.looks.HeaderStyle;
import com.jgoodies.looks.BorderStyle;
import com.jgoodies.looks.plastic.PlasticLookAndFeel;

import IceGridGUI.*;
import IceGridGUI.LiveDeployment.MetricsViewEditor.MetricsViewInfo;
import IceGridGUI.LiveDeployment.MetricsViewEditor.MetricsCell;
import IceGridGUI.LiveDeployment.MetricsViewEditor.MetricsViewTransferableData;
import IceGridGUI.LiveDeployment.MetricsViewEditor.FormatedNumberRenderer;

import java.util.prefs.Preferences;
import java.util.prefs.BackingStoreException;

public class GraphView extends JFrame implements MetricsFieldContext, Coordinator.IGraphView
{
    class TimeFormatter extends StringConverter<java.lang.Number>
    {
        TimeFormatter(String format)
        {
            setDateFormat(format);
            _dateFormat.setTimeZone(TimeZone.getDefault());
        }

        @Override
        public synchronized String toString(Number timestamp)
        {
            Date date = new Date(timestamp.longValue());
            if(date.getTime() < 1000)
            {
                //
                // When the x axis is first draw we don't have times to display.
                //
                return "";
            }
            else
            {
                return _dateFormat.format(date);
            }
        }

        @Override
        public synchronized Number fromString(String time)
        {
            try
            {
                long t = _dateFormat.parse(time).getTime();
                return t;
            }
            catch(java.text.ParseException e)
            {
                return 0;
            }
        }

        public synchronized void setDateFormat(String format)
        {
            _dateFormat = new SimpleDateFormat(format);
        }

        private DateFormat _dateFormat;
    }

    class TransferHandler extends javax.swing.TransferHandler
    {
        @Override
        public boolean 
        canImport(TransferHandler.TransferSupport support)
        {
            boolean supported = false;
            for(DataFlavor f : support.getDataFlavors())
            {
                if(f.equals(MetricsViewTransferableData.dataFlavor()))
                {
                    supported = true;
                    break;
                }
            }
            return supported;
        }

        @Override
        public boolean
        importData(TransferHandler.TransferSupport support)
        {
            if(!canImport(support))
            {
                return false;
            }

            Transferable t = support.getTransferable();
            
            for(DataFlavor flavor : support.getDataFlavors())
            {
                try
                {
                    if(flavor.equals(MetricsViewTransferableData.dataFlavor()))
                    {
                        addSeries((MetricsViewTransferableData)t.getTransferData(flavor));
                        break;
                    }
                }
                catch(UnsupportedFlavorException ex)
                {
                }
                catch(java.io.IOException ex)
                {
                }
            }

            return true;
        }
    }

    public GraphView(Coordinator coordinator, String title)
    {
        _coordinator = coordinator;
        setTitle(title);

        _preferences = Preferences.userNodeForPackage(getClass());

        //
        // Don't destroy JavaFX when the frame is disposed.
        //
        Platform.setImplicitExit(false);
        setIconImage(Utils.getIcon("/icons/16x16/metrics_graph.png").getImage());
        setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
        addWindowListener(new WindowAdapter()
        {
            @Override
            public void windowClosing(WindowEvent e) 
            {
                close();
            }
        });

        //
        // Graph preferences.
        //
        Action preferences = new AbstractAction("Preferences")
            {
                @Override
                public void actionPerformed(ActionEvent event) 
                {
                    //
                    // Set the title
                    //
                    JTextField title = new JTextField(getTitle());
                    JPanel titlePanel;
                    {
                        DefaultFormBuilder builder =
                                                new DefaultFormBuilder(new FormLayout("pref,2dlu,pref:grow", "pref"));
                        builder.append("Title:", title);
                        titlePanel = builder.getPanel();
                    }

                    //
                    // SpinnerNumberModel to set a refresh period.
                    //
                    SpinnerNumberModel refreshPeriod = new SpinnerNumberModel(getRefreshPeriod(), _minRefreshPeriod, 
                                                                              _maxRefreshPeriod, 1);

                    //
                    // SpinnerNumberModel to set the maximum number of samples to keep in X axis.
                    //
                    final SpinnerNumberModel samples = new SpinnerNumberModel(_samples, _minSamples, 
                                                                              _maxSamples, 1);

                    JPanel refreshPanel;
                    {
                        DefaultFormBuilder builder =
                                                new DefaultFormBuilder(new FormLayout("pref,2dlu,pref:grow", "pref"));
                        builder.rowGroupingEnabled(true);
                        final JSpinner spinner = new JSpinner(refreshPeriod);
                        builder.append("Sample interval:", spinner);
                        builder.append("", new JLabel("<html><p>Sample interval in seconds; must be between 1" +
                                                      "<br/>and 3600 seconds.</p></html>"));
                        refreshPanel = builder.getPanel();
                    }

                    //
                    // JComboBox to select time format used in X Axis
                    //
                    JComboBox<String> dateFormats = new JComboBox<String>(_dateFormats);
                    dateFormats.setSelectedItem(getDateFormat());
                    JPanel xAxisPanel;
                    {
                        DefaultFormBuilder builder = 
                                                new DefaultFormBuilder(new FormLayout("pref,2dlu,pref:grow", "pref"));
                        builder.append("Samples displayed:", new JSpinner(samples));
                        builder.append("", new JLabel("<html><p>The number of samples displayed on a graph;" +
                                                      "<br/>must be between 2 and 300." + "</p></html>"));
                        builder.append("Time format:", dateFormats);

                        xAxisPanel = builder.getPanel();
                    }

                    FormLayout layout = new FormLayout("fill:pref:grow", "pref");
                    final DefaultFormBuilder builder = new DefaultFormBuilder(layout);
                    builder.border(Borders.DIALOG);
                    builder.append(titlePanel);
                    builder.nextLine();
                    builder.append(refreshPanel);
                    builder.nextLine();
                    builder.append(xAxisPanel);

                    if(JOptionPane.showConfirmDialog(GraphView.this, builder.getPanel(), "Metrics Graph Preferences", 
                       JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE) != JOptionPane.OK_OPTION)
                    {
                        return;
                    }

                    setTitle(title.getText());
                    setRefreshPeriod(refreshPeriod.getNumber().intValue());
                    setMaximumSamples(samples.getNumber().intValue());
                    setDateFormat((String)dateFormats.getSelectedItem());
                }
            };

        _legendTable = new JTable(_legendModel)
            {
                //Implement table cell tool tips.
                @Override
                public String getToolTipText(java.awt.event.MouseEvent e)
                {
                    if(convertColumnIndexToModel(columnAtPoint(e.getPoint())) == 6)
                    {
                        return _legendModel.getRows(new int[]{rowAtPoint(e.getPoint())})[0].cell.getField().
                                                                                                    getColumnToolTip();
                    }
                    else
                    {
                        return null;
                    }
                }
            };
        
        //
        // Adjust row height for larger fonts
        //
        int fontSize = _legendTable.getFont().getSize();
        int minRowHeight = fontSize + fontSize / 3;
        if(_legendTable.getRowHeight() < minRowHeight)
        {
            _legendTable.setRowHeight(minRowHeight);
        }
        
        //
        // Graph preferences.
        //
        final Action delete = new AbstractAction("Delete")
            {
                @Override
                public void actionPerformed(ActionEvent event) 
                {
                    int[] selectedRows = _legendTable.getSelectedRows();
                    for(int i = 0; i < selectedRows.length; ++i)
                    {
                        selectedRows[i] = _legendTable.convertRowIndexToModel(selectedRows[i]);
                    }
                    //
                    // Remove selected rows from the legend model.
                    //
                    final MetricsRow[] rows = _legendModel.removeRows(selectedRows);

                    //
                    // Remove rows from series hash maps
                    //
                    for(MetricsRow row : rows)
                    {
                        Map<String, Map<String, Map<String, MetricsRow>>> j = _series.get(row.info);
                        Map<String, Map<String, MetricsRow>> k = j.get(row.cell.getField().getMetricsName());
                        Map<String, MetricsRow> l = k.get(row.cell.getId());
                        l.remove(row.cell.getField().getFieldName());
                        if(l.size() == 0)
                        {
                            k.remove(row.cell.getId());
                            if(k.size() == 0)
                            {
                                j.remove(row.cell.getField().getMetricsName());
                                if(j.size() == 0)
                                {
                                    _series.remove(row.info);
                                    if(_series.size() == 0)
                                    {
                                        stopRefresh();
                                    }
                                }
                            }
                        }
                    }

                    //
                    // Remove series from the chart, in JavaFx thread.
                    //
                    enqueueJFX(new Runnable()
                        {
                            @Override
                            public void run()
                            {
                                for(MetricsRow row : rows)
                                {
                                    for(int i = 0; i < row.series.size(); ++i)
                                    {
                                        XYChart.Series<Number, Number> series = row.series.get(i);
                                        String seriesClass = getSeriesClass(series);
                                        if(seriesClass != null)
                                        {
                                            _styles.remove(seriesClass);
                                        }
                                        //
                                        // Don't remove the XYChart.Series object here, to avoid the series
                                        // style classes to be reasign by JavaFX.
                                        //
                                        // _chart.getData().remove(row.series);
                                        try
                                        {
                                            series.getData().clear();
                                        }
                                        catch(NullPointerException ex)
                                        {
                                            // JavaFX bug
                                        }
                                    }
                                }
                            }
                        });
                }
            };
        delete.setEnabled(false);
        delete.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0));

        _legendTable.getSelectionModel().addListSelectionListener(new ListSelectionListener()
            {
                @Override
                public void valueChanged(ListSelectionEvent e)
                {
                    if(!e.getValueIsAdjusting())
                    {
                        delete.setEnabled(_legendTable.getSelectedRows().length > 0);
                    }
                }
            });

        //
        // Create the tool bar
        //
        class ToolBar extends JToolBar
        {
            public ToolBar()
            {
                putClientProperty(Options.HEADER_STYLE_KEY, HeaderStyle.BOTH);
                putClientProperty(PlasticLookAndFeel.BORDER_STYLE_KEY, BorderStyle.SEPARATOR);
                setFloatable(false);
                putClientProperty("JToolBar.isRollover", Boolean.TRUE);

                JButton button = new JButton(delete);
                button.setText(null);
                button.setIcon(Utils.getIcon("/icons/24x24/delete.png"));
                add(button);
            }
        }

        //
        // Create menus
        //
        JMenuBar menuBar = new JMenuBar();

        JMenu fileMenu = new JMenu("File");
        fileMenu.setMnemonic(java.awt.event.KeyEvent.VK_F);
        fileMenu.add(preferences);
        menuBar.add(fileMenu);

        JMenu editMenu = new JMenu("Edit");
        editMenu.setMnemonic(java.awt.event.KeyEvent.VK_E);
        editMenu.add(delete);
        menuBar.add(editMenu);

        setJMenuBar(menuBar);

        //
        // Set default renderer for numeric values.
        //
        _legendTable.setDefaultRenderer(Double.class, new FormatedNumberRenderer("#0.000"));

        //
        // Set a combobox to edit the scale factors.
        //
        JComboBox<Double> scales = new JComboBox<Double>(_scales);
        scales.setRenderer(new DecimalRenderer(scales.getRenderer()));
        _legendTable.getColumnModel().getColumn(7).setCellEditor(new DefaultCellEditor(scales));

        //
        //Set default renderer and editor for Color.class column.
        //
        _legendTable.setDefaultRenderer(Color.class, new ColorRenderer(true));
        _legendTable.setDefaultEditor(Color.class, new ColorEditor());
        
        _legendTable.setAutoCreateRowSorter(true);

        final JFXPanel fxPanel = new JFXPanel();
        fxPanel.setMinimumSize(new Dimension(0, 200));
        fxPanel.setPreferredSize(new Dimension(800, 400));
        //
        // Build the split pane, with the chart graph and the legend table.
        //
        _splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
        _splitPane.setTopComponent(fxPanel);
        JScrollPane scrollPane = new JScrollPane(_legendTable);
        scrollPane.setTransferHandler(new TransferHandler());
        scrollPane.setMinimumSize(new Dimension(0, 50));
        scrollPane.setPreferredSize(new Dimension(800, 200));
        _splitPane.setBottomComponent(scrollPane);
        
        DefaultFormBuilder builder = new DefaultFormBuilder(new FormLayout("fill:pref:grow", "fill:pref:grow, pref"));
        builder.append(_splitPane);
        builder.nextLine();

        JPanel panel = builder.getPanel();
        panel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
        getContentPane().add(new ToolBar(), BorderLayout.PAGE_START);
        getContentPane().add(panel, BorderLayout.CENTER);
        setPreferredSize(new Dimension(800, 600));

        //
        // initialize the scene in JavaFX thread.
        //
        enqueueJFX(new Runnable()
            {
                @Override
                public void run()
                {
                    _xAxis = new NumberAxis();
                    _yAxis = new NumberAxis();

                    _chart = new LineChart<Number, Number>(_xAxis, _yAxis);
                    _chart.setCreateSymbols(false);
                    _xAxis.setLabel("Time (" + getDateFormat() + ")");
                    _xAxis.setTickLabelFormatter(_timeFormater);
                    _xAxis.setForceZeroInRange(false);
                    _chart.setAnimated(true);
                    _chart.setLegendVisible(false);

                    final Scene scene = new Scene(_chart);
                    scene.setOnDragOver(
                        new EventHandler<DragEvent>()
                            {
                                @Override
                                public void handle(DragEvent event)
                                {
                                    Dragboard db = event.getDragboard();
                                    if(event.getGestureSource() != scene && db.hasContent(LocalObjectMimeType))
                                    {
                                        Object object = db.getContent(LocalObjectMimeType);
                                        if(object instanceof MetricsViewTransferableData)
                                        {
                                            event.acceptTransferModes(TransferMode.COPY);
                                        }
                                    }
                                    event.consume();
                                }
                            });

                    scene.setOnDragDropped(
                        new EventHandler<DragEvent>()
                            {
                                @Override
                                public void handle(DragEvent event)
                                {
                                    boolean success = false;
                                    Dragboard db = event.getDragboard();
                                    if(event.getGestureSource() != scene && db.hasContent(LocalObjectMimeType))
                                    {
                                        Object object = db.getContent(LocalObjectMimeType);
                                        if(object instanceof MetricsViewTransferableData)
                                        {
                                            addSeries((MetricsViewTransferableData)object);
                                            success = true;                                            
                                        }
                                    }
                                    event.setDropCompleted(success);
                                    event.consume();
                                }
                            });
                    fxPanel.setScene(scene);
                }
            });

        pack();
        if(!loadPreferences())
        {
            Rectangle otherRect = _coordinator.getMainFrame().getBounds();
            Rectangle thisRect = getBounds();
            if(otherRect.width < thisRect.width || otherRect.height < thisRect.height)
            {
                setLocationRelativeTo(null);
            }
            else
            {
                setLocationRelativeTo(_coordinator.getMainFrame());
            }
            _splitPane.setDividerLocation(400);
        }
        setVisible(true);

        //
        // Show info dialog if required.
        //
        if(showInfo())
        {
            JCheckBox checkbox = new JCheckBox("Do not show this message again.");  
            String message = "Drop metrics cells to add them to the graph.";  

            JOptionPane.showConfirmDialog(this, new Object[]{message, checkbox}, "Information", 
                                          JOptionPane.DEFAULT_OPTION, JOptionPane.INFORMATION_MESSAGE);  
            if(checkbox.isSelected())
            {
                _preferences.node("GraphView").putBoolean("showInfo", false);
            }
        }
    }

    private boolean showInfo()
    {
        try
        {
            if(!_preferences.nodeExists("GraphView"))
            {
                return true;
            }
        }
        catch(BackingStoreException ex)
        {
            return true;
        }
        return _preferences.node("GraphView").getBoolean("showInfo", true);
    }

    public void storePreferences()
    {
        Preferences preferences = _preferences.node("GraphView");
        Rectangle rect = getBounds();

        for(int i = _columnNames.length -1; i >= 0; --i)
        {
            preferences.putInt("colPos" + Integer.toString(i), _legendTable.convertColumnIndexToModel(i));
            preferences.putInt("colWidth" + Integer.toString(i), _legendTable.getColumnModel().getColumn(i).getWidth());
        }

        preferences.putInt("x", rect.x);
        preferences.putInt("y", rect.y);
        preferences.putInt("width", rect.width);
        preferences.putInt("height", rect.height);
        preferences.putBoolean("maximized", getExtendedState() == Frame.MAXIMIZED_BOTH);
        preferences.putInt("splitLocation", _splitPane.getDividerLocation());

        preferences.putInt("sampleInterval", getRefreshPeriod());
        preferences.putInt("maximumSamples", getMaximumSamples());
        preferences.put("dateFormat", getDateFormat());
    }

    public boolean loadPreferences()
    {
        try
        {
            if(!_preferences.nodeExists("GraphView"))
            {
                return false;
            }
        }
        catch(BackingStoreException ex)
        {
            return false;
        }

        Preferences preferences = _preferences.node("GraphView");

        int x = preferences.getInt("x", 0);
        int y = preferences.getInt("y", 0);
        int width = preferences.getInt("width", 0);
        int height = preferences.getInt("height", 0);
        setBounds(new Rectangle(x, y, width, height));
        if(preferences.getBoolean("maximized", false))
        {
            setExtendedState(Frame.MAXIMIZED_BOTH);
        }
        _splitPane.setDividerLocation(_preferences.node("GraphView").getInt("splitLocation", 600));
        for(int i = _columnNames.length -1; i >= 0; --i)
        {
            int pos = _legendTable.convertColumnIndexToView(preferences.getInt("columnPos" + Integer.toString(i), i));
            if(i != pos)
            {
                _legendTable.getColumnModel().moveColumn(pos, i);
            }            
            int columnWidth = preferences.getInt("colWidth" + Integer.toString(i), -1);
            if(columnWidth != -1)
            {
                _legendTable.getColumnModel().getColumn(i).setPreferredWidth(columnWidth);
            }
        }
        
        int refreshPeriod = preferences.getInt("refreshPeriod", _defaultRefreshPeriod);
        if(refreshPeriod < _minRefreshPeriod)
        {
            refreshPeriod = _minRefreshPeriod;
        }
        else if(refreshPeriod > _maxRefreshPeriod)
        {
            refreshPeriod = _maxRefreshPeriod;
        }
        setRefreshPeriod(refreshPeriod);

        int samples = preferences.getInt("maximumSamples", _defaultSamples);
        if(samples < _minSamples)
        {
            samples = _minSamples;
        }
        else if(samples > _maxSamples)
        {
            samples = _maxSamples;
        }
        setMaximumSamples(samples);

        setDateFormat(preferences.get("dateFormat", getDateFormat()));
        return true;
    }

    @Override
    public void close()
    {
        storePreferences();
        stopRefresh();
        setVisible(false);
        _coordinator.removeGraphView(GraphView.this);
        dispose();
    }

    @Override
    public void addSeries(final MetricsViewTransferableData data)
    {
        //
        // Must run in JavaFX thread.
        //
        enqueueJFX(new Runnable()
            {
                @Override
                public void run()
                {
                    Map<String, Map<String, Map<String, MetricsRow>>> metrics = _series.get(data.info);
                    if(metrics == null)
                    {
                        metrics = new HashMap<String, Map<String, Map<String, MetricsRow>>>();
                        _series.put(data.info, metrics);
                    }

                    Map<String, Map<String, MetricsRow>> rows = metrics.get(data.name);
                    if(rows == null)
                    {
                        rows = new HashMap<String, Map<String, MetricsRow>>();
                        metrics.put(data.name, rows);
                    }

                    for(Map.Entry<String, List<MetricsCell>> i : data.rows.entrySet())
                    {
                        final String rowId = i.getKey();
                        Map<String, MetricsRow> columns = rows.get(rowId);
                        if(columns == null)
                        {
                            columns = new HashMap<String, MetricsRow>();
                            rows.put(rowId, columns);
                        }

                        for(MetricsCell j : i.getValue())
                        {
                            if(columns.get(j.getField().getFieldName()) == null)
                            {
                                String color = DefaultColors[_chart.getData().size() % DefaultColors.length];
                                final MetricsRow row = new MetricsRow(data.info, j, color, 
                                                                      new XYChart.Series<Number, Number>());

                                XYChart.Series<Number, Number> series = row.series.peek();
                                _chart.getData().add(series);

                                String styleClass = getSeriesClass(series);
                                addStyle(series, styleClass, color);
                                setNodesStyle(styleClass);

                                columns.put(j.getField().getFieldName(), row);
                                j.getField().setContext(GraphView.this);
                                //
                                // When a line is clicked we select the correspoding row in the legend table.
                                //
                                javafx.scene.Node n = _chart.lookup(".chart-series-line." + styleClass);
                                if(n != null)
                                {
                                    n.setOnMousePressed(new EventHandler<MouseEvent>()
                                    {
                                        @Override public void 
                                        handle(MouseEvent e)
                                        {
                                            if(e.getEventType() == MouseEvent.MOUSE_PRESSED &&
                                               e.getButton() == MouseButton.PRIMARY)
                                            {
                                                //
                                                // Must run in Swing thread.
                                                //
                                                enqueueSwing(new Runnable()
                                                    {
                                                        @Override
                                                        public void run()
                                                        {
                                                            int i = _legendModel.getRowIndex(row);
                                                            if(i != -1)
                                                            {
                                                                i = _legendTable.convertRowIndexToView(i);
                                                                _legendTable.setRowSelectionInterval(i, i);
                                                            }
                                                        }
                                                    });
                                            }
                                        }
                                    });
                                }
                                //
                                // Add the serie to the legend, must run in Swing thread.
                                //
                                enqueueSwing(new Runnable()
                                    {
                                        @Override
                                        public void run()
                                        {
                                            _legendModel.addRow(row);
                                        }
                                    });
                            } 
                        }
                    }
                    if(_chart.getData().size() > 0)
                    {
                        startRefresh();
                    }
                }
            });
    }

    //
    // Added a new chart series to an existing row, the graph series will use the
    // same configuration, the row cell field must be reset so calculations doesn't 
    // take into account previous data. If we don't reset fields here caculations
    // can be bogus in case the view was disabled and the data in the view was reset.
    //
    void addSeries(final MetricsRow row)
    {
        XYChart.Series<Number, Number> series = new XYChart.Series<Number, Number>();
        row.series.push(series);
        _chart.getData().add(series);

        //
        // Retrieve the style class asigned by JavaFX to this series, and set the
        // same style
        //
        String styleClass = getSeriesClass(series);
        addStyle(series, styleClass, row.color);
        setNodesStyle(styleClass);

        //
        // Reset the cell field necessary to avoid bogus calculations.
        //
        row.cell.resetField();

        //
        // We need also a new click handler so click works in all segments 
        // of the line.
        // 
        // When a line is clicked we select the correspoding row in the legend table.
        //
        javafx.scene.Node n = _chart.lookup(".chart-series-line." + styleClass);
        if(n != null)
        {
            n.setOnMousePressed(new EventHandler<MouseEvent>()
            {
                @Override public void 
                handle(MouseEvent e)
                {
                    if(e.getEventType() == MouseEvent.MOUSE_PRESSED &&
                       e.getButton() == MouseButton.PRIMARY)
                    {
                        //
                        // Must run in Swing thread.
                        //
                        enqueueSwing(new Runnable()
                            {
                                @Override
                                public void run()
                                {
                                    int i = _legendModel.getRowIndex(row);
                                    if(i != -1)
                                    {
                                        i = _legendTable.convertRowIndexToView(i);
                                        _legendTable.setRowSelectionInterval(i, i);
                                    }
                                }
                            });
                    }
                }
            });
        }
    }

    private void addData(final MetricsViewInfo info, final Map<String, IceMX.Metrics[]> data, 
                         final long timestamp)
    {
        //
        // Update the graph series in JavaFX thread.
        //
        enqueueJFX(new Runnable()
            {
                @Override
                public void run()
                {
                    Map<String, Map<String, Map<String, MetricsRow>>> series = _series.get(info);
                    if(series == null)
                    {
                        return;
                    }

                    for(Map.Entry<String, Map<String, Map<String, MetricsRow>>> i : series.entrySet())
                    {
                        IceMX.Metrics[] metricsSeq = null;
                        if(data != null)
                        {
                            metricsSeq = data.get(i.getKey());
                        }

                        //
                        // Iterate over all configured values, if there isn't data for one configured
                        // field we need to add a gap.
                        //

                        for(Map.Entry<String, Map<String, MetricsRow>> j : i.getValue().entrySet())
                        {
                            IceMX.Metrics metrics = null;

                            if(metricsSeq != null)
                            {
                                for(IceMX.Metrics m : metricsSeq)
                                {
                                    if(m.id.equals(j.getKey()))
                                    {
                                        metrics = m;
                                        break;
                                    }
                                }
                            }
                            for(Map.Entry<String, MetricsRow> k : j.getValue().entrySet())
                            {
                                MetricsRow row = k.getValue();
                                //
                                // If there isn't a metrics object we disable the row and add a dummy value.
                                //
                                if(metrics == null)
                                {
                                    //
                                    // If the row isn't disabled we add a new serie to represent the gap
                                    // and mark the row as disabled.
                                    //
                                    if(!row.disabled)
                                    {
                                        row.series.push(new XYChart.Series<Number, Number>());
                                        row.disabled = true;
                                    }
                                    //
                                    // This dummy value is added to represent gap sizes, but isn't displayed
                                    // as the series isn't added to the graph.
                                    //
                                    row.series.peek().getData().add(new XYChart.Data<Number, Number>(0, 0));
                                }
                                else
                                {
                                    try
                                    {
                                        if(row.disabled)
                                        {
                                            addSeries(row);
                                            row.disabled = false;
                                        }

                                        Number value = row.cell.getValue(metrics, timestamp);
                                        //
                                        // The cell returns null to indicate the value must be skipped,
                                        // this is usually because it needs two values to calculate
                                        // the average.
                                        //
                                        if(value == null)
                                        {
                                            continue;
                                        }

                                        row.series.peek().getData().add(
                                            new XYChart.Data<Number, Number>(timestamp, value));
                                    }
                                    catch(java.lang.RuntimeException ex)
                                    {
                                        ex.printStackTrace();
                                    }
                                }

                                //
                                // Remove the vertices from the beginning of the series that exceeded 
                                // the maximum number of samples.
                                //
                                adjustSize(row);
                            }
                        }
                    }
                    //
                    // Fire an event on the legend model to update all cells.
                    //
                    enqueueSwing(new Runnable()
                        {
                            @Override
                            public void run()
                            {
                                _legendModel.fireTableChanged(
                                            new TableModelEvent(_legendModel, 0, _legendModel.getRowCount() - 1, 
                                                                TableModelEvent.ALL_COLUMNS, 
                                                                TableModelEvent.UPDATE));
                            }
                        });
                }
            });
    }

    int seriesSize(MetricsRow row)
    {
        int size = 0;
        for(int i = 0; i < row.series.size(); ++i)
        {
            size += row.series.get(i).getData().size();
        }
        return size;
    }

    void adjustSize(MetricsRow row)
    {
        int samples = seriesSize(row);
        final int n = getMaximumSamples();
        while(samples > n)
        {
            for(int i = 0; i < row.series.size(); ++i)
            {
                XYChart.Series<Number, Number> series = row.series.get(i);
                while(series.getData().size() > 0 && samples > n)
                {
                    try
                    {
                        series.getData().remove(0);
                    }
                    catch(java.lang.NullPointerException ex)
                    {
                        // JavaFX bug
                    }
                    samples--;

                    //
                    // Remove empty series not longer in use, if there is only one
                    // series that is keep to add new values.
                    //
                    if(series.getData().size() == 0 && row.series.size() > 1)
                    {
                        row.series.remove(series);
                        i--;
                    }
                }
            }
        }
    }

    synchronized private void startRefresh()
    {
        if(_refreshFuture == null)
        {
            _refreshFuture = _coordinator.getExecutor().scheduleAtFixedRate(new Runnable()
            {
                @Override
                public void run()
                {
                    java.util.Set<MetricsViewInfo> metrics = null;
                    synchronized(GraphView.this)
                    {
                        metrics = new  java.util.HashSet<MetricsViewInfo>(_series.keySet());
                    }

                    for(final MetricsViewInfo m : metrics)
                    {
                        IceMX.Callback_MetricsAdmin_getMetricsView cb = new IceMX.Callback_MetricsAdmin_getMetricsView()
                            {
                                @Override
                                public void response(final java.util.Map<java.lang.String, IceMX.Metrics[]> data,
                                                     long timestamp)
                                {
                                    addData(m, data, timestamp);
                                }

                                @Override
                                public void exception(final Ice.LocalException e)
                                {
                                    addData(m, null, 0);
                                }

                                @Override
                                public void exception(final Ice.UserException e)
                                {
                                    addData(m, null, 0);
                                }
                            };
                        try
                        {
                            m.admin.begin_getMetricsView(m.view, cb);
                        }
                        catch(Ice.LocalException e)
                        {
                            addData(m, null, 0);
                        }
                    }
                }
            }, 0, _refreshPeriod, java.util.concurrent.TimeUnit.SECONDS);
        }
    }

    synchronized private void stopRefresh()
    {
        if(_refreshFuture != null)
        {
            _refreshFuture.cancel(false);
            _refreshFuture = null;
        }
    }

    @Override
    public synchronized int getRefreshPeriod()
    {
        return _refreshPeriod;
    }

    synchronized void setRefreshPeriod(int refreshPeriod)
    {
        if(refreshPeriod == _refreshPeriod)
        {
            return;
        }

        _refreshPeriod = refreshPeriod;
        if(_refreshFuture != null)
        {
           _refreshFuture.cancel(false);
            _refreshFuture = null;

            startRefresh();
        }

    }

    synchronized String getDateFormat()
    {
        return _dateFormat;
    }

    synchronized void setDateFormat(String dateFormat)
    {
        _dateFormat = dateFormat;
        _timeFormater.setDateFormat(dateFormat);
        //
        // Update the horizontal axis label, in JavaFx thread.
        //
        enqueueJFX(new Runnable()
            {
                @Override
                public void run()
                {
                    _xAxis.setLabel("Time (" + getDateFormat() + ")");
                }
            });
    }

    synchronized private void setMaximumSamples(final int samples)
    {
        if(samples == _samples)
        {
            return;
        }

        if(samples < _samples)
        {
            _samples = samples;
            //
            // If maximum samples change, we remove older samples.
            //
            enqueueJFX(new Runnable()
                {
                    @Override
                    public void run()
                    {
                        MetricsRow[] rows = _legendModel.getRows();
                        for(MetricsRow row : rows)
                        {
                            adjustSize(row);
                        }
                    }
                });
        }
        else
        {
            _samples = samples;
        }
    }

    synchronized private int getMaximumSamples()
    {
        return _samples;
    }

    class MetricsRow
    {
        public MetricsRow(MetricsViewInfo info, MetricsCell cell, String color, XYChart.Series<Number, Number> series)
        {
            this.visible = true;
            this.disabled = false;
            this.info = info;
            this.cell = cell;
            this.color = color;
            this.series.push(series);
        }

        boolean visible;
        boolean disabled;
        MetricsViewInfo info;
        MetricsCell cell;
        String color;
        //
        // Stack of all the chart series used to represent this metrics object
        // new values are added to the chart series at the top.
        //
        Stack<XYChart.Series<Number, Number>> series = new Stack<XYChart.Series<Number, Number>>();
    }

    class LegendTableModel extends javax.swing.table.AbstractTableModel
    {
        @Override
        public String getColumnName(int col)
        {
            return _columnNames[col];
        }

        @Override
        public int getRowCount()
        {
            return _rows.size();
        }

        @Override
        public int getColumnCount()
        {
            return _columnNames.length;
        }

        @Override
        public Class<?> getColumnClass(int columnIndex)
            {
            switch(columnIndex)
            {
                case 0: // Node Name
                {
                    return Boolean.class;
                }
                case 1: // Node Name
                {
                    return String.class;
                }
                case 2: // Server Name
                {
                    return String.class;
                }
                case 3: // View Name
                {
                    return String.class;
                }
                case 4: // Metrics Name
                {
                    return String.class;
                }
                case 5: // Metrics Id
                {
                    return String.class;
                }
                case 6: // Column Name
                {
                    return String.class;
                }
                case 7: // Scale factor
                {
                    return Double.class;
                }
                case 8: // Last value
                {
                    return Double.class;
                }
                case 9: // Average value
                {
                    return Double.class;
                }
                case 10: // Min value
                {
                    return Double.class;
                }
                case 11: // Max value
                {
                    return Double.class;
                }
                case 12: // Color
                {
                    return Color.class;
                }
                default:
                {
                    return null;
                }
            }
            }

        @Override
        public Object getValueAt(int rowIndex, int columnIndex)
        {
            if(rowIndex > _rows.size() || columnIndex > _columnNames.length)
            {
                return null;
            }
            MetricsRow row = _rows.get(rowIndex);
            switch(columnIndex)
            {
                case 0:
                {
                    return row.visible;
                }
                case 1:
                {
                    return row.info.node;
                }
                case 2:
                {
                    return row.info.server;
                }
                case 3:
                {
                    return row.info.view;
                }
                case 4:
                {
                    return row.cell.getField().getMetricsName();
                }
                case 5:
                {
                    return row.cell.getId();
                }
                case 6:
                {
                    return row.cell.getField().getColumnName();
                }
                case 7:
                {
                    return row.cell.getScaleFactor();
                }
                case 8:
                {
                    return row.cell.getLast();
                }
                case 9:
                {
                    return row.cell.getAverage();
                }
                case 10:
                {
                    return row.cell.getMin();
                }
                case 11:
                {
                    return row.cell.getMax();
                }
                case 12:
                {
                    return new Color(Integer.parseInt(row.color.substring(1), 16));
                }
                default:
                {
                    return null;
                }
            }
        }

        @Override
        public boolean isCellEditable(int row, int col)
        {
            if(col < _columnNames.length && (_columnNames[col].equals("Show") || 
                                             _columnNames[col].equals("Scale") ||
                                             _columnNames[col].equals("Color")))
            {
                return true;
            }
            else
            {
                return false;
            }
        }

        @Override
        public void setValueAt(final Object value, int rowIndex, int columnIndex)
        {
            if(isCellEditable(rowIndex, columnIndex))
            {
                final MetricsRow row = _rows.get(rowIndex);
                if(_columnNames[columnIndex].equals("Show"))
                {
                    row.visible = ((Boolean)value).booleanValue();
                    enqueueJFX(new Runnable()
                        {
                            @Override
                            public void run()
                            {
                                for(int i = 0; i < row.series.size(); ++i)
                                {
                                    setNodesVisible(getSeriesClass(row.series.get(i)), row.visible);
                                }
                            }
                        });
                }
                else if(_columnNames[columnIndex].equals("Scale"))
                {
                    double s1 = ((Double)getValueAt(rowIndex, columnIndex)).doubleValue();
                    double s2 = ((Double)value).doubleValue();
                    for(int i = 0; i < row.series.size(); ++i)
                    {
                        updateScaleFactor(row.series.get(i), s1, s2);
                    }
                    row.cell.setScaleFactor(((Double)value).doubleValue());
                }
                else if(_columnNames[columnIndex].equals("Color"))
                {
                    Color color = (Color)value;
                    //
                    // Convert color to the CSS representation used by JavaFX style.
                    // example: #ff00aa
                    //
                    row.color = "#" + String.format("%02X", color.getRed()) + 
                                      String.format("%02X", color.getGreen()) +
                                      String.format("%02X", color.getBlue());
                    for(int i = 0; i < row.series.size(); ++i)
                    {
                        updateSeriesColor(row.series.get(i), row.color);
                    }
                }
                fireTableCellUpdated(rowIndex, columnIndex);
            }
        }

        public void addRow(MetricsRow row)
        {
            int i = _rows.size();
            _rows.put(i, row);
            fireTableRowsInserted(i, i);
        }

        public MetricsRow[] removeRows(int[] rowIndexes)
        {
            MetricsRow[] deletedRows = new MetricsRow[rowIndexes.length];
            for(int i = 0; i < rowIndexes.length; i++)
            {
                deletedRows[i] = _rows.remove(rowIndexes[i]);
            }
            Map<Integer, MetricsRow> rows = new HashMap<Integer, MetricsRow>();
            for(Map.Entry<Integer, MetricsRow> e : _rows.entrySet())
            {
                rows.put(rows.size(), e.getValue());
            }
            _rows = rows;
            fireTableDataChanged();
            return deletedRows;
        }

        public MetricsRow[] getRows(int[] rowIndexes)
        {
            MetricsRow[] rows = new MetricsRow[rowIndexes.length];
            for(int i = 0; i < rowIndexes.length; i++)
            {
                rows[i] = _rows.get(rowIndexes[i]);
            }
            return rows;
        }

        public MetricsRow[] getRows()
        {
            return _rows.values().toArray(new MetricsRow[_rows.size()]);
        }

        public int getRowIndex(MetricsRow row)
        {
            int index = -1;
            for(Map.Entry<Integer, MetricsRow> entry : _rows.entrySet())
            {
                if(row == entry.getValue())
                {
                    index = entry.getKey();
                    break;
                }
            }
            return index;
        }

        private Map<Integer, MetricsRow> _rows = new HashMap<Integer, MetricsRow>();
    }

    void
    updateScaleFactor(final XYChart.Series<Number, Number> series, final double s1, final double s2)
    {
        //
        // Must run in JavaFX thread.
        //
        enqueueJFX(new Runnable()
            {
                @Override
                public void run()
                {
                    for(XYChart.Data<Number, Number> i : series.getData())
                    {
                        i.setYValue(i.getYValue().doubleValue() * s2 / s1);
                    }
                }
            });
    }

    void
    updateSeriesColor(final XYChart.Series<Number, Number> series, final String color)
    {
        //
        // Must run in JavaFX thread.
        //
        enqueueJFX(new Runnable()
            {
                @Override
                public void run()
                {
                    String styleClass = getSeriesClass(series);
                    if(styleClass != null)
                    {
                        addStyle(series, styleClass, color);
                        setNodesStyle(styleClass);
                    }
                }
            });
    }

    //
    // Must be called in JavaFX thread.
    //
    // Return the class used to style a serie.
    //
    public String getSeriesClass(XYChart.Series<Number, Number> series)
    {
        if(series == null || series.getNode() == null || series.getNode().getStyleClass() == null)
        {
            return null;
        }
        String value = null;
        for(String styleClass : series.getNode().getStyleClass())
        {
            if(styleClass.startsWith("series"))
            {
                value = styleClass;
                break;
            }
        }
        return value;
    }

    //
    // Must be called on JavaFX thread
    //
    private void setNodesStyle(String seriesClass)
    {
        String style = _styles.get(seriesClass);
        for(javafx.scene.Node n : _chart.lookupAll("." + seriesClass))
        {
            n.setStyle(style);
            //
            // Disable effects like shadows. This effect doesn't look good in some
            // Windows versions.
            //
            n.setEffect(null);
        }
    }

    //
    // Must be called on JavaFX thread
    //
    private void setNodesVisible(String seriesClass, boolean visible)
    {
        for(javafx.scene.Node n : _chart.lookupAll("." + seriesClass))
        {
            n.setVisible(visible);
        }
    }

    //
    // Must be called on JavaFX thread
    //
    private void addStyle(XYChart.Series<Number, Number> series, String seriesClass, String color)
    {
        //
        // Customize the style.
        //
        StringBuilder sb = new StringBuilder();
        sb.append("-fx-stroke: ");
        sb.append(color);
        sb.append("; ");
        sb.append("-fx-background-color: ");
        sb.append(color);
        sb.append(", white;");
        sb.append("-fx-stroke-width: 3;");
        _styles.put(seriesClass, sb.toString()); 
    }

    private void enqueueJFX(final Runnable runnable) {
        _queue.submit(new Runnable()
        {
            @Override
            public void run()
            {
                Platform.runLater(new Runnable()
                    {
                        @Override
                        public void run()
                        {
                            try
                            {
                                runnable.run();
                            }
                            finally
                            {
                                _sem.release();
                            }
                        }
                    });
                _sem.acquireUninterruptibly();
            }
        });
    }

    private void enqueueSwing(final Runnable runnable) {
        _queue.submit(new Runnable()
        {
            @Override
            public void run()
            {
                SwingUtilities.invokeLater(new Runnable()
                    {
                        @Override
                        public void run()
                        {
                            try
                            {
                                runnable.run();
                            }
                            finally
                            {
                                _sem.release();
                            }
                        }
                    });
                _sem.acquireUninterruptibly();
            }
        });
    }

    @SuppressWarnings("rawtypes") 
    static class DecimalRenderer extends DefaultListCellRenderer
    { 
        public DecimalRenderer(ListCellRenderer renderer)
        {
            this._renderer = renderer;
        }
 
        @Override
        public Component
        getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus)
        {
            @SuppressWarnings("unchecked")
            Component c = _renderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
            if(c instanceof JLabel)
            {
                ((JLabel) c).setText(_format.format(value));
            }
            else
            {
                c = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
                setText(_format.format(value));
            }
            return c;
        }

        private ListCellRenderer _renderer;
        private static final DecimalFormat _format = new DecimalFormat("#,###,###,##0.0########");
    }

    public class ColorEditor extends AbstractCellEditor implements TableCellEditor, ActionListener
    {
        public ColorEditor()
        {
            _button = new JButton();
            _button.setActionCommand(EDIT);
            _button.addActionListener(this);
            _button.setBorderPainted(false);

            _colorChooser = new JColorChooser();

            AbstractColorChooserPanel[] panels = _colorChooser.getChooserPanels();
            for(AbstractColorChooserPanel panel : panels)
            {
                if(!panel.getClass().getName().equals("javax.swing.colorchooser.DefaultSwatchChooserPanel"))
                {
                    _colorChooser.removeChooserPanel(panel);
                }
            }

            _dialog = JColorChooser.createDialog(_button, "Select the metrics color", true, _colorChooser, this, null);
            
        }

        @Override
        public void actionPerformed(ActionEvent e)
        {
            if(EDIT.equals(e.getActionCommand()))
            {
                _button.setBackground(_currentColor);
                _colorChooser.setColor(_currentColor);
                _dialog.setVisible(true);
                //Make the renderer reappear.
                fireEditingStopped();
            }
            else
            {
                _currentColor = _colorChooser.getColor();
            }
        }

        @Override
        public Object getCellEditorValue()
        {
            return _currentColor;
        }

        @Override
        public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected,
                                                     int row, int column)
        {
            _currentColor = (Color)value;
            return _button;
        }

        Color _currentColor;
        JButton _button;
        JColorChooser _colorChooser;
        JDialog _dialog;
        protected static final String EDIT = "edit";
    }

    static class ColorRenderer extends JLabel implements TableCellRenderer
    {
        Border unselectedBorder = null;
        Border selectedBorder = null;
        boolean isBordered = true;
 
        public ColorRenderer(boolean isBordered)
        {
            this.isBordered = isBordered;
            setOpaque(true); //MUST do this for background to show up.
        }
 
        @Override
        public Component getTableCellRendererComponent(JTable table, Object color, boolean isSelected, boolean hasFocus,
                                                       int row, int column)
        {
            Color newColor = (Color)color;
            setBackground(newColor);
            if(isBordered)
            {
                if(isSelected)
                {
                    if(selectedBorder == null)
                    {
                        selectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5, table.getSelectionBackground());
                    }
                    setBorder(selectedBorder);
                }
                else
                {
                    if(unselectedBorder == null)
                    {
                        unselectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5, table.getBackground());
                    }
                    setBorder(unselectedBorder);
                }
            }
            return this;
        }
    }

    private final Coordinator _coordinator;
    private java.util.concurrent.Future<?> _refreshFuture;

    private final static int _minRefreshPeriod = 1;         //     1 seconds
    private final static int _maxRefreshPeriod = 60 * 60;   //  3600 seconds = 1 hour.
    private final static int _defaultRefreshPeriod = 5;     //     5 seconds
    private int _refreshPeriod = _defaultRefreshPeriod;

    private int _minSamples = 2;   // We need at least to points to draw a line.
    private int _maxSamples = 300; // More that 300 points in a line doesn't work well with JavaFX charts.
    private int _defaultSamples = 120;
    private int _samples = _defaultSamples;

    private String[] _dateFormats = new String[]{"HH:mm:ss", "mm:ss"};
    private String _dateFormat = _dateFormats[0];
    private final TimeFormatter _timeFormater = new TimeFormatter(_dateFormat);

    private LineChart<Number, Number> _chart;
    private NumberAxis _xAxis;
    private NumberAxis _yAxis;

    private final static String[] _columnNames = new String[]{"Show", "Node", "Server", "Metrics View Name", 
                                                              "Metrics Name", "Metrics Id", "Metrics Field", "Scale", 
                                                              "Last", "Average", "Minimum", "Maximum", "Color"};


    //
    // The metrics view being graph
    //

    //
    // MetricsViewInfo (Metrics view of a server)
    // Metrics Name
    // Row identity
    // Field name
    //
    private final Map<MetricsViewInfo,
        Map<String, 
            Map<String, 
                Map<String, MetricsRow>>>> _series =
                                    new HashMap<MetricsViewInfo, Map<String, Map<String, Map<String, MetricsRow>>>>();

    private final static String[] DefaultColors = new String[]
                                                    {
                                                        "#FF0000", // Red
                                                        "#00FF00", // Lime
                                                        "#00FFFF", // Aqua
                                                        "#FFA07A", // LightSalmon
                                                        "#FFC0CB", // Pink
                                                        "#FFD700", // Gold
                                                        "#CD5C5C", // Indian red
                                                        "#32CD32", // LimeGreen
                                                        "#AFEEEE", // PaleTurquoise
                                                        "#FF4500", // OrangeRed
                                                        "#FF69B4", // HotPink
                                                        "#BDB76B", // DarkKhaki
                                                        "#8B0000", // DarkRed
                                                        "#9ACD32", // YellowGreen
                                                        "#00BFFF", // DeepSkyBlue
                                                        "#4B0082", // Indigo
                                                    };

    private final JTable _legendTable;
    private final LegendTableModel _legendModel = new LegendTableModel();
    private JSplitPane _splitPane;

    private final Map<String, String> _styles = new HashMap<String, String>();

    private final Double[] _scales = new Double[]{0.000000001d, 
                                                  0.00000001d,
                                                  0.0000001d, 
                                                  0.000001d, 
                                                  0.00001d,
                                                  0.0001d, 
                                                  0.001d, 
                                                  0.01d, 
                                                  0.1d, 
                                                  1.0d, 
                                                  10.0d, 
                                                  100.0d,
                                                  1000.0d,
                                                  10000.0d, 
                                                  100000.0d, 
                                                  1000000.0d, 
                                                  10000000.0d,
                                                  100000000.0d,
                                                  1000000000.0d};
    
    private final java.util.concurrent.Semaphore _sem = new java.util.concurrent.Semaphore(0);                
    private final java.util.concurrent.ExecutorService _queue = java.util.concurrent.Executors.newSingleThreadExecutor(
        new java.util.concurrent.ThreadFactory()
        {
            @Override
            public Thread newThread(Runnable r)
            {
                Thread t = new Thread(r);
                t.setDaemon(true);
                t.setName("GraphView-Thread");
                return t;
            }
        });
    private final Preferences _preferences;

    private final static DataFormat LocalObjectMimeType = new DataFormat("application/x-java-jvm-local-objectref");
}

