001/*
002 * $Id: LinearCombo.java,v 1.52 2011/03/24 16:06:32 davep Exp $
003 *
004 * This file is part of McIDAS-V
005 *
006 * Copyright 2007-2011
007 * Space Science and Engineering Center (SSEC)
008 * University of Wisconsin - Madison
009 * 1225 W. Dayton Street, Madison, WI 53706, USA
010 * https://www.ssec.wisc.edu/mcidas
011 * 
012 * All Rights Reserved
013 * 
014 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
015 * some McIDAS-V source code is based on IDV and VisAD source code.  
016 * 
017 * McIDAS-V is free software; you can redistribute it and/or modify
018 * it under the terms of the GNU Lesser Public License as published by
019 * the Free Software Foundation; either version 3 of the License, or
020 * (at your option) any later version.
021 * 
022 * McIDAS-V is distributed in the hope that it will be useful,
023 * but WITHOUT ANY WARRANTY; without even the implied warranty of
024 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
025 * GNU Lesser Public License for more details.
026 * 
027 * You should have received a copy of the GNU Lesser Public License
028 * along with this program.  If not, see http://www.gnu.org/licenses.
029 */
030
031package edu.wisc.ssec.mcidasv.control;
032
033import java.awt.Color;
034import java.awt.Container;
035import java.awt.Dimension;
036import java.rmi.RemoteException;
037import java.util.ArrayList;
038import java.util.Collection;
039import java.util.HashMap;
040import java.util.HashSet;
041import java.util.Hashtable;
042import java.util.Iterator;
043import java.util.LinkedHashSet;
044import java.util.List;
045import java.util.Map;
046import java.util.Map.Entry;
047import java.util.Set;
048
049import javax.swing.JComponent;
050import javax.swing.JPanel;
051import javax.swing.JTabbedPane;
052
053import org.python.core.PyDictionary;
054import org.python.core.PyFloat;
055import org.python.core.PyInteger;
056import org.slf4j.Logger;
057import org.slf4j.LoggerFactory;
058
059import visad.ConstantMap;
060import visad.Data;
061import visad.Real;
062import visad.VisADException;
063import visad.georef.MapProjection;
064
065import ucar.unidata.data.DataChoice;
066import ucar.unidata.data.DataSource;
067import ucar.unidata.data.DirectDataChoice;
068import ucar.unidata.idv.MapViewManager;
069import ucar.unidata.util.GuiUtils;
070import ucar.unidata.util.LogUtil;
071import ucar.unidata.view.geoloc.MapProjectionDisplay;
072import ucar.visad.display.DisplayMaster;
073
074import edu.wisc.ssec.mcidasv.Constants;
075import edu.wisc.ssec.mcidasv.McIDASV;
076import edu.wisc.ssec.mcidasv.data.ComboDataChoice;
077import edu.wisc.ssec.mcidasv.data.hydra.MultiDimensionSubset;
078import edu.wisc.ssec.mcidasv.data.hydra.MultiSpectralData;
079import edu.wisc.ssec.mcidasv.data.hydra.MultiSpectralDataSource;
080import edu.wisc.ssec.mcidasv.display.hydra.MultiSpectralDisplay;
081import edu.wisc.ssec.mcidasv.display.hydra.MultiSpectralDisplay.DragLine;
082import edu.wisc.ssec.mcidasv.jython.Console;
083import edu.wisc.ssec.mcidasv.jython.ConsoleCallback;
084
085public class LinearCombo extends HydraControl implements ConsoleCallback {
086
087    /** Trusty logging object. */
088    private static final Logger logger = LoggerFactory.getLogger(LinearCombo.class);
089
090    /** Help topic identifier. */
091    public static final String HYDRA_HELP_ID = 
092        "idv.controls.hydra.linearcombinationcontrol";
093
094    /** 
095     * Path to the Jython source code that allows for interaction with a 
096     * linear combination display control.
097     */
098    public static final String HYDRA_SRC = 
099        "/edu/wisc/ssec/mcidasv/resources/python/linearcombo/hydra.py";
100
101    /** Name used in Jython namespace to refer to the {@literal "IDV god object"}. */
102    public static final String CONSOLE_IDV_OBJECT = "_idv";
103
104    /** 
105     * Name used in Jython namespace to refer back to an instantiation of a 
106     * linear combination control.
107     */
108    public static final String CONSOLE_CONTROL_OBJECT = "_linearCombo";
109
110    public static final String CONSOLE_OBJECT = "_jythonConsole";
111
112    public static final String CONSOLE_DATA_OBJECT = "_data";
113
114    private Console console;
115
116    private MultiSpectralDisplay display;
117
118    private DisplayMaster displayMaster;
119
120    private String sourceFile = "";
121
122    private ComboDataChoice comboChoice;
123
124    private MultiSpectralDataSource source;
125
126    private List<String> jythonHistory;
127
128    private Map<String, Selector> selectorMap;
129
130    private Map<String, Selector> jythonMap;
131
132    private DataChoice dataChoice = null;
133
134    /**
135     * 
136     */
137    public LinearCombo() {
138        super();
139        setHelpUrl(HYDRA_HELP_ID);
140        jythonHistory = new ArrayList<String>();
141        selectorMap = new HashMap<String, Selector>();
142        jythonMap = new HashMap<String, Selector>();
143    }
144
145    @Override public boolean init(final DataChoice choice) throws VisADException, RemoteException {
146        List<DataSource> sources = new ArrayList<DataSource>();
147        choice.getDataSources(sources);
148        dataChoice = choice;
149
150        ((McIDASV)getIdv()).getMcvDataManager().setHydraControl(choice, this);
151
152        source = ((MultiSpectralDataSource)sources.get(0));
153        sourceFile = source.getDatasetName();
154
155        MultiSpectralData data = source.getMultiSpectralData(choice);
156
157        Float fieldSelectorChannel = (Float)getDataSelection().getProperty(Constants.PROP_CHAN);
158        if (fieldSelectorChannel == null)
159            fieldSelectorChannel = data.init_wavenumber;
160
161        console = new Console();
162        console.setCallbackHandler(this);
163
164        console.injectObject(CONSOLE_IDV_OBJECT, getIdv());
165        console.injectObject(CONSOLE_CONTROL_OBJECT, this);
166        console.injectObject(CONSOLE_OBJECT, console);
167        console.injectObject(CONSOLE_DATA_OBJECT, source.getMultiSpectralData(choice));
168
169        console.runFile("__main__", "/edu/wisc/ssec/mcidasv/resources/python/console_init.py");
170        console.runFile("__main__", HYDRA_SRC);
171
172        display = new MultiSpectralDisplay((DirectDataChoice)choice);
173        display.setWaveNumber(fieldSelectorChannel);
174        display.setDisplayControl(this);
175        ((McIDASV)getIdv()).getMcvDataManager().setHydraDisplay(choice, display);
176        return true;
177    }
178
179    @Override public void initDone() {
180        MapViewManager viewManager = (MapViewManager)getViewManager();
181        MapProjectionDisplay dispMaster = 
182            (MapProjectionDisplay)viewManager.getMaster();
183        
184        try {
185            dispMaster.setMapProjection(getDataProjection());
186        } catch (Exception e) {
187            logException("problem setting MapProjection", e);
188        }
189
190        getIdv().getIdvUIManager().showDashboard();
191        console.queueBatch("history", jythonHistory);
192        jythonHistory.clear();
193    }
194
195    public List<String> getJythonHistory() {
196        return console.getHistory();
197    }
198
199    public void setJythonHistory(final List<String> persistedHistory) {
200        jythonHistory = persistedHistory;
201    }
202
203    @Override public MapProjection getDataProjection() {
204        MapProjection mp = null;
205        HashMap subset = null;
206        Hashtable table = dataChoice.getProperties();
207        MultiDimensionSubset dataSel =
208           (MultiDimensionSubset)table.get(MultiDimensionSubset.key);
209        
210        if (dataSel != null) {
211            subset = dataSel.getSubset();
212        }
213        mp = source.getDataProjection(subset);
214        return mp;
215    }
216
217    @Override public Container doMakeContents() {
218        JTabbedPane pane = new JTabbedPane();
219        pane.add("Console", GuiUtils.inset(getConsoleTab(), 5));
220        GuiUtils.handleHeavyWeightComponentsInTabs(pane);
221        return pane;
222    }
223
224    private JComponent getConsoleTab() {
225        JPanel consolePanel = console.getPanel();
226        consolePanel.setPreferredSize(new Dimension(500, 150));
227        return GuiUtils.topCenter(display.getDisplayComponent(), consolePanel);
228    }
229
230    @Override public void doRemove() throws VisADException, RemoteException {
231        super.doRemove();
232    }
233
234    @Override public String toString() {
235        return "[LinearCombo@" + Integer.toHexString(hashCode()) + 
236            ": sourceFile=" + sourceFile + ']';
237    }
238
239    public void moveSelector(final String id, final float wavenum) {
240        if (!selectorMap.containsKey(id)) {
241            return;
242        }
243        display.updateControlSelector(id, wavenum);
244    }
245
246    public void updateSelector(final String id, final float wavenum) {
247        if (!selectorMap.containsKey(id)) {
248            return;
249        }
250
251        selectorMap.get(id).setWaveNumber(wavenum);
252        String cmd = new StringBuilder("_linearCombo.moveSelector('")
253            .append(id)
254            .append("', ")
255            .append(wavenum)
256            .append(')')
257            .toString();
258
259        console.addPretendHistory(cmd);
260    }
261
262    protected void addSelector(final Selector selector) throws Exception {
263        ConstantMap[] mapping = selector.getColor();
264        float r = Double.valueOf(mapping[0].getConstant()).floatValue();
265        float g = Double.valueOf(mapping[1].getConstant()).floatValue();
266        float b = Double.valueOf(mapping[2].getConstant()).floatValue();
267        Color javaColor = new Color(r, g, b);
268        display.createSelector(selector.getId(), javaColor);
269        display.setSelectorValue(selector.getId(), selector.getWaveNumber());
270        selectorMap.put(selector.getId(), selector);
271        logger.trace("added selector={}", selector);
272    }
273
274    protected MultiSpectralDisplay getMultiSpectralDisplay() {
275        return display;
276    }
277
278    protected int getSelectorCount() {
279        return selectorMap.size();
280    }
281
282    private Set<String> getSelectorIds(final Map<String, Object> objMap) {
283        assert objMap != null : objMap;
284
285        Set<String> ids = new HashSet<String>();
286        Collection<Object> jython = objMap.values();
287
288        for (Iterator<Object> i = jython.iterator(); i.hasNext();) {
289            Object obj = i.next();
290            if (!(obj instanceof Selector)) {
291                continue;
292            }
293
294            String selectorId = ((Selector)obj).getId();
295            ids.add(selectorId);
296        }
297
298        return ids;
299    }
300
301    /**
302     * 
303     * 
304     * @param objMap
305     * 
306     * @return
307     */
308    private Map<String, Selector> mapNamesToThings(final Map<String, Object> objMap) {
309        assert objMap != null : objMap;
310
311        Map<String, Selector> nameMap = new HashMap<String, Selector>(objMap.size());
312        Set<Selector> seen = new LinkedHashSet<Selector>();
313        for (Map.Entry<String, Object> entry : objMap.entrySet()) {
314            Object obj = entry.getValue();
315            if (!(obj instanceof Selector)) {
316                continue;
317            }
318
319            String name = entry.getKey();
320            Selector selector = (Selector)obj;
321            if (!seen.contains(selector)) {
322                seen.add(selector);
323                selector.clearNames();
324            }
325            nameMap.put(name, selector);
326            selector.addName(name);
327        }
328        return nameMap;
329    }
330
331    public float getInitialWavenumber() {
332        return display.getMultiSpectralData().init_wavenumber;
333    }
334
335    public PyDictionary getBandNameMappings() {
336        PyDictionary map = new PyDictionary();
337        MultiSpectralData data = display.getMultiSpectralData();
338        if (!data.hasBandNames())
339           return map;
340
341        for (Entry<String, Float> entry : data.getBandNameMap().entrySet()) {
342            map.__setitem__(entry.getKey(), new PyFloat(entry.getValue()));
343        }
344
345        return map;
346    }
347
348    public void addCombination(final String name, final Data combo) {
349        source.addChoice(name, combo);
350    }
351
352//    public void addRealCombination(final String name, final Combination combo) {
353//        source.addRealCombo(name, combo, console);
354//    }
355//
356//    public Console getConsole() {
357//        return console;
358//    }
359
360    /**
361     * Called after Jython's internals have finished processing {@code line}
362     * (and before control is given back to the user).
363     * 
364     * <p>This is where {@code LinearCombo} controls map Jython names to Java
365     * objects.
366     */
367    public void ranBlock(final String line) {
368        List<DragLine> dragLines = display.getSelectors();
369        Map<String, Object> javaObjects = console.getJavaInstances();
370        Set<String> ids = getSelectorIds(javaObjects);
371        for (DragLine dragLine : dragLines) {
372            String lineId = dragLine.getControlId();
373            if (!ids.contains(lineId)) {
374                display.removeSelector(lineId);
375                selectorMap.remove(lineId);
376            }
377        }
378
379        jythonMap = mapNamesToThings(javaObjects);
380        logger.trace("ranBlock: javaObjs={}", javaObjects);
381    }
382
383//    public void saveJythonThings() {
384//        // well, only selectors so far...
385//        for (Map.Entry<String, Selector> entry : jythonMap.entrySet()) {
386//            String cmd = String.format("%s.setWaveNumber(%f)", entry.getKey(), entry.getValue().getWaveNumber());
387//            System.err.println("saving: "+cmd);
388//            console.addMetaCommand(cmd);
389//        }
390//    }
391
392    public static abstract class JythonThing {
393        protected Set<String> jythonNames = new LinkedHashSet<String>();
394        public JythonThing() { }
395        public abstract Data getData();
396
397        public static String colorString(final ConstantMap[] color) {
398            if (color == null) {
399                return "[null]";
400            }
401            if (color.length != 3) {
402                return "[invalid color string]";
403            }
404
405            double r = color[0].getConstant();
406            double g = color[1].getConstant();
407            double b = color[2].getConstant();
408            return String.format("[r=%.3f; g=%.3f; b=%.3f]", r, g, b);
409        }
410
411        private static Data extractData(final Object other) throws VisADException, RemoteException {
412            if (other instanceof JythonThing) {
413                return ((JythonThing)other).getData();
414            }
415            if (other instanceof PyFloat) {
416                return new Real(((PyFloat)other).getValue());
417            }
418            if (other instanceof PyInteger) {
419                return new Real(((PyInteger)other).getValue());
420            }
421            if (other instanceof Double) {
422                return new Real((Double)other);
423            }
424            if (other instanceof Integer) {
425                return new Real((Integer)other);
426            }
427            if (other instanceof Data) {
428                return (Data)other;
429            }
430            throw new IllegalArgumentException("Can't figure out what to do with " + other);
431        }
432
433        private static String extractName(final Object other) {
434            if (other instanceof JythonThing) {
435                return ((JythonThing)other).getName();
436            }
437            if (other instanceof PyInteger) {
438                return ((PyInteger)other).toString();
439            }
440            if (other instanceof PyFloat) {
441                return ((PyFloat)other).toString();
442            }
443            if (other instanceof Double) {
444                return ((Double)other).toString();
445            }
446            if (other instanceof Integer) {
447                return ((Integer)other).toString();
448            }
449            throw new IllegalArgumentException("UGH: "+other);
450        }
451
452        public abstract boolean removeName(final String name);
453        public abstract boolean addName(final String name);
454        public abstract String getName();
455        public abstract Collection<String> getNames();
456
457        public Combination __add__(final Object other) throws VisADException, RemoteException {
458            return new AddCombination(this, other);
459        }
460        public Combination __sub__(final Object other) throws VisADException, RemoteException {
461            return new SubtractCombination(this, other);
462        }
463        public Combination __mul__(final Object other) throws VisADException, RemoteException {
464            return new MultiplyCombination(this, other);
465        }
466        public Combination __div__(final Object other) throws VisADException, RemoteException {
467            return new DivideCombination(this, other);
468        }
469        public Combination __pow__(final Object other) throws VisADException, RemoteException {
470            return new ExponentCombination(this, other);
471        }
472        public Combination __mod__(final Object other) throws VisADException, RemoteException {
473            return new ModuloCombination(this, other);
474        }
475        public Combination __radd__(final Object other) throws VisADException, RemoteException {
476            return new AddCombination(other, this);
477        }
478        public Combination __rsub__(final Object other) throws VisADException, RemoteException {
479            return new SubtractCombination(other, this);
480        }
481        public Combination __rmul__(final Object other) throws VisADException, RemoteException {
482            return new MultiplyCombination(other, this);
483        }
484        public Combination __rdiv__(final Object other) throws VisADException, RemoteException {
485            return new DivideCombination(other, this);
486        }
487        public Combination __rpow__(final Object other) throws VisADException, RemoteException {
488            return new ExponentCombination(other, this);
489        }
490        public Combination __rmod__(final Object other) throws VisADException, RemoteException {
491            return new ModuloCombination(other, this);
492        }
493        public Combination __neg__() throws VisADException, RemoteException {
494            return new NegateCombination(this);
495        }
496    }
497
498    /**
499     * 
500     */
501    public static class Selector extends JythonThing {
502
503        /** */
504        private final String ID;
505
506        /** */
507        private float waveNumber;
508
509        /** */
510        private ConstantMap[] color;
511
512        /** */
513        private Console console;
514
515        /** */
516        private HydraControl control;
517
518        /** */
519        private Data data;
520
521        /** */
522        private MultiSpectralDisplay display;
523
524        /**
525         * 
526         * 
527         * @param waveNumber 
528         * @param color 
529         * @param control 
530         * @param console 
531         */
532        public Selector(final float waveNumber, final ConstantMap[] color, final HydraControl control, final Console console) {
533            super();
534            this.ID = hashCode() + "_jython";
535            this.waveNumber = waveNumber;
536            this.control = control;
537            this.console = console;
538            this.display = control.getMultiSpectralDisplay();
539
540            this.color = new ConstantMap[color.length];
541            for (int i = 0; i < this.color.length; i++) {
542                ConstantMap mappedColor = (ConstantMap)color[i];
543                this.color[i] = (ConstantMap)mappedColor.clone();
544            }
545
546            if (control instanceof LinearCombo) {
547                LinearCombo lc = (LinearCombo)control;
548                try {
549                    lc.addSelector(this);
550                } catch (Exception e) {
551                    // TODO(jon): no way jose
552                    System.err.println("Could not create selector: "+e.getMessage());
553                    e.printStackTrace();
554                }
555            }
556        }
557
558        /**
559         * Attempts removal of a known name for the current Selector.
560         * 
561         * @param name Name (within Jython namespace) to remove.
562         * 
563         * @return {@code true} if removal was successful, {@code false} 
564         * otherwise.
565         */
566        public boolean removeName(final String name) {
567            return jythonNames.remove(name);
568        }
569
570        /**
571         * Returns the known Jython names associated with this Selector.
572         * 
573         * @param {@literal "Names"} (aka variables) in a Jython namespace that
574         * refer to this Selector. Collection may be empty, but never 
575         * {@code null}.
576         */
577        public Collection<String> getNames() {
578            return new LinkedHashSet<String>(jythonNames);
579        }
580
581        /**
582         * Resets the known names of a Selector.
583         */
584        public void clearNames() {
585            jythonNames.clear();
586        }
587
588        /**
589         * Attempts to associate a Jython {@literal "variable"/"name"} with 
590         * this Selector.
591         * 
592         * @param name Name used within the Jython namespace. Cannot be 
593         * {@code null}.
594         * 
595         * @return {@code true} if {@code name} was successfully added, 
596         * {@code false} otherwise.
597         */
598        public boolean addName(final String name) {
599            return jythonNames.add(name);
600        }
601
602        /**
603         * Returns a Jython name associated with this Selector. Consider using
604         * {@link #getNames()} instead.
605         * 
606         * @return Either a blank {@code String} if there are no associated 
607         * names, or the {@literal "first"} (iteration-order) name.
608         * 
609         * @see #getNames()
610         */
611        public String getName() {
612            if (jythonNames.isEmpty()) {
613                return "";
614            } else {
615                return jythonNames.iterator().next();
616            }
617        }
618
619        /**
620         * Changes the {@literal "selected"} wave number to the given value.
621         * 
622         * <p><b>WARNING:</b>no bounds-checking is currently being performed,
623         * but this is expected to change in the near future.</p>
624         * 
625         * @param newWaveNumber New wave number to associate with the current
626         * Selector.
627         */
628        public void setWaveNumber(final float newWaveNumber) {
629            waveNumber = newWaveNumber;
630            try {
631                display.setSelectorValue(ID, waveNumber);
632            } catch (Exception e) {
633                LogUtil.logException("Selector.setWaveNumber", e);
634            }
635        }
636
637        /**
638         * Returns the {@literal "selected"} wave number associated with this
639         * Selector.
640         * 
641         * @return Wave number currently selected by this Selector.
642         */
643        public float getWaveNumber() {
644            return waveNumber;
645        }
646
647        /**
648         * 
649         * 
650         * @return
651         */
652        public ConstantMap[] getColor() {
653            return color;
654        }
655
656        /**
657         * 
658         * 
659         * @return 
660         */
661        public Data getData() {
662            return control.getMultiSpectralDisplay().getImageDataFrom(waveNumber);
663        }
664
665        /**
666         * 
667         * 
668         * @return 
669         */
670       public String getId() {
671            return ID;
672        }
673
674       /**
675        * 
676        * @return 
677        */
678       @Override public String toString() {
679           int hashLen = 0;
680           int idLen = 0;
681           int waveLen = 0;
682           int colorLen = 0;
683           int namesLen = 0;
684           return String.format("[Selector@%x: id=%s, waveNumber=%f, color=%s, jythonNames=%s]",
685               hashCode(), ID, waveNumber, colorString(color), jythonNames);
686           
687       }
688    }
689
690    public static abstract class Combination extends JythonThing {
691        private final Object left;
692        private final Object right;
693
694        private final String leftName;
695        private final String rightName;
696
697        private final Data leftData;
698        private final Data rightData;
699
700        private Data operationData;
701
702        public Combination(final Object lhs, final Object rhs) throws VisADException, RemoteException {
703            left = lhs;
704            right = rhs;
705
706            leftName = extractName(left);
707            rightName = extractName(right);
708
709            leftData = extractData(left);
710            rightData = extractData(right);
711        }
712
713        private static Data extractData(final Object obj) throws VisADException, RemoteException {
714            if (obj instanceof JythonThing) {
715                return ((JythonThing)obj).getData();
716            }
717            if (obj instanceof PyFloat) {
718                return new Real(((PyFloat)obj).getValue());
719            }
720            if (obj instanceof PyInteger) {
721                return new Real(((PyInteger)obj).getValue());
722            }
723            if (obj instanceof Double) {
724                return new Real((Double)obj);
725            }
726            if (obj instanceof Integer) {
727                return new Real((Integer)obj);
728            }
729            if (obj instanceof Data) {
730                return (Data)obj;
731            }
732            throw new IllegalArgumentException("Can't figure out what to do with " + obj);
733        }
734
735        protected static String extractName(final Object obj) {
736            if (obj instanceof JythonThing) {
737                return ((JythonThing)obj).getName();
738            }
739            if (obj instanceof PyFloat) {
740                return ((PyFloat)obj).toString();
741            }
742            if (obj instanceof PyInteger) {
743                return ((PyInteger)obj).toString();
744            }
745            if (obj instanceof Double) {
746                return ((Double)obj).toString();
747            }
748            if (obj instanceof Integer) {
749                return ((Integer)obj).toString();
750            }
751            throw new IllegalArgumentException("UGH: "+obj);
752        }
753
754        protected void setOperationData(final Data opData) {
755            operationData = opData;
756        }
757
758        protected Data getOperationData() {
759            return operationData;
760        }
761
762        //public Data 
763        
764        public Object getLeft() {
765            return left;
766        }
767
768        public Object getRight() {
769            return right;
770        }
771
772        public String getLeftName() {
773            return leftName;
774        }
775
776        public String getRightName() {
777            return rightName;
778        }
779
780        public Data getLeftData() {
781            return leftData;
782        }
783
784        public Data getRightData() {
785            return rightData;
786        }
787
788        public boolean removeName(final String name) {
789            return true;
790        }
791
792        public boolean addName(final String name) {
793            return true;
794        }
795
796        public String getName() {
797            return getFriendlyString();
798        }
799
800        public Data getData() {
801            return operationData;
802        }
803
804        public Collection<String> getNames() {
805            Set<String> set = new LinkedHashSet<String>(1);
806            set.add(getFriendlyString());
807            return set;
808        }
809
810        public abstract String getFriendlyString();
811        public abstract String getPersistableString();
812        public abstract String toString();
813    }
814
815    private static class AddCombination extends Combination {
816        public AddCombination(final Object lhs, final Object rhs) throws VisADException, RemoteException {
817            super(lhs, rhs);
818            setOperationData(getLeftData().add(getRightData()));
819        }
820        public String getFriendlyString() {
821            return String.format("(%s + %s)", getLeftName(), getRightName());
822        }
823        public String getPersistableString() {
824            return getFriendlyString();
825        }
826        public String toString() {
827            return String.format("[AddCombo@%x: leftName=%s, rightName=%s, friendlyString=%s, persistableString=%s]", hashCode(), getLeftName(), getRightName(), getFriendlyString(), getPersistableString());
828        }
829    }
830    private static class SubtractCombination extends Combination {
831        public SubtractCombination(final Object lhs, final Object rhs) throws VisADException, RemoteException {
832            super(lhs, rhs);
833            setOperationData(getLeftData().subtract(getRightData()));
834        }
835        public String getFriendlyString() {
836            return String.format("(%s - %s)", getLeftName(), getRightName());
837        }
838        public String getPersistableString() {
839            return getFriendlyString();
840        }
841        public String toString() {
842            return String.format("[SubtractCombo@%x: leftName=%s, rightName=%s, friendlyString=%s, persistableString=%s]", 
843                hashCode(), getLeftName(), getRightName(), getFriendlyString(), getPersistableString());
844        }
845    }
846    private static class MultiplyCombination extends Combination {
847        public MultiplyCombination(final Object lhs, final Object rhs) throws VisADException, RemoteException {
848            super(lhs, rhs);
849            setOperationData(getLeftData().multiply(getRightData()));
850        }
851        public String getFriendlyString() {
852            return String.format("(%s * %s)", getLeftName(), getRightName());
853        }
854        public String getPersistableString() {
855            return getFriendlyString();
856        }
857        public String toString() {
858            return String.format("[MultiplyCombo@%x: leftName=%s, rightName=%s, friendlyString=%s, persistableString=%s]", 
859                hashCode(), getLeftName(), getRightName(), getFriendlyString(), getPersistableString());
860        }
861    }
862    private static class DivideCombination extends Combination {
863        public DivideCombination(final Object lhs, final Object rhs) throws VisADException, RemoteException {
864            super(lhs, rhs);
865            setOperationData(getLeftData().divide(getRightData()));
866        }
867        public String getFriendlyString() {
868            return String.format("(%s / %s)", getLeftName(), getRightName());
869        }
870        public String getPersistableString() {
871            return getFriendlyString();
872        }
873        public String toString() {
874            return String.format("[DivideCombo@%x: leftName=%s, rightName=%s, friendlyString=%s, persistableString=%s]", 
875                hashCode(), getLeftName(), getRightName(), getFriendlyString(), getPersistableString());
876        }
877    }
878    private static class ExponentCombination extends Combination {
879        public ExponentCombination(final Object lhs, final Object rhs) throws VisADException, RemoteException {
880            super(lhs, rhs);
881            setOperationData(getLeftData().pow(getRightData()));
882        }
883        public String getFriendlyString() {
884            return String.format("(%s**%s)", getLeftName(), getRightName());
885        }
886        public String getPersistableString() {
887            return getFriendlyString();
888        }
889        public String toString() {
890            return String.format("[ExponentCombo@%x: leftName=%s, rightName=%s, friendlyString=%s, persistableString=%s]", 
891                hashCode(), getLeftName(), getRightName(), getFriendlyString(), getPersistableString());
892        }
893    }
894    private static class ModuloCombination extends Combination {
895        public ModuloCombination(final Object lhs, final Object rhs) throws VisADException, RemoteException {
896            super(lhs, rhs);
897            setOperationData(getLeftData().remainder(getRightData()));
898        }
899        public String getFriendlyString() {
900            return String.format("(%s %% %s)", getLeftName(), getRightName());
901        }
902        public String getPersistableString() {
903            return getFriendlyString();
904        }
905        public String toString() {
906            return String.format("[ModuloCombo@%x: leftName=%s, rightName=%s, friendlyString=%s, persistableString=%s]", 
907                hashCode(), getLeftName(), getRightName(), getFriendlyString(), getPersistableString());
908        }
909    }
910    private static class NegateCombination extends Combination {
911        public NegateCombination(final Object lhs) throws VisADException, RemoteException {
912            super(lhs, null);
913            setOperationData(getLeftData().negate());
914        }
915        public String getFriendlyString() {
916            return String.format("(-%s)", getLeftName());
917        }
918        public String getPersistableString() {
919            return getFriendlyString();
920        }
921        public String toString() {
922            return String.format("[NegateCombo@%x: leftName=%s, friendlyString=%s, persistableString=%s]", 
923                hashCode(), getLeftName(), getFriendlyString(), getPersistableString());
924        }
925    }
926}