changeset 1362:d79e21855aef

Add mechanism for saving/loading colour maps by name/id rather than by numerical index, for future compatibility when adding to or changing the supported colour maps. Add two new colour maps (and one old one). Write out backward-compatible numerical indices for use when reloading in older versions. Also add a mechanism to invert the colour map, though I don't think it turns out useful enough to include in the UI.
author Chris Cannam
date Thu, 18 Oct 2018 13:21:56 +0100
parents 2e3b3fadba27
children bbeffb29bf09
files layer/Colour3DPlotLayer.cpp layer/Colour3DPlotLayer.h layer/Colour3DPlotRenderer.cpp layer/ColourMapper.cpp layer/ColourMapper.h layer/ColourScale.cpp layer/ColourScale.h layer/NoteLayer.h layer/RegionLayer.cpp layer/RegionLayer.h layer/SliceLayer.cpp layer/SliceLayer.h layer/SpectrogramLayer.cpp layer/SpectrogramLayer.h layer/SpectrumLayer.cpp layer/SpectrumLayer.h layer/TimeValueLayer.cpp layer/TimeValueLayer.h view/Pane.cpp view/PaneStack.cpp widgets/ColourMapComboBox.cpp
diffstat 21 files changed, 486 insertions(+), 100 deletions(-) [+]
line wrap: on
line diff
--- a/layer/Colour3DPlotLayer.cpp	Fri Oct 12 11:17:29 2018 +0100
+++ b/layer/Colour3DPlotLayer.cpp	Thu Oct 18 13:21:56 2018 +0100
@@ -47,6 +47,7 @@
     m_colourScale(ColourScaleType::Linear),
     m_colourScaleSet(false),
     m_colourMap(0),
+    m_colourInverted(false),
     m_gain(1.0),
     m_binScale(BinScale::Linear),
     m_normalization(ColumnNormalization::None),
