/* Copyright (c) 2005 Stanford University and Christopher Bruns
 * 
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including 
 * without limitation the rights to use, copy, modify, merge, publish, 
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject
 * to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included 
 * in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

/*
 * Created on May 24, 2006
 * Original author: Christopher Bruns
 */
package org.simtk.isimsu;

import java.awt.*;
import java.awt.event.*;
import java.text.DecimalFormat;
import java.util.*;
import java.util.regex.*;
import javax.swing.*;
import javax.swing.border.BevelBorder;
import javax.swing.event.*;
import java.math.*;

class IonSelectionPanel 
extends JPanel
implements ActionListener
{
    protected IonSelectionDialog ionSelectionDialog = null;
    protected SaltRangeSet ionConditions;
    protected JButton deleteButton = new JButton("Delete Ion");
    protected IonTypePanel ionTypePanel = null;
    protected IonConcentrationPanel ionConcentrationPanel;
    protected IonSpecies cachedIonSpecies = null;
    
    IonSelectionPanel(IonSelectionDialog ionSelectionDialog,
            IonSpecies initialIon,
            SaltRangeSet ionConditions) {

        this.ionSelectionDialog = ionSelectionDialog;
        this.ionConditions = ionConditions;

        // define ionConcentrationPanel after ionSelectionDialog
        ionConcentrationPanel = new IonConcentrationPanel();
        
        setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
        setBorder(BorderFactory.createBevelBorder(BevelBorder.LOWERED));
        
        cachedIonSpecies = initialIon;
        
        ionTypePanel = new IonTypePanel(initialIon);
        add(ionTypePanel);

        // Concentration panel
        add(ionConcentrationPanel);
        
        deleteButton.addActionListener(this);
        deleteButton.setToolTipText("Remove this ion");
    }

    public double minConcentration() {return ionConcentrationPanel.minConcentration();}
    public double maxConcentration() {return ionConcentrationPanel.maxConcentration();}
    public double concentration() {return ionConcentrationPanel.concentration();}

    public void setConcentrationRange(double min, double max) {
        ionConcentrationPanel.setConcentrationRange(min, max);
    }
    public void setConcentration(double concentration) {
        ionConcentrationPanel.setConcentration(concentration);
    }

    /**
     * Establish the set of other ions that can be selected here
     * @param ions
     */
    public void setUnselectedIons(Set<IonSpecies> ions) {
        JComboBox box = ionTypePanel.ionBox;
        IonSpecies selectedItem = (IonSpecies) box.getSelectedItem();
        box.setEnabled(false);

        // TODO sort ions alphabetically
        java.util.List<IonSpecies> sortedIons = new Vector<IonSpecies>();
        for (IonSpecies ion : ions)
            sortedIons.add(ion);
        
        // Also include the selected ion in the sorted list
        sortedIons.add(selectedItem);

        Collections.sort(sortedIons);
        
        box.removeAllItems();
                
        // box.addItem(selectedItem);
        for (IonSpecies ion : sortedIons)
            box.addItem(ion);

        box.setSelectedItem(selectedItem);
        
        box.setEnabled(true);
    }
    
    public void actionPerformed(ActionEvent event) {

        if (event.getSource() == deleteButton) {
            ionSelectionDialog.deleteIonSelectionPanel(this);
            setVisible(false);
        }
        
        // User selected a new ion type
        if ( (ionTypePanel != null) && (ionTypePanel.ionBox != null) &&
                (event.getSource() == ionTypePanel.ionBox) ) {
            IonSpecies newIon = (IonSpecies) ionTypePanel.ionBox.getSelectedItem();
            if (newIon == null) return;
            // Has the chosen ion actually changed? (avoid a potential race condition)
            if (! newIon.equals(cachedIonSpecies)) {
                cachedIonSpecies = newIon;
                ionSelectionDialog.updateIonChoices();
                ionSelectionDialog.updateConcentrationRanges(
                        ionSelectionDialog.getAvailableRanges());

                // Choose best possible initial concentration for new ion
                adjustConcentrationToNeutralizeCharge();
            }
        }

    }
    
    public IonSpecies displayedIon() {
        return ionTypePanel.displayedIon();
    }

    public void setConcentrationValues(Set<Double> concentrations) {
        ionConcentrationPanel.setConcentrationValues(concentrations);
    }
    
    public double netCharge() {
        double charge = 0.0;
        if (displayedIon() != null) 
            charge = concentration() * displayedIon().getCharge();
        return charge;
    }
    
    public void adjustConcentrationToNeutralizeCharge() {
        double excessCharge = ionSelectionDialog.netCharge() - netCharge();
        double newConcentration = - excessCharge / displayedIon().getCharge();
        if (newConcentration < minConcentration()) newConcentration = minConcentration();
        if (newConcentration > maxConcentration()) newConcentration = maxConcentration();
        setConcentration(newConcentration);
    }

    protected void enableDeletion() {
        deleteButton.setEnabled(true);
        deleteButton.setToolTipText("Remove this ion.");
    }
    
    protected void disableDeletion() {
        deleteButton.setEnabled(false); // don't delete initial panel
        deleteButton.setToolTipText("At least one ion is required");
    }
    
    // The panel on the left, where the user chooses the type of ion
    class IonTypePanel extends JPanel {
        protected String instructionLabel = new String("(Choose an ion)");
        protected JComboBox ionBox = new JComboBox();
        
        IonTypePanel(IonSpecies initialIon) {
            setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
            setBorder(BorderFactory.createEmptyBorder(10,10,10,10));

            JLabel label = new JLabel("Choose ion type:");
            // Create a whole panel, just to left-justify the label
            JPanel labelPanel = new JPanel();
            labelPanel.setLayout(new BoxLayout(labelPanel, BoxLayout.X_AXIS));
            labelPanel.add(label);
            labelPanel.add(Box.createHorizontalGlue());
            label.setHorizontalAlignment(SwingConstants.LEFT);

            add(labelPanel);

            ionBox.setRenderer(new IonSpeciesRenderer());
            ionBox.addActionListener(IonSelectionPanel.this);
            ionBox.addActionListener(ionSelectionDialog);
            ionBox.setToolTipText("Choose the ion type.");
            add(ionBox);
            
            // Stop the stupid JComboBox from growing vertically
            // Why does it do that???
            Dimension d = ionBox.getPreferredSize();
            d.width = Integer.MAX_VALUE;
            ionBox.setMaximumSize(d);
            
            ionBox.addItem(initialIon);
            ionBox.setSelectedItem(initialIon);

            add(Box.createVerticalStrut(5));
            
            deleteButton.setHorizontalAlignment(JButton.CENTER);
            disableDeletion();
            JPanel deleteButtonPanel = new JPanel();
            deleteButtonPanel.setLayout(new BoxLayout(deleteButtonPanel, BoxLayout.X_AXIS));
            deleteButtonPanel.add(deleteButton);
            deleteButtonPanel.add(Box.createHorizontalGlue()); // left justify delete button
            add(deleteButtonPanel);
            
            add(Box.createVerticalGlue()); // Top justify
            
            // Let the other concentration panel fill up extra width
            setMaximumSize(new Dimension(getPreferredSize().width, Integer.MAX_VALUE));
        }
        
        public IonSpecies displayedIon() {
            return (IonSpecies) ionBox.getSelectedItem();
        }
        
        class IonSpeciesRenderer extends DefaultListCellRenderer
        implements ListCellRenderer {
            public IonSpeciesRenderer() {
                setOpaque(true);
                setHorizontalAlignment(LEFT);
                setVerticalAlignment(CENTER);
            }
            
            @Override
            public Component getListCellRendererComponent(
                    JList list,
                    Object value,
                    int index,
                    boolean isSelected,
                    boolean cellHasFocus)
            {
                Component component = super.getListCellRendererComponent(
                        list, value, index, isSelected, cellHasFocus);

                String labelText = value.toString();

                if (value instanceof IonSpecies) {
                    IonSpecies ion = (IonSpecies) value;
                    
                    labelText = " " + ion.getIonId();
                    float charge = ion.getCharge();
                    DecimalFormat chargeFormat = new DecimalFormat("0");
                    String chargeString = chargeFormat.format(charge);
                    if (charge > 0) chargeString = "+"+chargeString;
                    labelText += " (" + chargeString + ")";
                }
                
                if (component instanceof JLabel) {
                    JLabel label = (JLabel) component;
                    label.setText(labelText);
                }
                
                return component;
            }
        }
    }
    
    // The panel on the right, where the user chooses the ion concentration
    class IonConcentrationPanel 
    extends JPanel 
    implements ChangeListener, ActionListener, ComponentListener
    {
        protected JLabel label = new JLabel("Choose ion concentration:");
        protected JComboBox pulldown = new JComboBox();
        protected LogarithmicJSlider slider;
        protected double sliderTickFactor = 10.0;
        
        IonConcentrationPanel() {
            
            // Test values for slider
            slider = new LogarithmicJSlider();
            
            NativeTickedSlider.setNativeTickUI(slider);
            
            slider.setMajorTickSpacing(10);
            slider.setMinorTickSpacing(0);
            slider.setPaintTicks(true);
            slider.setPaintLabels(true);
            slider.addChangeListener(this);
            slider.setToolTipText("Drag the slider to adjust the ion concentration.");
            // createNewSliderLabels();
            slider.addComponentListener(this);
            slider.setSnapToTicks(false);
            
            setConcentrationRange(0.10, 100.0);

            pulldown.setEditable(true);
            pulldown.addActionListener(this);
            pulldown.setToolTipText("Type value and press ENTER (or select from menu)");
            
            setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
            setBorder(BorderFactory.createEmptyBorder(10,10,10,10));

            // Create a whole panel, just to left-justify the label
            JPanel labelPanel = new JPanel();
            labelPanel.setLayout(new BoxLayout(labelPanel, BoxLayout.X_AXIS));
            labelPanel.add(label);
            labelPanel.add(Box.createHorizontalGlue());
            label.setHorizontalAlignment(SwingConstants.LEFT);
            
            add(labelPanel);
            add(pulldown);
            add(slider);
            add(Box.createVerticalGlue()); // Top justify 
            
            // setEnabled(false); // default to not enabled
            
            updateConcentrationTextBox();

            // Stop the stupid JComboBox from growing vertically
            Dimension d = pulldown.getPreferredSize();
            d.width = Integer.MAX_VALUE;
            pulldown.setMaximumSize(d);
        }
        
        public void setEnabled(boolean isEnabled) {
            label.setEnabled(isEnabled);
            pulldown.setEnabled(isEnabled);
            slider.setEnabled(isEnabled);
        }
        
        // What to do when the slider is moved
        int cachedSliderValue = -1;
        public void stateChanged(ChangeEvent event) {
            
            if (event.getSource() == slider) {
                
                // If the user is actively dragging, round the number
                if (slider.getValueIsAdjusting()) { // user is source of this change
                    int internalValue = slider.getValue();
                    if (internalValue != cachedSliderValue) { // The slider value actually changed
                        cachedSliderValue = internalValue;
                        double concentration = slider.userValueForInternalValue(internalValue);

                        // Round all values selected by slider
                        concentration = roundedConcentration(concentration);

                        setConcentration(concentration);
                    }                    
                }
            }
        }
        
        // What to do when the combo box is used
        public void actionPerformed(ActionEvent event) {

            if (event.getSource() == pulldown) {
                // Restore default color when anything happens
                pulldown.getEditor().getEditorComponent().setForeground(Color.BLACK);                        

                Object item = pulldown.getSelectedItem();
                
                // The selected item should be a Number if selected using slider or pulldown
                // The item will be a *String* only when the user typed something in
                // The item will be a ErroneousConcentrationFormat when the user just typed a goofy string.

                if (item instanceof String) { // Concentration selected by user typing
                    boolean isGoodParse = false; // begin pessimistic
                    
                    // Parse what the user typed as a concentration
                    String userString = (String) item;
                    // System.out.println("User typed: " + userString);
                    Pattern concentrationPattern = 
                        Pattern.compile("^\\s*(([0-9]*\\.)?[0-9]+)\\s*([mµu\u03BC]?M)?\\s*$");
                    Matcher matcher = concentrationPattern.matcher(userString);
                    if ( matcher.matches() ) {
                        double value = new Double(matcher.group(1));
                        String units = matcher.group(3);
                        
                        // Default units is mM (millimolar)
                        if (units == null) {} // no units? assume millimolar
                        else if (units.equals("uM")) value *= 0.001;
                        else if (units.equals("\u03BCM")) value *= 0.001;
                        else if (units.equals("µM")) value *= 0.001;
                        else if (units.equals("M")) value *= 1000.0;
                        
                        if ( (value >= minConcentration()) &&
                             (value <= maxConcentration()) ) {
                            
                            // The first call to set concentration fires a slider
                            // "stateChanged()", which rounds the value
                            setConcentration(value);
                            
                            isGoodParse = true;
                        }
                        
                        // System.out.println("Value = "+value+" Units = "+units);
                    }
                    else {
//                        System.out.println("Problem with input: "+userString);
//
//                        Pattern p = 
//                            Pattern.compile("^\\s*(([0-9]*\\.)?[0-9]+)\\s*((.)M)?\\s*$");
//                        Matcher m = p.matcher(userString);
//                        if (m.matches()) {
//                            String unitModifier = m.group(4);
//                            char unitChar = unitModifier.charAt(0);
//                            System.out.println("Units modifier = : " + unitModifier +" ("+unitChar+")");
//                        }
                    }
                    
                    
                    if (! isGoodParse) {
                        pulldown.getEditor().getEditorComponent().setForeground(Color.RED);
                    }
                    
                }
                
                else if (item instanceof Number) { // Concentration selected from menu, not by typing
                    Number number = (Number) item;
                    // Two calls to setConcentration() prevents rounding
                    setConcentration(number.doubleValue());
                }
            }
        }
    

        public double minConcentration() {return slider.getDoubleMinimum();}
        public double maxConcentration() {return slider.getDoubleMaximum();}
        public double concentration() {return slider.getUserValue();}

        public void setConcentration(double concentration) {
            if (slider.getUserValue() == concentration) return;

            slider.setUserValue(concentration);

            updateConcentrationTextBox();
            ionSelectionDialog.updateNetCharge();
        }
        
        public void setConcentrationRange(double min, double max) {
            if ( (slider.getDoubleMinimum() == min) &&
                 (slider.getDoubleMaximum() == max) ) return; // short circuit
            
            slider.setDoubleRange(min, max);
            createNewSliderLabels();
        }
        
        public void setConcentrationValues(Set<Double> concentrations) {
            // Remember what item was selected, because
            // selection can be replaced by a cascade of events
            double currentConcentration = concentration();
            
            pulldown.removeAllItems();
            for (Double c : concentrations)
                pulldown.addItem(new DisplayableIonConcentration(c));
            
            setConcentration(currentConcentration);
            
            updateConcentrationTextBox();
        }

//        protected void updateSliderTickSpacing() {
//            int min = sliderValueForConcentration(minConcentration());
//            int next = sliderValueForConcentration(sliderTickFactor * minConcentration);
//            int tickSpacing = next - min;
//            slider.setMajorTickSpacing(tickSpacing);
//        }
        
        // ComponentListener interface
        public void componentHidden(ComponentEvent event) {}
        public void componentShown(ComponentEvent event) {}
        public void componentMoved(ComponentEvent event) {}
        // Capture resize events for labelling - overriding setSize() doesn't work
        public void componentResized(ComponentEvent event) {
            if (event.getSource() == slider) {
                // System.out.println("componentResized jslider");
                createNewSliderLabels();
            }
        }


        // Put these in global namespace so multiple JScrollbar labelling methods can share them
        private Hashtable<Double, JLabel> labelTable = new Hashtable<Double, JLabel>();
        private Set<Double> majorTicks = new HashSet<Double>();
        private Set<Double> minorTicks = new HashSet<Double>();
        private double previousLabelFraction = -1.0;
        private double labelFraction = 0.0;
        private void setMinorTick(double value) {
            if ( (value > minConcentration()) && (value < maxConcentration()) )
                minorTicks.add(value);
        }
        private void setLabel(double value) {
            if ( (value >= minConcentration()) && (value <= maxConcentration()) ) {
                majorTicks.add(value);

                // Is the label too close to the previous one?
                double tickFraction = slider.userValueFraction(value);
                double intervalFraction = tickFraction - previousLabelFraction;
                if (intervalFraction > labelFraction) { // enough separation on the left
                    // Is there enough separation on the right?
                    intervalFraction = 1.0 - tickFraction;
                    if (intervalFraction > labelFraction) // enough separation from max label
                        definitelySetLabel(value);
                }
            }
        }
        private void definitelySetLabel(double value) {
            JLabel label = new JLabel( ""+new DisplayableIonConcentration(value));
            labelTable.put(value, label);
            previousLabelFraction = slider.userValueFraction(value);            
        }
        
        private void setMinorTicks(double start, double interval) {
            for (int i = 1; i < 9; ++i) {
                double value = start + i * interval;
                // System.out.println("minor tick at " + value);
                setMinorTick(value);
            }
        }

        public void createNewSliderLabels() {
            // Initialize data structures
            labelTable.clear();
            majorTicks.clear();
            minorTicks.clear();
            previousLabelFraction = -1.0;
            labelFraction = 0.0;

            double min = minConcentration();
            double max = maxConcentration();
            double logTen = Math.log(10.0);
            double logMin = Math.log(min)/logTen;
            double logMax = Math.log(max)/logTen;
            
            // Alway paint min and max value
            definitelySetLabel(max);
            definitelySetLabel(min); // set min second, so previousLabelFraction gets set to 0.0
            
            JLabel minLabel = new JLabel( ""+new DisplayableIonConcentration(min));
            JLabel maxLabel = new JLabel( ""+new DisplayableIonConcentration(max));
            int labelWidth = Math.max(minLabel.getPreferredSize().width,
                    maxLabel.getPreferredSize().width);

            int trackWidth = getSize().width - labelWidth; // kludge
            if (trackWidth <= 0) return; // No width? => no labels.

            double minLabelSeparation = labelWidth + 10;
            labelFraction = ((double) minLabelSeparation) / ((double) trackWidth);

            // Largest power of ten less than starting concentration
            double baseConcentration = 
                Math.pow(10.0, Math.floor(logMin));            
            
            double magnitudesPerTrack = logMax - logMin;
            double magnitudeWidth = trackWidth / magnitudesPerTrack;
            // System.out.println("magnitudesPerTrack = " + magnitudesPerTrack);
            // System.out.println("magnitudeWidth = " + magnitudeWidth);

            double magnitude = baseConcentration;
            while (magnitude < max) {
                // System.out.println("magnitude = "+magnitude);
                
                // Always put a major tick at each magnitude
                // setLabel(magnitude); // happens automatically later
                
                double labelCount = magnitudeWidth / minLabelSeparation;
                // System.out.println("labelCount = " + labelCount);
                
                // Start with largest dynamic range cases
                if ( labelCount <= 3 ) { // no additional major labels
                    setLabel(magnitude);
                    setMinorTicks(magnitude, magnitude);
                }
                else if ( labelCount <= 4) { // use 3/10 log type labels
                    setLabel(magnitude);
                    setLabel(3 * magnitude);
                    setMinorTicks(magnitude, magnitude);
                }
                else if ( labelCount <= 10) { // use 2/5/10 log type labels
                    setLabel(magnitude);
                    setLabel(2 * magnitude);
                    setLabel(5 * magnitude);
                    setMinorTicks(magnitude, magnitude);
                }
                else { // linear-ish labeling
                    double rangeMin = Math.max(magnitude, min);
                    double rangeMax = Math.min(10 * magnitude, max);
                    double range = rangeMax - rangeMin;
                    double rangeFraction = range/(max - min); // fraction of slider

                    double incrementUnits = Math.pow( 10.0, Math.floor(Math.log(3 * range) / Math.log(10.0)));
                    // System.out.println("increment units = "+incrementUnits);
                    
                    // How many labels per 10* increment in the range?
                    // Can never show more than this many labels
                    int maxLabelCount = (int) Math.floor(rangeFraction/labelFraction) + 1;
                    double maxLabelsPerIncrement = (incrementUnits * maxLabelCount) / range;
                    // System.out.println("max labels = "+maxLabelsPerIncrement);
                    
                    double[] majorTickIncrements;
                    double[] minorTickIncrements;
                    
                    if (maxLabelsPerIncrement <= 2.0) {
                        double[] t = {0.0}; 
                        majorTickIncrements = t;
                        double[] t2 = {0.2, 0.4, 0.6, 0.8}; 
                        minorTickIncrements = t2;
                    }
                    else if (maxLabelsPerIncrement <= 6.0) {
                        double[] t = {0.0, 0.5};
                        majorTickIncrements = t;                    
                        double[] t2 = {0.1, 0.2, 0.3, 0.4, 0.6, 0.7, 0.8, 0.9}; 
                        minorTickIncrements = t2;
                    }
                    else if (maxLabelsPerIncrement <= 12.0) {
                        double[] t = {0.0, 0.2, 0.4, 0.6, 0.8}; 
                        majorTickIncrements = t;                    
                        double[] t2 = {0.1, 0.3, 0.5, 0.7, 0.9}; 
                        minorTickIncrements = t2;
                    }
                    else {
                        double[] t = {0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9}; 
                        majorTickIncrements = t;
                        double[] t2 = {0.05, 0.15, 0.25, 0.35, 0.45, 0.55, 0.65, 0.75, 0.85, 0.95}; 
                        minorTickIncrements = t2;
                    }

                    // Place the ticks and labels
                    for (double majorValue = magnitude; majorValue < rangeMax; majorValue += incrementUnits) {

                        // Major ticks with labels
                        for (int i = 0; i < majorTickIncrements.length; ++i) {
                            double value = majorValue + majorTickIncrements[i] * incrementUnits;
                            setLabel(value);
                        }

                        // Minor ticks
                        for (int i = 0; i < minorTickIncrements.length; ++i) {
                            double value = majorValue + minorTickIncrements[i] * incrementUnits;
                            setMinorTick(value);
                        }
                    }
                } 

                magnitude *= 10.0;
            }
            
            // Store the labels we created
            slider.setLabelTable(labelTable);
            
            // Store the tick marks we created
            // Are we allowed to place custom ticks with this slider type?
            if (slider.getUI() instanceof CustomTickSliderUI) {
                CustomTickSliderUI ui = (CustomTickSliderUI) slider.getUI();
                
                // Convert user values to slider values
                Set<Integer> majorIntTicks = new HashSet<Integer>();
                for (double d : majorTicks) 
                    majorIntTicks.add(slider.internalValueForUserValue(d));
                Set<Integer> minorIntTicks = new HashSet<Integer>();
                for (double d : minorTicks)
                    minorIntTicks.add(slider.internalValueForUserValue(d));

                ui.setMajorTickPositions(majorIntTicks);
                ui.setMinorTickPositions(minorIntTicks);
            }
        }

        
        protected void updateConcentrationTextBox() {
            DisplayableIonConcentration c = 
                new DisplayableIonConcentration(concentration());

            pulldown.setSelectedItem(c);
        }
        
        protected double roundedConcentration(double conc) {
            // Avoid small roundoff errors by using BigDecimal methods
            BigDecimal roundedDecimal = 
                new BigDecimal(2.0 * conc, new MathContext(2));
            roundedDecimal = roundedDecimal.divide(new BigDecimal(2.0));
            return roundedDecimal.doubleValue();
        }

        // This method might be subject to small roundoff errors
        protected double oldRoundedConcentration(double conc) {
            if (conc == 0) return 0;
            
            double concentration = Math.abs(conc);
            double sign = (conc < 0) ? -1.0 : 1.0;
            
            double logConcentration = Math.log(concentration);
            // Round to 2.5 significant figures (third figure is 0 or 5)
            int decimalExponent = (int) Math.floor(logConcentration/Math.log(10.0)) - 1;
            double decimalMultiplier = Math.pow(10.0, decimalExponent);
            double roundedConcentration = 
                decimalMultiplier * 0.50 * Math.round((1.0/decimalMultiplier) * 2.0 * concentration);
            
            return roundedConcentration * sign;
        }

        /**
         *  
          * @author Christopher Bruns
          * 
          * Class with sole use to show user red text version of improperly
          * formatted ion concentration in pulldown editor on concentration panel.
         */
        class ErroneousConcentrationFormat {
            protected String string;
            
            public ErroneousConcentrationFormat(String s) {
                string = s;
            }
            
            @Override
            public String toString() {return string;}
        }        
    }        
}