@@ -394,7 +395,7 @@
                                     int value) const
 {
     if (name == "Colour") {
-        return ColourMapper::getColourMapName(value);
+        return ColourMapper::getColourMapLabel(value);
     }
     if (name == "Colour Scale") {
         switch (value) {
@@ -824,7 +825,9 @@
         return "";
     }
 
-    if (m_invertVertical) sy = m_model->getHeight() - sy - 1;
+    if (m_invertVertical) {
+        sy = m_model->getHeight() - sy - 1;
+    }
 
     float value = m_model->getValueAt(sx0, sy);
 
@@ -979,7 +982,9 @@
         if (i > symin) {
 
             int idx = i - 1;
-            if (m_invertVertical) idx = m_model->getHeight() - idx - 1;
+            if (m_invertVertical) {
+                idx = m_model->getHeight() - idx - 1;
+            }
 
             QString text = m_model->getBinName(idx);
             if (text == "") text = QString("[%1]").arg(idx + 1);
@@ -1009,6 +1014,7 @@
 
         ColourScale::Parameters cparams;
         cparams.colourMap = m_colourMap;
+        cparams.inverted = m_colourInverted;
         cparams.scaleType = m_colourScale;
         cparams.gain = m_gain;
 
@@ -1027,7 +1033,10 @@
         }
 
         SVDEBUG << "Colour3DPlotLayer: rebuilding renderer, value range is "
-                << minValue << " -> " << maxValue << endl;
+                << minValue << " -> " << maxValue
+                << " (model min = " << m_model->getMinimumLevel()
+                << ", max = " << m_model->getMaximumLevel() << ")"
+                << endl;
         
         if (maxValue <= minValue) {
             maxValue = minValue + 0.1f;
@@ -1172,13 +1181,11 @@
                          QString indent, QString extraAttributes) const
 {
     QString s = QString("scale=\"%1\" "
-                        "colourScheme=\"%2\" "
-                        "minY=\"%3\" "
-                        "maxY=\"%4\" "
-                        "invertVertical=\"%5\" "
-                        "opaque=\"%6\" %7")
+                        "minY=\"%2\" "
+                        "maxY=\"%3\" "
+                        "invertVertical=\"%4\" "
+                        "opaque=\"%5\" %6")
         .arg(convertFromColourScale(m_colourScale))
-        .arg(m_colourMap)
         .arg(m_miny)
         .arg(m_maxy)
         .arg(m_invertVertical ? "true" : "false")
@@ -1187,6 +1194,17 @@
              .arg(int(m_binScale))
              .arg(m_smooth ? "true" : "false")
              .arg(m_gain));
+
+    // New-style colour map attribute, by string id rather than by
+    // number
+
+    s += QString("colourMap=\"%1\" ")
+        .arg(ColourMapper::getColourMapId(m_colourMap));
+
+    // Old-style colour map attribute
+
+    s += QString("colourScheme=\"%1\" ")
+        .arg(ColourMapper::getBackwardCompatibilityColourMap(m_colourMap));
     
     // New-style normalization attributes, allowing for more types of
     // normalization in future: write out the column normalization
@@ -1219,8 +1237,16 @@
         (attributes.value("scale").toInt(&ok));
     if (ok) setColourScale(colourScale);
 
-    int colourMap = attributes.value("colourScheme").toInt(&ok);
-    if (ok) setColourMap(colourMap);
+    QString colourMapId = attributes.value("colourMap");
+    int colourMap = ColourMapper::getColourMapById(colourMapId);
+    if (colourMap >= 0) {
+        setColourMap(colourMap);
+    } else {
+        colourMap = attributes.value("colourScheme").toInt(&ok);
+        if (ok && colourMap < ColourMapper::getColourMapCount()) {
+            setColourMap(colourMap);
+        }
+    }
 
     BinScale binScale = (BinScale)
         attributes.value("binScale").toInt(&ok);
--- a/layer/Colour3DPlotLayer.h	Fri Oct 12 11:17:29 2018 +0100
+++ b/layer/Colour3DPlotLayer.h	Thu Oct 18 13:21:56 2018 +0100
@@ -160,6 +160,7 @@
     ColourScaleType m_colourScale;
     bool m_colourScaleSet;
     int m_colourMap;
+    bool m_colourInverted;
     float m_gain;
     BinScale m_binScale;
     ColumnNormalization m_normalization; // of individual columns
--- a/layer/Colour3DPlotRenderer.cpp	Fri Oct 12 11:17:29 2018 +0100
+++ b/layer/Colour3DPlotRenderer.cpp	Thu Oct 18 13:21:56 2018 +0100
@@ -35,7 +35,7 @@
 #include <utility>
 using namespace std::rel_ops;
 
-#define DEBUG_COLOUR_PLOT_REPAINT 1
+//#define DEBUG_COLOUR_PLOT_REPAINT 1
 
 using namespace std;
 
--- a/layer/ColourMapper.cpp	Fri Oct 12 11:17:29 2018 +0100
+++ b/layer/ColourMapper.cpp	Thu Oct 18 13:21:56 2018 +0100
@@ -27,11 +27,14 @@
 
 using namespace std;
 
-static vector<QColor> convertStrings(const vector<QString> &strs)
+static vector<QColor> convertStrings(const vector<QString> &strs,
+                                     bool reversed)
 {
     vector<QColor> converted;
     for (const auto &s: strs) converted.push_back(QColor(s));
-    reverse(converted.begin(), converted.end());
+    if (reversed) {
+        reverse(converted.begin(), converted.end());
+    }
     return converted;
 }
 
@@ -39,13 +42,70 @@
         // Based on ColorBrewer ylGnBu
         "#ffffff", "#ffff00", "#f7fcf0", "#e0f3db", "#ccebc5", "#a8ddb5",
         "#7bccc4", "#4eb3d3", "#2b8cbe", "#0868ac", "#084081", "#042040"
-        });
+    },
+    true);
 
 static vector<QColor> cherry = convertStrings({
         "#f7f7f7", "#fddbc7", "#f4a582", "#d6604d", "#b2182b", "#dd3497",
         "#ae017e", "#7a0177", "#49006a"
-        });
-    
+    },
+    true);
+
+static vector<QColor> magma = convertStrings({
+        "#FCFFB2", "#FCDF96", "#FBC17D", "#FBA368", "#FA8657", "#F66B4D",
+        "#ED504A", "#E03B50", "#C92D59", "#B02363", "#981D69", "#81176D",
+        "#6B116F", "#57096E", "#43006A", "#300060", "#1E0848", "#110B2D",
+        "#080616", "#000005"
+    },
+    true);
+
+static vector<QColor> cividis = convertStrings({
+        "#00204c", "#00204e", "#002150", "#002251", "#002353", "#002355",
+        "#002456", "#002558", "#00265a", "#00265b", "#00275d", "#00285f",
+        "#002861", "#002963", "#002a64", "#002a66", "#002b68", "#002c6a",
+        "#002d6c", "#002d6d", "#002e6e", "#002e6f", "#002f6f", "#002f6f",
+        "#00306f", "#00316f", "#00316f", "#00326e", "#00336e", "#00346e",
+        "#00346e", "#01356e", "#06366e", "#0a376d", "#0e376d", "#12386d",
+        "#15396d", "#17396d", "#1a3a6c", "#1c3b6c", "#1e3c6c", "#203c6c",
+        "#223d6c", "#243e6c", "#263e6c", "#273f6c", "#29406b", "#2b416b",
+        "#2c416b", "#2e426b", "#2f436b", "#31446b", "#32446b", "#33456b",
+        "#35466b", "#36466b", "#37476b", "#38486b", "#3a496b", "#3b496b",
+        "#3c4a6b", "#3d4b6b", "#3e4b6b", "#404c6b", "#414d6b", "#424e6b",
+        "#434e6b", "#444f6b", "#45506b", "#46506b", "#47516b", "#48526b",
+        "#49536b", "#4a536b", "#4b546b", "#4c556b", "#4d556b", "#4e566b",
+        "#4f576c", "#50586c", "#51586c", "#52596c", "#535a6c", "#545a6c",
+        "#555b6c", "#565c6c", "#575d6d", "#585d6d", "#595e6d", "#5a5f6d",
+        "#5b5f6d", "#5c606d", "#5d616e", "#5e626e", "#5f626e", "#5f636e",
+        "#60646e", "#61656f", "#62656f", "#63666f", "#64676f", "#65676f",
+        "#666870", "#676970", "#686a70", "#686a70", "#696b71", "#6a6c71",
+        "#6b6d71", "#6c6d72", "#6d6e72", "#6e6f72", "#6f6f72", "#6f7073",
+        "#707173", "#717273", "#727274", "#737374", "#747475", "#757575",
+        "#757575", "#767676", "#777776", "#787876", "#797877", "#7a7977",
+        "#7b7a77", "#7b7b78", "#7c7b78", "#7d7c78", "#7e7d78", "#7f7e78",
+        "#807e78", "#817f78", "#828078", "#838178", "#848178", "#858278",
+        "#868378", "#878478", "#888578", "#898578", "#8a8678", "#8b8778",
+        "#8c8878", "#8d8878", "#8e8978", "#8f8a78", "#908b78", "#918c78",
+        "#928c78", "#938d78", "#948e78", "#958f78", "#968f77", "#979077",
+        "#989177", "#999277", "#9a9377", "#9b9377", "#9c9477", "#9d9577",
+        "#9e9676", "#9f9776", "#a09876", "#a19876", "#a29976", "#a39a75",
+        "#a49b75", "#a59c75", "#a69c75", "#a79d75", "#a89e74", "#a99f74",
+        "#aaa074", "#aba174", "#aca173", "#ada273", "#aea373", "#afa473",
+        "#b0a572", "#b1a672", "#b2a672", "#b4a771", "#b5a871", "#b6a971",
+        "#b7aa70", "#b8ab70", "#b9ab70", "#baac6f", "#bbad6f", "#bcae6e",
+        "#bdaf6e", "#beb06e", "#bfb16d", "#c0b16d", "#c1b26c", "#c2b36c",
+        "#c3b46c", "#c5b56b", "#c6b66b", "#c7b76a", "#c8b86a", "#c9b869",
+        "#cab969", "#cbba68", "#ccbb68", "#cdbc67", "#cebd67", "#d0be66",
+        "#d1bf66", "#d2c065", "#d3c065", "#d4c164", "#d5c263", "#d6c363",
+        "#d7c462", "#d8c561", "#d9c661", "#dbc760", "#dcc860", "#ddc95f",
+        "#deca5e", "#dfcb5d", "#e0cb5d", "#e1cc5c", "#e3cd5b", "#e4ce5b",
+        "#e5cf5a", "#e6d059", "#e7d158", "#e8d257", "#e9d356", "#ebd456",
+        "#ecd555", "#edd654", "#eed753", "#efd852", "#f0d951", "#f1da50",
+        "#f3db4f", "#f4dc4e", "#f5dd4d", "#f6de4c", "#f7df4b", "#f9e049",
+        "#fae048", "#fbe147", "#fce246", "#fde345", "#ffe443", "#ffe542",
+        "#ffe642", "#ffe743", "#ffe844", "#ffe945"
+    },
+    false);
+
 static void
 mapDiscrete(double norm, vector<QColor> &colours, double &r, double &g, double &b)
 {
@@ -61,8 +121,9 @@
     b = c0.blueF() * prop0 + c1.blueF() * prop1;
 }
 
-ColourMapper::ColourMapper(int map, double min, double max) :
+ColourMapper::ColourMapper(int map, bool inverted, double min, double max) :
     m_map(map),
+    m_inverted(inverted),
     m_min(min),
     m_max(max)
 {
@@ -80,14 +141,16 @@
 int
 ColourMapper::getColourMapCount()
 {
-    return 12;
+    return 15;
 }
 
 QString
-ColourMapper::getColourMapName(int n)
+ColourMapper::getColourMapLabel(int n)
 {
+    // When adding a map, be sure to also update getColourMapCount()
+    
     if (n >= getColourMapCount()) return QObject::tr("<unknown>");
-    StandardMap map = (StandardMap)n;
+    ColourMap map = (ColourMap)n;
 
     switch (map) {
     case Green:            return QObject::tr("Green");
@@ -102,17 +165,112 @@
     case Highlight:        return QObject::tr("Highlight");
     case Printer:          return QObject::tr("Printer");
     case HighGain:         return QObject::tr("High Gain");
+    case BlueOnBlack:      return QObject::tr("Blue on Black");
+    case Cividis:          return QObject::tr("Cividis");
+    case Magma:            return QObject::tr("Magma");
     }
 
     return QObject::tr("<unknown>");
 }
 
+QString
+ColourMapper::getColourMapId(int n)
+{
+    if (n >= getColourMapCount()) return "<unknown>";
+    ColourMap map = (ColourMap)n;
+
+    switch (map) {
+    case Green:            return "Green";
+    case WhiteOnBlack:     return "White on Black";
+    case BlackOnWhite:     return "Black on White";
+    case Cherry:           return "Cherry";
+    case Wasp:             return "Wasp";
+    case Ice:              return "Ice";
+    case Sunset:           return "Sunset";
+    case FruitSalad:       return "Fruit Salad";
+    case Banded:           return "Banded";
+    case Highlight:        return "Highlight";
+    case Printer:          return "Printer";
+    case HighGain:         return "High Gain";
+    case BlueOnBlack:      return "Blue on Black";
+    case Cividis:          return "Cividis";
+    case Magma:            return "Magma";
+    }
+
+    return "<unknown>";
+}
+
+int
+ColourMapper::getColourMapById(QString id)
+{
+    ColourMap map = (ColourMap)getColourMapCount();
+
+    if      (id == "Green")            { map = Green; }
+    else if (id == "White on Black")   { map = WhiteOnBlack; }
+    else if (id == "Black on White")   { map = BlackOnWhite; }
+    else if (id == "Cherry")           { map = Cherry; }
+    else if (id == "Wasp")             { map = Wasp; }
+    else if (id == "Ice")              { map = Ice; }
+    else if (id == "Sunset")           { map = Sunset; }
+    else if (id == "Fruit Salad")      { map = FruitSalad; }
+    else if (id == "Banded")           { map = Banded; }
+    else if (id == "Highlight")        { map = Highlight; }
+    else if (id == "Printer")          { map = Printer; }
+    else if (id == "High Gain")        { map = HighGain; }
+    else if (id == "Blue on Black")    { map = BlueOnBlack; }
+    else if (id == "Cividis")          { map = Cividis; }
+    else if (id == "Magma")            { map = Magma; }
+
+    if (map == (ColourMap)getColourMapCount()) {
+        return -1;
+    } else {
+        return int(map);
+    }
+}
+
+int
+ColourMapper::getBackwardCompatibilityColourMap(int n)
+{
+    /* Returned value should be an index into the series
+     * (Default/Green, Sunset, WhiteOnBlack, BlackOnWhite, RedOnBlue,
+     * YellowOnBlack, BlueOnBlack, FruitSalad, Banded, Highlight,
+     * Printer, HighGain). Minimum 0, maximum 11.
+     */
+        
+    if (n >= getColourMapCount()) return 0;
+    ColourMap map = (ColourMap)n;
+
+    switch (map) {
+    case Green:            return 0;
+    case WhiteOnBlack:     return 2;
+    case BlackOnWhite:     return 3;
+    case Cherry:           return 4;
+    case Wasp:             return 5;
+    case Ice:              return 6;
+    case Sunset:           return 1;
+    case FruitSalad:       return 7;
+    case Banded:           return 8;
+    case Highlight:        return 9;
+    case Printer:          return 10;
+    case HighGain:         return 11;
+    case BlueOnBlack:      return 6;
+    case Cividis:          return 6;
+    case Magma:            return 1;
+    }
+
+    return 0;
+}
+
 QColor
 ColourMapper::map(double value) const
 {
     double norm = (value - m_min) / (m_max - m_min);
     if (norm < 0.0) norm = 0.0;
     if (norm > 1.0) norm = 1.0;
+
+    if (m_inverted) {
+        norm = 1.0 - norm;
+    }
     
     double h = 0.0, s = 0.0, v = 0.0, r = 0.0, g = 0.0, b = 0.0;
     bool hsv = true;
@@ -120,7 +278,7 @@
     double blue = 0.6666, pieslice = 0.3333;
 
     if (m_map >= getColourMapCount()) return Qt::black;
-    StandardMap map = (StandardMap)m_map;
+    ColourMap map = (ColourMap)m_map;
 
     switch (map) {
 
@@ -150,6 +308,18 @@
         s = 1.0;
         v = norm;
         break;
+        
+    case BlueOnBlack:
+        h = blue;
+        s = 1.0;
+        v = norm * 2.0;
+        if (v > 1.0) {
+            v = 1.0;
+            s = 1.0 - (sqrt(norm) - 0.707) * 3.413;
+            if (s < 0.0) s = 0.0;
+            if (s > 1.0) s = 1.0;
+        }
+        break;
 
     case Sunset:
         r = (norm - 0.24) * 2.38;
@@ -237,6 +407,17 @@
     case Ice:
         hsv = false;
         mapDiscrete(norm, ice, r, g, b);
+        break;
+
+    case Cividis:
+        hsv = false;
+        mapDiscrete(norm, cividis, r, g, b);
+        break;
+
+    case Magma:
+        hsv = false;
+        mapDiscrete(norm, magma, r, g, b);
+        break;
     }
 
     if (hsv) {
@@ -250,7 +431,7 @@
 ColourMapper::getContrastingColour() const
 {
     if (m_map >= getColourMapCount()) return Qt::white;
-    StandardMap map = (StandardMap)m_map;
+    ColourMap map = (ColourMap)m_map;
 
     switch (map) {
 
@@ -289,6 +470,15 @@
 
     case HighGain:
         return Qt::red;
+
+    case BlueOnBlack:
+        return Qt::red;
+
+    case Cividis:
+        return Qt::white;
+
+    case Magma:
+        return Qt::white;
     }
 
     return Qt::white;
@@ -298,7 +488,7 @@
 ColourMapper::hasLightBackground() const
 {
     if (m_map >= getColourMapCount()) return false;
-    StandardMap map = (StandardMap)m_map;
+    ColourMap map = (ColourMap)m_map;
 
     switch (map) {
 
@@ -316,6 +506,9 @@
     case FruitSalad:
     case Banded:
     case Highlight:
+    case BlueOnBlack:
+    case Cividis:
+    case Magma:
         
     default:
         return false;
--- a/layer/ColourMapper.h	Fri Oct 12 11:17:29 2018 +0100
+++ b/layer/ColourMapper.h	Thu Oct 18 13:21:56 2018 +0100
@@ -27,13 +27,16 @@
 class ColourMapper
 {
 public:
-    ColourMapper(int map, double minValue, double maxValue);
+    ColourMapper(int map,
+                 bool inverted,
+                 double minValue,
+                 double maxValue);
     ~ColourMapper();
 
-    ColourMapper(const ColourMapper &) = default;
-    ColourMapper &operator=(const ColourMapper &) = default;
+    ColourMapper(const ColourMapper &) =default;
+    ColourMapper &operator=(const ColourMapper &) =default;
 
-    enum StandardMap {
+    enum ColourMap {
         Green,
         Sunset,
         WhiteOnBlack,
@@ -45,25 +48,86 @@
         Banded,
         Highlight,
         Printer,
-        HighGain
+        HighGain,
+        BlueOnBlack,
+        Cividis,
+        Magma
     };
 
     int getMap() const { return m_map; }
+    bool isInverted() const { return m_inverted; }
     double getMinValue() const { return m_min; }
     double getMaxValue() const { return m_max; }
 
+    /**
+     * Return the number of known colour maps.
+     */
     static int getColourMapCount();
-    static QString getColourMapName(int n);
 
+    /**
+     * Return a human-readable label for the colour map with the given
+     * index. This may have been subject to translation.
+     */
+    static QString getColourMapLabel(int n);
+
+    /**
+     * Return a machine-readable id string for the colour map with the
+     * given index. This is not translated and is intended for use in
+     * file I/O.
+     */
+    static QString getColourMapId(int n);
+
+    /**
+     * Return the index for the colour map with the given
+     * machine-readable id string, or -1 if the id is not recognised.
+     */
+    static int getColourMapById(QString id);
+
+    /**
+     * Older versions of colour-handling code save and reload colour
+     * maps by numerical index and can't properly handle situations in
+     * which the index order changes between releases, or new indices
+     * are added. So when we save a colour map by id, we should also
+     * save a compatibility value that can be re-read by such
+     * code. This value is an index into the series of colours used by
+     * pre-3.2 SV code, namely (Default/Green, Sunset, WhiteOnBlack,
+     * BlackOnWhite, RedOnBlue, YellowOnBlack, BlueOnBlack,
+     * FruitSalad, Banded, Highlight, Printer, HighGain). It should
+     * represent the closest equivalent to the current colour scheme
+     * available in that set. This function returns that index.
+     */    
+    static int getBackwardCompatibilityColourMap(int n);
+    
+    /**
+     * Map the given value to a colour. The value will be clamped to
+     * the range minValue to maxValue (where both are drawn from the
+     * constructor arguments).
+     */
     QColor map(double value) const;
 
-    QColor getContrastingColour() const; // for cursors etc
+    /**
+     * Return a colour that contrasts somewhat with the colours in the
+     * map, so as to be used for cursors etc.
+     */
+    QColor getContrastingColour() const;
+
+    /**
+     * Return true if the colour map is intended to be placed over a
+     * light background, false otherwise. This is typically true if
+     * the colours corresponding to higher values are darker than
+     * those corresponding to lower values.
+     */
     bool hasLightBackground() const;
 
+    /**
+     * Return a pixmap of the given size containing a preview swatch
+     * for the colour map.
+     */
     QPixmap getExamplePixmap(QSize size) const;
     
 protected:
     int m_map;
+    bool m_inverted;
     double m_min;
     double m_max;
 };
--- a/layer/ColourScale.cpp	Fri Oct 12 11:17:29 2018 +0100
+++ b/layer/ColourScale.cpp	Thu Oct 18 13:21:56 2018 +0100
@@ -27,7 +27,7 @@
 
 ColourScale::ColourScale(Parameters parameters) :
     m_params(parameters),
-    m_mapper(m_params.colourMap, 1.f, double(m_maxPixel))
+    m_mapper(m_params.colourMap, m_params.inverted, 1.f, double(m_maxPixel))
 {
     if (m_params.minValue >= m_params.maxValue) {
         SVCERR << "ERROR: ColourScale::ColourScale: minValue = "
--- a/layer/ColourScale.h	Fri Oct 12 11:17:29 2018 +0100
+++ b/layer/ColourScale.h	Thu Oct 18 13:21:56 2018 +0100
@@ -36,7 +36,7 @@
 public:
     struct Parameters {
         Parameters() : colourMap(0), scaleType(ColourScaleType::Linear),
-                       minValue(0.0), maxValue(1.0),
+                       minValue(0.0), maxValue(1.0), inverted(false),
                        threshold(0.0), gain(1.0), multiple(1.0) { }
 
         /** A colour map index as used by ColourMapper */
@@ -51,6 +51,9 @@
         /** Maximum value in source range. Must be > minValue */
         double maxValue;
 
+        /** Whether the colour scale should be mapped inverted */
+        bool inverted;
+
         /** Threshold below which every value is mapped to background
             pixel 0 */
         double threshold;
--- a/layer/NoteLayer.h	Fri Oct 12 11:17:29 2018 +0100
+++ b/layer/NoteLayer.h	Thu Oct 18 13:21:56 2018 +0100
@@ -13,8 +13,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _NOTE_LAYER_H_
-#define _NOTE_LAYER_H_
+#ifndef SV_NOTE_LAYER_H
+#define SV_NOTE_LAYER_H
 
 #include "SingleColourLayer.h"
 #include "VerticalScaleLayer.h"
--- a/layer/RegionLayer.cpp	Fri Oct 12 11:17:29 2018 +0100
+++ b/layer/RegionLayer.cpp	Thu Oct 18 13:21:56 2018 +0100
@@ -57,6 +57,7 @@
     m_editingCommand(0),
     m_verticalScale(EqualSpaced),
     m_colourMap(0),
+    m_colourInverted(false),
     m_plotStyle(PlotLines)
 {
     
@@ -174,7 +175,7 @@
                                    int value) const
 {
     if (name == "Colour" && m_plotStyle == PlotSegmentation) {
-        return ColourMapper::getColourMapName(value);
+        return ColourMapper::getColourMapLabel(value);
     } else if (name == "Plot Type") {
 
         switch (value) {
@@ -854,7 +855,7 @@
 //    SVDEBUG << "RegionLayer::getColourForValue: min " << min << ", max "
 //              << max << ", log " << log << ", value " << val << endl;
 
-    QColor solid = ColourMapper(m_colourMap, min, max).map(val);
+    QColor solid = ColourMapper(m_colourMap, m_colourInverted, min, max).map(val);
     return QColor(solid.red(), solid.green(), solid.blue(), 120);
 }
 
@@ -1550,10 +1551,25 @@
 RegionLayer::toXml(QTextStream &stream,
                  QString indent, QString extraAttributes) const
 {
-    SingleColourLayer::toXml(stream, indent, extraAttributes +
-                             QString(" verticalScale=\"%1\" plotStyle=\"%2\"")
-                             .arg(m_verticalScale)
-                             .arg(m_plotStyle));
+    QString s;
+
+    s += QString("verticalScale=\"%1\" "
+                 "plotStyle=\"%2\" ")
+        .arg(m_verticalScale)
+        .arg(m_plotStyle);
+
+    // New-style colour map attribute, by string id rather than by
+    // number
+
+    s += QString("fillColourMap=\"%1\" ")
+        .arg(ColourMapper::getColourMapId(m_colourMap));
+    
+    // Old-style colour map attribute
+
+    s += QString("colourMap=\"%1\" ")
+        .arg(ColourMapper::getBackwardCompatibilityColourMap(m_colourMap));
+    
+    SingleColourLayer::toXml(stream, indent, extraAttributes + " " + s);
 }
 
 void
@@ -1568,6 +1584,17 @@
     PlotStyle style = (PlotStyle)
         attributes.value("plotStyle").toInt(&ok);
     if (ok) setPlotStyle(style);
+    
+    QString colourMapId = attributes.value("fillColourMap");
+    int colourMap = ColourMapper::getColourMapById(colourMapId);
+    if (colourMap >= 0) {
+        setFillColourMap(colourMap);
+    } else {
+        colourMap = attributes.value("colourMap").toInt(&ok);
+        if (ok && colourMap < ColourMapper::getColourMapCount()) {
+            setFillColourMap(colourMap);
+        }
+    }
 }
 
 
--- a/layer/RegionLayer.h	Fri Oct 12 11:17:29 2018 +0100
+++ b/layer/RegionLayer.h	Thu Oct 18 13:21:56 2018 +0100
@@ -13,8 +13,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _REGION_LAYER_H_
-#define _REGION_LAYER_H_
+#ifndef SV_REGION_LAYER_H
+#define SV_REGION_LAYER_H
 
 #include "SingleColourLayer.h"
 #include "VerticalScaleLayer.h"
@@ -156,6 +156,7 @@
     RegionModel::EditCommand *m_editingCommand;
     VerticalScale m_verticalScale;
     int m_colourMap;
+    bool m_colourInverted;
     PlotStyle m_plotStyle;
 
     typedef std::map<double, int> SpacingMap;
--- a/layer/SliceLayer.cpp	Fri Oct 12 11:17:29 2018 +0100
+++ b/layer/SliceLayer.cpp	Thu Oct 18 13:21:56 2018 +0100
@@ -32,6 +32,7 @@
 SliceLayer::SliceLayer() :
     m_sliceableModel(0),
     m_colourMap(int(ColourMapper::Ice)),
+    m_colourInverted(false),
     m_energyScale(dBScale),
     m_samplingMode(SampleMean),
     m_plotStyle(PlotLines),
@@ -469,7 +470,7 @@
 
     double nx = getXForBin(v, bin0);
 
-    ColourMapper mapper(m_colourMap, 0, 1);
+    ColourMapper mapper(m_colourMap, m_colourInverted, 0, 1);
 
     for (int bin = 0; bin < mh; ++bin) {
 
@@ -580,7 +581,7 @@
 SliceLayer::hasLightBackground() const
 {
     if (usesSolidColour()) {
-        ColourMapper mapper(m_colourMap, 0, 1);
+        ColourMapper mapper(m_colourMap, m_colourInverted, 0, 1);
         return mapper.hasLightBackground();
     } else {
         return SingleColourLayer::hasLightBackground();
@@ -742,7 +743,7 @@
                                   int value) const
 {
     if (name == "Colour" && usesSolidColour()) {
-        return ColourMapper::getColourMapName(value);
+        return ColourMapper::getColourMapLabel(value);
     }
     if (name == "Scale") {
         switch (value) {
@@ -925,15 +926,13 @@
 {
     QString s;
     
-    s += QString("colourScheme=\"%1\" "
-                 "energyScale=\"%2\" "
-                 "samplingMode=\"%3\" "
-                 "plotStyle=\"%4\" "
-                 "binScale=\"%5\" "
-                 "gain=\"%6\" "
-                 "threshold=\"%7\" "
-                 "normalize=\"%8\" %9")
-        .arg(m_colourMap)
+    s += QString("energyScale=\"%1\" "
+                 "samplingMode=\"%2\" "
+                 "plotStyle=\"%3\" "
+                 "binScale=\"%4\" "
+                 "gain=\"%5\" "
+                 "threshold=\"%6\" "
+                 "normalize=\"%7\" %8 ")
         .arg(m_energyScale)
         .arg(m_samplingMode)
         .arg(m_plotStyle)
@@ -946,6 +945,17 @@
              .arg(m_minbin)
              .arg(m_maxbin));
 
+    // New-style colour map attribute, by string id rather than by
+    // number
+
+    s += QString("fillColourMap=\"%1\" ")
+        .arg(ColourMapper::getColourMapId(m_colourMap));
+
+    // Old-style colour map attribute
+
+    s += QString("colourScheme=\"%1\" ")
+        .arg(ColourMapper::getBackwardCompatibilityColourMap(m_colourMap));
+    
     SingleColourLayer::toXml(stream, indent, extraAttributes + " " + s);
 }
 
@@ -964,8 +974,16 @@
         attributes.value("samplingMode").toInt(&ok);
     if (ok) setSamplingMode(mode);
 
-    int colourMap = attributes.value("colourScheme").toInt(&ok);
-    if (ok) setFillColourMap(colourMap);
+    QString colourMapId = attributes.value("fillColourMap");
+    int colourMap = ColourMapper::getColourMapById(colourMapId);
+    if (colourMap >= 0) {
+        setFillColourMap(colourMap);
+    } else {
+        colourMap = attributes.value("colourScheme").toInt(&ok);
+        if (ok && colourMap < ColourMapper::getColourMapCount()) {
+            setFillColourMap(colourMap);
+        }
+    }
 
     PlotStyle s = (PlotStyle)
         attributes.value("plotStyle").toInt(&ok);
--- a/layer/SliceLayer.h	Fri Oct 12 11:17:29 2018 +0100
+++ b/layer/SliceLayer.h	Thu Oct 18 13:21:56 2018 +0100
@@ -142,6 +142,7 @@
 
     const DenseThreeDimensionalModel *m_sliceableModel;
     int                               m_colourMap;
+    bool                              m_colourInverted;
     EnergyScale                       m_energyScale;
     SamplingMode                      m_samplingMode;
     PlotStyle                         m_plotStyle;
--- a/layer/SpectrogramLayer.cpp	Fri Oct 12 11:17:29 2018 +0100
+++ b/layer/SpectrogramLayer.cpp	Thu Oct 18 13:21:56 2018 +0100
@@ -73,6 +73,7 @@
     m_colourScale(ColourScaleType::Log),
     m_colourScaleMultiple(1.0),
     m_colourMap(0),
+    m_colourInverted(false),
     m_binScale(BinScale::Linear),
     m_binDisplay(BinDisplay::AllBins),
     m_normalization(ColumnNormalization::None),
@@ -448,7 +449,7 @@
                                         int value) const
 {
     if (name == "Colour") {
-        return ColourMapper::getColourMapName(value);
+        return ColourMapper::getColourMapLabel(value);
     }
     if (name == "Colour Scale") {
         switch (value) {
@@ -1081,7 +1082,8 @@
 bool
 SpectrogramLayer::hasLightBackground() const 
 {
-    return ColourMapper(m_colourMap, 1.f, 255.f).hasLightBackground();
+    return ColourMapper(m_colourMap, m_colourInverted, 1.f, 255.f)
+        .hasLightBackground();
 }
 
 double
@@ -1530,7 +1532,8 @@
         m_renderers[viewId] = new Colour3DPlotRenderer(sources, params);
 
         m_crosshairColour =
-            ColourMapper(m_colourMap, 1.f, 255.f).getContrastingColour();
+            ColourMapper(m_colourMap, m_colourInverted, 1.f, 255.f)
+            .getContrastingColour();
     }
 
     return m_renderers[viewId];
@@ -2508,18 +2511,27 @@
     s += QString("minFrequency=\"%1\" "
                  "maxFrequency=\"%2\" "
                  "colourScale=\"%3\" "
-                 "colourScheme=\"%4\" "
-                 "colourRotation=\"%5\" "
-                 "frequencyScale=\"%6\" "
-                 "binDisplay=\"%7\" ")
+                 "colourRotation=\"%4\" "
+                 "frequencyScale=\"%5\" "
+                 "binDisplay=\"%6\" ")
         .arg(m_minFrequency)
         .arg(m_maxFrequency)
         .arg(convertFromColourScale(m_colourScale, m_colourScaleMultiple))
-        .arg(m_colourMap)
         .arg(m_colourRotation)
         .arg(int(m_binScale))
         .arg(int(m_binDisplay));
 
+    // New-style colour map attribute, by string id rather than by
+    // number
+
+    s += QString("colourMap=\"%1\" ")
+        .arg(ColourMapper::getColourMapId(m_colourMap));
+
+    // Old-style colour map attribute
+
+    s += QString("colourScheme=\"%1\" ")
+        .arg(ColourMapper::getBackwardCompatibilityColourMap(m_colourMap));
+    
     // New-style normalization attributes, allowing for more types of
     // normalization in future: write out the column normalization
     // type separately, and then whether we are normalizing visible
@@ -2596,8 +2608,16 @@
         setColourScaleMultiple(colourScale.second);
     }
 
-    int colourMap = attributes.value("colourScheme").toInt(&ok);
-    if (ok) setColourMap(colourMap);
+    QString colourMapId = attributes.value("colourMap");
+    int colourMap = ColourMapper::getColourMapById(colourMapId);
+    if (colourMap >= 0) {
+        setColourMap(colourMap);
+    } else {
+        colourMap = attributes.value("colourScheme").toInt(&ok);
+        if (ok && colourMap < ColourMapper::getColourMapCount()) {
+            setColourMap(colourMap);
+        }
+    }
 
     int colourRotation = attributes.value("colourRotation").toInt(&ok);
     if (ok) setColourRotation(colourRotation);
--- a/layer/SpectrogramLayer.h	Fri Oct 12 11:17:29 2018 +0100
+++ b/layer/SpectrogramLayer.h	Thu Oct 18 13:21:56 2018 +0100
@@ -258,6 +258,7 @@
     ColourScaleType     m_colourScale;
     double              m_colourScaleMultiple;
     int                 m_colourMap;
+    bool                m_colourInverted;
     mutable QColor      m_crosshairColour;
     BinScale            m_binScale;
     BinDisplay          m_binDisplay;
--- a/layer/SpectrumLayer.cpp	Fri Oct 12 11:17:29 2018 +0100
+++ b/layer/SpectrumLayer.cpp	Thu Oct 18 13:21:56 2018 +0100
@@ -418,7 +418,7 @@
         paint.setFont(fn);
     }
 
-    ColourMapper mapper(m_colourMap, 0, 1);
+    ColourMapper mapper(m_colourMap, m_colourInverted, 0, 1);
     paint.setPen(mapper.getContrastingColour());
 
     int xorigin = m_xorigins[v->getId()];
@@ -618,8 +618,8 @@
 
         ColourMapper mapper =
             hasLightBackground() ?
-            ColourMapper(ColourMapper::BlackOnWhite, 0, 1) :
-            ColourMapper(ColourMapper::WhiteOnBlack, 0, 1);
+            ColourMapper(ColourMapper::BlackOnWhite, m_colourInverted, 0, 1) :
+            ColourMapper(ColourMapper::WhiteOnBlack, m_colourInverted, 0, 1);
         
         int peakminbin = 0;
         int peakmaxbin = fft->getHeight() - 1;
--- a/layer/SpectrumLayer.h	Fri Oct 12 11:17:29 2018 +0100
+++ b/layer/SpectrumLayer.h	Thu Oct 18 13:21:56 2018 +0100
@@ -111,9 +111,9 @@
     DenseTimeValueModel    *m_originModel;
     int                     m_channel;
     bool                    m_channelSet;
-    int                  m_windowSize;
+    int                     m_windowSize;
     WindowType              m_windowType;
-    int                  m_windowHopLevel;
+    int                     m_windowHopLevel;
     bool                    m_showPeaks;
     mutable bool            m_newFFTNeeded;
 
--- a/layer/TimeValueLayer.cpp	Fri Oct 12 11:17:29 2018 +0100
+++ b/layer/TimeValueLayer.cpp	Thu Oct 18 13:21:56 2018 +0100
@@ -60,6 +60,7 @@
     m_editingPoint(0, 0.0, tr("New Point")),
     m_editingCommand(0),
     m_colourMap(0),
+    m_colourInverted(false),
     m_plotStyle(PlotConnectedPoints),
     m_verticalScale(AutoAlignScale),
     m_drawSegmentDivisions(true),
@@ -223,7 +224,7 @@
                                     int value) const
 {
     if (name == "Colour" && m_plotStyle == PlotSegmentation) {
-        return ColourMapper::getColourMapName(value);
+        return ColourMapper::getColourMapLabel(value);
     } else if (name == "Plot Type") {
         switch (value) {
         default:
@@ -896,7 +897,7 @@
               << max << ", log " << log << ", value " << val << endl;
 #endif
 
-    QColor solid = ColourMapper(m_colourMap, min, max).map(val);
+    QColor solid = ColourMapper(m_colourMap, m_colourInverted, min, max).map(val);
     return QColor(solid.red(), solid.green(), solid.blue(), 120);
 }
 
@@ -1907,16 +1908,33 @@
 TimeValueLayer::toXml(QTextStream &stream,
                       QString indent, QString extraAttributes) const
 {
-    SingleColourLayer::toXml(stream, indent,
-                             extraAttributes +
-                             QString(" colourMap=\"%1\" plotStyle=\"%2\" verticalScale=\"%3\" scaleMinimum=\"%4\" scaleMaximum=\"%5\" drawDivisions=\"%6\" derivative=\"%7\" ")
-                             .arg(m_colourMap)
-                             .arg(m_plotStyle)
-                             .arg(m_verticalScale)
-                             .arg(m_scaleMinimum)
-                             .arg(m_scaleMaximum)
-                             .arg(m_drawSegmentDivisions ? "true" : "false")
-                             .arg(m_derivative ? "true" : "false"));
+    QString s;
+
+    s += QString("plotStyle=\"%1\" "
+                 "verticalScale=\"%2\" "
+                 "scaleMinimum=\"%3\" "
+                 "scaleMaximum=\"%4\" "
+                 "drawDivisions=\"%5\" "
+                 "derivative=\"%6\" ")
+        .arg(m_plotStyle)
+        .arg(m_verticalScale)
+        .arg(m_scaleMinimum)
+        .arg(m_scaleMaximum)
+        .arg(m_drawSegmentDivisions ? "true" : "false")
+        .arg(m_derivative ? "true" : "false");
+    
+    // New-style colour map attribute, by string id rather than by
+    // number
+
+    s += QString("fillColourMap=\"%1\" ")
+        .arg(ColourMapper::getColourMapId(m_colourMap));
+
+    // Old-style colour map attribute
+
+    s += QString("colourMap=\"%1\" ")
+        .arg(ColourMapper::getBackwardCompatibilityColourMap(m_colourMap));
+    
+    SingleColourLayer::toXml(stream, indent, extraAttributes + " " + s);
 }
 
 void
@@ -1926,8 +1944,16 @@
 
     bool ok, alsoOk;
 
-    int cmap = attributes.value("colourMap").toInt(&ok);
-    if (ok) setFillColourMap(cmap);
+    QString colourMapId = attributes.value("fillColourMap");
+    int colourMap = ColourMapper::getColourMapById(colourMapId);
+    if (colourMap >= 0) {
+        setFillColourMap(colourMap);
+    } else {
+        colourMap = attributes.value("colourMap").toInt(&ok);
+        if (ok && colourMap < ColourMapper::getColourMapCount()) {
+            setFillColourMap(colourMap);
+        }
+    }
 
     PlotStyle style = (PlotStyle)
         attributes.value("plotStyle").toInt(&ok);
--- a/layer/TimeValueLayer.h	Fri Oct 12 11:17:29 2018 +0100
+++ b/layer/TimeValueLayer.h	Thu Oct 18 13:21:56 2018 +0100
@@ -184,6 +184,7 @@
     SparseTimeValueModel::Point m_editingPoint;
     SparseTimeValueModel::EditCommand *m_editingCommand;
     int m_colourMap;
+    bool m_colourInverted;
     PlotStyle m_plotStyle;
     VerticalScale m_verticalScale;
     bool m_drawSegmentDivisions;
--- a/view/Pane.cpp	Fri Oct 12 11:17:29 2018 +0100
+++ b/view/Pane.cpp	Thu Oct 18 13:21:56 2018 +0100
@@ -111,7 +111,9 @@
     if (!isVisible()) return;
 
     Layer *layer = 0;
-    if (getLayerCount() > 0) layer = getLayer(getLayerCount() - 1);
+    if (getLayerCount() > 0) {
+        layer = getLayer(getLayerCount() - 1);
+    }
 
     if (!m_headsUpDisplay) {
 
@@ -189,12 +191,8 @@
     m_hthumb->setMaximumValue(count);
     m_hthumb->setValue(count - current);
 
-//    cerr << "set value to " << count - 1 - current << endl;
-//    cerr << "default value is " << m_hthumb->getDefaultValue() << endl;
-
     if (m_hthumb->getDefaultValue() == 0) {
         m_hthumb->setDefaultValue(count - current);
-//        cerr << "set default value to " << m_hthumb->getDefaultValue() << endl;
     }
 
     bool haveVThumb = false;
--- a/view/PaneStack.cpp	Fri Oct 12 11:17:29 2018 +0100
+++ b/view/PaneStack.cpp	Thu Oct 18 13:21:56 2018 +0100
@@ -286,7 +286,9 @@
 void
 PaneStack::deletePane(Pane *pane)
 {
-    cerr << "PaneStack::deletePane(" << pane << ")" << endl;
+#ifdef DEBUG_PANE_STACK
+    SVCERR << "PaneStack::deletePane(" << pane << ")" << endl;
+#endif
 
     std::vector<PaneRec>::iterator i;
     bool found = false;
@@ -322,7 +324,9 @@
     emit paneAboutToBeDeleted(pane);
     unlinkAlignmentViews();
 
-    cerr << "PaneStack::deletePane: about to delete parent " << pane->parent() << " of pane " << pane << endl;
+#ifdef DEBUG_PANE_STACK
+    SVCERR << "PaneStack::deletePane: about to delete parent " << pane->parent() << " of pane " << pane << endl;
+#endif
 
     // The property stack associated with the parent was initially
     // created with the same parent as it, so it would be deleted when
@@ -353,7 +357,9 @@
 void
 PaneStack::showOrHidePaneAccessories()
 {
-    cerr << "PaneStack::showOrHidePaneAccessories: count == " << getPaneCount() << endl;
+#ifdef DEBUG_PANE_STACK
+    SVCERR << "PaneStack::showOrHidePaneAccessories: count == " << getPaneCount() << endl;
+#endif
 
     bool multi = (getPaneCount() > 1);
     for (std::vector<PaneRec>::iterator i = m_panes.begin();
@@ -407,7 +413,7 @@
 
     relinkAlignmentViews();
 
-    cerr << "WARNING: PaneStack::hidePane(" << pane << "): Pane not found in visible panes" << endl;
+    SVCERR << "WARNING: PaneStack::hidePane(" << pane << "): Pane not found in visible panes" << endl;
 }
 
 void
@@ -433,7 +439,7 @@
 
     relinkAlignmentViews();
 
-    cerr << "WARNING: PaneStack::showPane(" << pane << "): Pane not found in hidden panes" << endl;
+    SVCERR << "WARNING: PaneStack::showPane(" << pane << "): Pane not found in hidden panes" << endl;
 }
 
 void
@@ -472,7 +478,7 @@
         m_currentPane = pane;
         emit currentPaneChanged(m_currentPane);
     } else {
-        cerr << "WARNING: PaneStack::setCurrentPane(" << pane << "): pane is not a visible pane in this stack" << endl;
+        SVCERR << "WARNING: PaneStack::setCurrentPane(" << pane << "): pane is not a visible pane in this stack" << endl;
     }
 }
 
--- a/widgets/ColourMapComboBox.cpp	Fri Oct 12 11:17:29 2018 +0100
+++ b/widgets/ColourMapComboBox.cpp	Thu Oct 18 13:21:56 2018 +0100
@@ -58,9 +58,9 @@
     if (size < 12) size = 12;
 
     for (int i = 0; i < ColourMapper::getColourMapCount(); ++i) {
-        QString name = ColourMapper::getColourMapName(i);
+        QString name = ColourMapper::getColourMapLabel(i);
         if (m_includeSwatches) {
-            ColourMapper mapper(i, 0.0, 1.0);
+            ColourMapper mapper(i, false, 0.0, 1.0);
             addItem(mapper.getExamplePixmap(QSize(size * 2, size)), name);
         } else {
             addItem(name);