changeset 1374:631897ba9fca zoom

Merge from default branch
author Chris Cannam
date Tue, 06 Nov 2018 08:59:03 +0000
parents e848ea0850fe (current diff) cca66ce390e0 (diff)
children 694004228ab7
files
diffstat 32 files changed, 1127 insertions(+), 670 deletions(-) [+]
line wrap: on
line diff
--- a/layer/Colour3DPlotLayer.cpp	Fri Oct 05 10:25:52 2018 +0100
+++ b/layer/Colour3DPlotLayer.cpp	Tue Nov 06 08:59:03 2018 +0000
@@ -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),
@@ -302,12 +303,18 @@
 {
     if (name == "Normalization" ||
         name == "Colour Scale" ||
-        name == "Gain") return tr("Scale");
+        name == "Gain") {
+        return tr("Scale");
+    }
     if (name == "Bin Scale" ||
-        name == "Invert Vertical Scale") return tr("Bins");
+        name == "Invert Vertical Scale") {
+        return tr("Bins");
+    }
     if (name == "Opaque" ||
         name == "Smooth" ||
-        name == "Colour") return tr("Colour");
+        name == "Colour") {
+        return tr("Colour");
+    }
     return QString();
 }
 
@@ -361,7 +368,9 @@
         val = convertFromColumnNorm(m_normalization, m_normalizeVisibleArea);
 
     } else if (name == "Invert Vertical Scale") {
-        
+
+        *min = 0;
+        *max = 1;
         *deflt = 0;
         val = (m_invertVertical ? 1 : 0);
 
@@ -374,11 +383,15 @@
 
     } else if (name == "Opaque") {
         
+        *min = 0;
+        *max = 1;
         *deflt = 0;
         val = (m_opaque ? 1 : 0);
         
     } else if (name == "Smooth") {
         
+        *min = 0;
+        *max = 1;
         *deflt = 0;
         val = (m_smooth ? 1 : 0);
         
@@ -394,7 +407,7 @@
                                     int value) const
 {
     if (name == "Colour") {
-        return ColourMapper::getColourMapName(value);
+        return ColourMapper::getColourMapLabel(value);
     }
     if (name == "Colour Scale") {
         switch (value) {
@@ -824,7 +837,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 +994,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 +1026,7 @@
 
         ColourScale::Parameters cparams;
         cparams.colourMap = m_colourMap;
+        cparams.inverted = m_colourInverted;
         cparams.scaleType = m_colourScale;
         cparams.gain = m_gain;
 
@@ -1027,7 +1045,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 +1193,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 +1206,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 +1249,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 05 10:25:52 2018 +0100
+++ b/layer/Colour3DPlotLayer.h	Tue Nov 06 08:59:03 2018 +0000
@@ -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 05 10:25:52 2018 +0100
+++ b/layer/Colour3DPlotRenderer.cpp	Tue Nov 06 08:59:03 2018 +0000
@@ -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;
 
@@ -353,8 +353,6 @@
 Colour3DPlotRenderer::getColumn(int sx, int minbin, int nbins,
                                 int peakCacheIndex) const
 {
-    Profiler profiler("Colour3DPlotRenderer::getColumn");
-    
     // order:
     // get column -> scale -> normalise -> record extents ->
     // peak pick -> distribute/interpolate -> apply display gain
@@ -363,7 +361,38 @@
     // get column -> scale -> normalise
 
     ColumnOp::Column column;
-                
+    
+    if (m_params.showDerivative && sx > 0) {
+
+        auto prev = getColumnRaw(sx - 1, minbin, nbins, peakCacheIndex);
+        column = getColumnRaw(sx, minbin, nbins, peakCacheIndex);
+        
+        for (int i = 0; i < nbins; ++i) {
+            column[i] -= prev[i];
+        }
+
+    } else {
+        column = getColumnRaw(sx, minbin, nbins, peakCacheIndex);
+    }
+
+    if (m_params.colourScale.getScale() == ColourScaleType::Phase &&
+        m_sources.fft) {
+        return column;
+    } else {
+        column = ColumnOp::applyGain(column, m_params.scaleFactor);
+        column = ColumnOp::normalize(column, m_params.normalization);
+        return column;
+    }
+}
+
+ColumnOp::Column
+Colour3DPlotRenderer::getColumnRaw(int sx, int minbin, int nbins,
+                                   int peakCacheIndex) const
+{
+    Profiler profiler("Colour3DPlotRenderer::getColumn");
+
+    ColumnOp::Column column;
+
     if (m_params.colourScale.getScale() == ColourScaleType::Phase &&
         m_sources.fft) {
 
@@ -382,10 +411,6 @@
                 
         column = vector<float>(fullColumn.data() + minbin,
                                fullColumn.data() + minbin + nbins);
-
-        column = ColumnOp::applyGain(column, m_params.scaleFactor);
-
-        column = ColumnOp::normalize(column, m_params.normalization);
     }
 
     return column;
--- a/layer/Colour3DPlotRenderer.h	Fri Oct 05 10:25:52 2018 +0100
+++ b/layer/Colour3DPlotRenderer.h	Tue Nov 06 08:59:03 2018 +0000
@@ -67,6 +67,7 @@
             alwaysOpaque(false),
             interpolate(false),
             invertVertical(false),
+            showDerivative(false),
             scaleFactor(1.0),
             colourRotation(0) { }
 
@@ -100,6 +101,10 @@
         /** Whether to render the whole caboodle upside-down. */
         bool invertVertical;
 
+        /** Whether to show the frame-to-frame difference instead of
+         *  the actual value */
+        bool showDerivative;
+
         /** Initial scale factor (e.g. for FFT scaling). This factor
          *  is applied to all values read from the underlying model
          *  *before* magnitude ranges are calculated, in contrast to
@@ -319,6 +324,8 @@
     
     ColumnOp::Column getColumn(int sx, int minbin, int nbins,
                                int peakCacheIndex) const; // -1 => don't use cache
+    ColumnOp::Column getColumnRaw(int sx, int minbin, int nbins,
+                                  int peakCacheIndex) const; // -1 => don't use cache
 
     void getPreferredPeakCache(const LayerGeometryProvider *,
                                int &peakCacheIndex, int &binsPerPeak) const;
--- a/layer/ColourDatabase.cpp	Fri Oct 05 10:25:52 2018 +0100
+++ b/layer/ColourDatabase.cpp	Tue Nov 06 08:59:03 2018 +0000
@@ -85,6 +85,26 @@
     return -1;
 }
 
+QColor
+ColourDatabase::getContrastingColour(int c) const
+{
+    QColor col = getColour(c);
+    if (col.red() > col.blue()) {
+        if (col.green() > col.blue()) {
+            return Qt::blue;
+        } else {
+            return Qt::yellow;
+        }
+    } else {
+        if (col.green() > col.blue()) {
+            return Qt::yellow;
+        } else {
+            return Qt::red;
+        }
+    }
+    return Qt::red;
+}
+
 bool
 ColourDatabase::useDarkBackground(int c) const
 {
--- a/layer/ColourMapper.cpp	Fri Oct 05 10:25:52 2018 +0100
+++ b/layer/ColourMapper.cpp	Tue Nov 06 08:59:03 2018 +0000
@@ -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 05 10:25:52 2018 +0100
+++ b/layer/ColourMapper.h	Tue Nov 06 08:59:03 2018 +0000
@@ -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 05 10:25:52 2018 +0100
+++ b/layer/ColourScale.cpp	Tue Nov 06 08:59:03 2018 +0000
@@ -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 05 10:25:52 2018 +0100
+++ b/layer/ColourScale.h	Tue Nov 06 08:59:03 2018 +0000
@@ -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 05 10:25:52 2018 +0100
+++ b/layer/NoteLayer.h	Tue Nov 06 08:59:03 2018 +0000
@@ -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/PaintAssistant.cpp	Fri Oct 05 10:25:52 2018 +0100
+++ b/layer/PaintAssistant.cpp	Tue Nov 06 08:59:03 2018 +0000
@@ -242,7 +242,7 @@
 }
 
 double
-PaintAssistant::scalePenWidth(double width)
+PaintAssistant::scaleSize(double size)
 {
     static double ratio = 0.0;
     if (ratio == 0.0) {
@@ -255,21 +255,28 @@
         double em = QFontMetrics(QFont()).height();
         ratio = em / baseEm;
 
-        SVDEBUG << "PaintAssistant::scalePenWidth: ratio is " << ratio
+        SVDEBUG << "PaintAssistant::scaleSize: ratio is " << ratio
                 << " (em = " << em << ")" << endl;
+
+        if (ratio < 1.0) {
+            SVDEBUG << "PaintAssistant::scaleSize: rounding ratio up to 1.0"
+                    << endl;
+            ratio = 1.0;
+        }
     }
 
-    if (ratio <= 1.0) {
-        // we only ever scale up in this method
-        return width;
+    return size * ratio;
+}
+
+double
+PaintAssistant::scalePenWidth(double width)
+{
+    if (width <= 0) {
+        // zero-width pen, produce a scaled one-pixel pen
+        width = 1;
     }
 
-    if (width <= 0) {
-        // zero-width pen, produce a scaled one-pixel pen
-        return ratio;
-    }
-
-    return width * ratio;
+    return scaleSize(width);
 }
 
 QPen
--- a/layer/PaintAssistant.h	Fri Oct 05 10:25:52 2018 +0100
+++ b/layer/PaintAssistant.h	Tue Nov 06 08:59:03 2018 +0000
@@ -49,9 +49,18 @@
                                 QString text, TextStyle style);
 
     /**
+     * Scale up a size in pixels for a hi-dpi display without pixel
+     * doubling. This is like ViewManager::scalePixelSize, but taking
+     * and returning floating-point values rather than integer
+     * pixels. It is also a little more conservative - it never
+     * shrinks the size, it can only increase or leave it unchanged.
+     */
+    static double scaleSize(double size);
+
+    /**
      * Scale up pen width for a hi-dpi display without pixel doubling.
-     * Very similar to ViewManager::scalePixelSize, but a bit more
-     * conservative.
+     * This is like scaleSize except that it also scales the
+     * zero-width case.
      */
     static double scalePenWidth(double width);
 
--- a/layer/RegionLayer.cpp	Fri Oct 05 10:25:52 2018 +0100
+++ b/layer/RegionLayer.cpp	Tue Nov 06 08:59:03 2018 +0000
@@ -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 05 10:25:52 2018 +0100
+++ b/layer/RegionLayer.h	Tue Nov 06 08:59:03 2018 +0000
@@ -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 05 10:25:52 2018 +0100
+++ b/layer/SliceLayer.cpp	Tue Nov 06 08:59:03 2018 +0000
@@ -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 05 10:25:52 2018 +0100
+++ b/layer/SliceLayer.h	Tue Nov 06 08:59:03 2018 +0000
@@ -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 05 10:25:52 2018 +0100
+++ b/layer/SpectrogramLayer.cpp	Tue Nov 06 08:59:03 2018 +0000
@@ -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 05 10:25:52 2018 +0100
+++ b/layer/SpectrogramLayer.h	Tue Nov 06 08:59:03 2018 +0000
@@ -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 05 10:25:52 2018 +0100
+++ b/layer/SpectrumLayer.cpp	Tue Nov 06 08:59:03 2018 +0000
@@ -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 05 10:25:52 2018 +0100
+++ b/layer/SpectrumLayer.h	Tue Nov 06 08:59:03 2018 +0000
@@ -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/TimeRulerLayer.cpp	Fri Oct 05 10:25:52 2018 +0100
+++ b/layer/TimeRulerLayer.cpp	Tue Nov 06 08:59:03 2018 +0000
@@ -154,8 +154,12 @@
 
     sv_frame_t startFrame = v->getStartFrame();
     sv_frame_t endFrame = v->getEndFrame();
+    if (endFrame == startFrame) {
+        endFrame = startFrame + 1;
+    }
 
-    int minPixelSpacing = ViewManager::scalePixelSize(50);
+    int exampleWidth = QFontMetrics(QFont()).width("10:42.987654");
+    int minPixelSpacing = v->getXForViewX(exampleWidth);
 
     RealTime rtStart = RealTime::frame2RealTime(startFrame, sampleRate);
     RealTime rtEnd = RealTime::frame2RealTime(endFrame, sampleRate);
@@ -164,6 +168,15 @@
     if (count < 1) count = 1;
     RealTime rtGap = (rtEnd - rtStart) / count;
 
+#ifdef DEBUG_TIME_RULER_LAYER
+    SVCERR << "zoomLevel = " << v->getZoomLevel()
+           << ", startFrame = " << startFrame << ", endFrame = " << endFrame
+           << ", rtStart = " << rtStart << ", rtEnd = " << rtEnd
+           << ", paint width = " << v->getPaintWidth()
+           << ", minPixelSpacing = " << minPixelSpacing
+           << ", count = " << count << ", rtGap = " << rtGap << endl;
+#endif
+
     int64_t incus;
     quarterTicks = false;
 
@@ -197,6 +210,10 @@
         if (us > 0) { incus *= 2; us /= 2; }
     }
 
+#ifdef DEBUG_TIME_RULER_LAYER
+    SVCERR << "getMajorTickUSec: returning incus = " << incus << endl;
+#endif
+
     return incus;
 }
 
@@ -277,7 +294,7 @@
     // We always use the exact incus in our calculations for where to
     // draw the actual ticks or lines.
 
-    int minPixelSpacing = 50;
+    int minPixelSpacing = v->getXForViewX(50);
     sv_frame_t incFrame = lrint((double(incus) * sampleRate) / 1000000);
     int incX = int(round(v->getZoomLevel().framesToPixels(double(incFrame))));
     int ticks = 10;
--- a/layer/TimeValueLayer.cpp	Fri Oct 05 10:25:52 2018 +0100
+++ b/layer/TimeValueLayer.cpp	Tue Nov 06 08:59:03 2018 +0000
@@ -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),
@@ -199,14 +200,14 @@
     } else if (name == "Draw Segment Division Lines") {
 
         if (min) *min = 0;
-        if (max) *max = 0;
+        if (max) *max = 1;
         if (deflt) *deflt = 1;
         val = (m_drawSegmentDivisions ? 1.0 : 0.0);
 
     } else if (name == "Show Derivative") {
 
         if (min) *min = 0;
-        if (max) *max = 0;
+        if (max) *max = 1;
         if (deflt) *deflt = 0;
         val = (m_derivative ? 1.0 : 0.0);
 
@@ -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 05 10:25:52 2018 +0100
+++ b/layer/TimeValueLayer.h	Tue Nov 06 08:59:03 2018 +0000
@@ -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/layer/WaveformLayer.cpp	Fri Oct 05 10:25:52 2018 +0100
+++ b/layer/WaveformLayer.cpp	Tue Nov 06 08:59:03 2018 +0000
@@ -45,7 +45,6 @@
     m_gain(1.0f),
     m_autoNormalize(false),
     m_showMeans(true),
-    m_greyscale(true),
     m_channelMode(SeparateChannels),
     m_channel(-1),
     m_scale(LinearScale),
@@ -274,15 +273,6 @@
 }
 
 void
-WaveformLayer::setUseGreyscale(bool useGreyscale)
-{
-    if (m_greyscale == useGreyscale) return;
-    m_greyscale = useGreyscale;
-    m_cacheValid = false;
-    emit layerParametersChanged();
-}
-
-void
 WaveformLayer::setChannelMode(ChannelMode channelMode)
 {
     if (m_channelMode == channelMode) return;
@@ -356,14 +346,14 @@
     return true;
 }
 
-int
+double
 WaveformLayer::dBscale(double sample, int m) const
 {
     if (sample < 0.0) return dBscale(-sample, m);
     double dB = AudioLevel::multiplier_to_dB(sample);
     if (dB < -50.0) return 0;
     if (dB > 0.0) return m;
-    return int(((dB + 50.0) * m) / 50.0 + 0.1);
+    return ((dB + 50.0) * m) / 50.0;
 }
 
 int
@@ -547,7 +537,7 @@
         paint = &viewPainter;
     }
 
-    paint->setRenderHint(QPainter::Antialiasing, false);
+    paint->setRenderHint(QPainter::Antialiasing, true);
 
     if (m_middleLineHeight != 0.5) {
         paint->save();
@@ -663,7 +653,7 @@
     
     if (mixingOrMerging) {
         if (minChannel != 0 || maxChannel != 0) {
-            SVCERR << "Internal error: min & max channels should be 0 when merging or mixing all channels" << endl;
+            throw std::logic_error("Internal error: min & max channels should be 0 when merging or mixing all channels");
         } else if (m_model->getChannelCount() > 1) {
             ranges.push_back({});
             m_model->getSummaries
@@ -674,11 +664,24 @@
 
 void
 WaveformLayer::getOversampledRanges(int minChannel, int maxChannel,
-                                    bool /* mixingOrMerging */,
+                                    bool mixingOrMerging,
                                     sv_frame_t frame0, sv_frame_t frame1,
                                     int oversampleBy, RangeVec &ranges)
     const
 {
+    if (mixingOrMerging) {
+        if (minChannel != 0 || maxChannel != 0) {
+            throw std::logic_error("Internal error: min & max channels should be 0 when merging or mixing all channels");
+        }
+        if (m_model->getChannelCount() > 1) {
+            // call back on self for the individual channels with
+            // mixingOrMerging false
+            getOversampledRanges
+                (0, 1, false, frame0, frame1, oversampleBy, ranges);
+            return;
+        }
+    }
+    
     // These frame values, tail length, etc variables are at the model
     // sample rate, not the oversampled rate
 
@@ -721,8 +724,6 @@
                << ", from which returning " << rr.size() << " ranges" << endl;
 #endif    
     }
-
-    //!!! + channel modes
     
     return;
 }
@@ -753,9 +754,8 @@
     if (channels == 0) return;
 
     QColor baseColour = getBaseQColor();
-    vector<QColor> greys = getPartialShades(v);
-        
     QColor midColour = baseColour;
+    
     if (midColour == Qt::black) {
         midColour = Qt::gray;
     } else if (v->hasLightBackground()) {
@@ -764,9 +764,6 @@
         midColour = midColour.light(50);
     }
 
-    int prevRangeBottom = -1, prevRangeTop = -1;
-    QColor prevRangeBottomColour = baseColour, prevRangeTopColour = baseColour;
-
     double gain = m_effectiveGains[ch];
 
     int m = (h / channels) / 2;
@@ -784,7 +781,8 @@
         my = m + (((ch - minChannel) * h) / channels);
     }
 
-    paint->setPen(greys[1]);
+    // Horizontal axis along middle
+    paint->setPen(QPen(midColour, 0));
     paint->drawLine(x0, my, x1, my);
 
     paintChannelScaleGuides(v, paint, rect, ch);
@@ -797,6 +795,14 @@
     (void)frame1; // not actually used
 #endif
 
+    QPainterPath waveformPath;
+    QPainterPath meanPath;
+    QPainterPath clipPath;
+    vector<QPointF> individualSamplePoints;
+
+    bool firstPoint = true;
+    double prevRangeBottom = 0, prevRangeTop = 0;
+    
     for (int x = x0; x <= x1; ++x) {
 
         sv_frame_t f0, f1;
@@ -852,7 +858,7 @@
             continue;
         }
 
-        int rangeBottom = 0, rangeTop = 0, meanBottom = 0, meanTop = 0;
+        double rangeBottom = 0, rangeTop = 0, meanBottom = 0, meanTop = 0;
 
         if (mergingChannels && ranges.size() > 1) {
 
@@ -884,65 +890,61 @@
             }
         }
 
-        int greyLevels = 1;
-        if (m_greyscale && (m_scale == LinearScale)) greyLevels = 4;
-
         switch (m_scale) {
 
         case LinearScale:
-            rangeBottom = int(double(m * greyLevels) * range.min() * gain);
-            rangeTop    = int(double(m * greyLevels) * range.max() * gain);
-            meanBottom  = int(double(-m) * range.absmean() * gain);
-            meanTop     = int(double(m) * range.absmean() * gain);
+            rangeBottom = range.min() * gain * m;
+            rangeTop    = range.max() * gain * m;
+            meanBottom  = range.absmean() * gain * (-m);
+            meanTop     = range.absmean() * gain * m;
             break;
 
         case dBScale:
             if (!mergingChannels) {
-                int db0 = dBscale(range.min() * gain, m);
-                int db1 = dBscale(range.max() * gain, m);
-                rangeTop    = std::max(db0, db1);
-                meanTop     = std::min(db0, db1);
+                double db0 = dBscale(range.min() * gain, m);
+                double db1 = dBscale(range.max() * gain, m);
+                rangeTop = std::max(db0, db1);
+                meanTop = std::min(db0, db1);
                 if (mixingChannels) rangeBottom = meanTop;
                 else rangeBottom = dBscale(range.absmean() * gain, m);
-                meanBottom  = rangeBottom;
+                meanBottom = rangeBottom;
             } else {
-                rangeBottom = -dBscale(range.min() * gain, m * greyLevels);
-                rangeTop    =  dBscale(range.max() * gain, m * greyLevels);
-                meanBottom  = -dBscale(range.absmean() * gain, m);
-                meanTop     =  dBscale(range.absmean() * gain, m);
+                rangeBottom = -dBscale(range.min() * gain, m);
+                rangeTop = dBscale(range.max() * gain, m);
+                meanBottom = -dBscale(range.absmean() * gain, m);
+                meanTop = dBscale(range.absmean() * gain, m);
             }
             break;
 
         case MeterScale:
             if (!mergingChannels) {
-                int r0 = abs(AudioLevel::multiplier_to_preview(range.min() * gain, m));
-                int r1 = abs(AudioLevel::multiplier_to_preview(range.max() * gain, m));
-                rangeTop    = std::max(r0, r1);
-                meanTop     = std::min(r0, r1);
+                double r0 = fabs(AudioLevel::multiplier_to_preview
+                                 (range.min() * gain, m));
+                double r1 = fabs(AudioLevel::multiplier_to_preview
+                                 (range.max() * gain, m));
+                rangeTop = std::max(r0, r1);
+                meanTop = std::min(r0, r1);
                 if (mixingChannels) rangeBottom = meanTop;
-                else rangeBottom = AudioLevel::multiplier_to_preview(range.absmean() * gain, m);
+                else rangeBottom = AudioLevel::multiplier_to_preview
+                         (range.absmean() * gain, m);
                 meanBottom  = rangeBottom;
             } else {
-                rangeBottom = -AudioLevel::multiplier_to_preview(range.min() * gain, m * greyLevels);
-                rangeTop    =  AudioLevel::multiplier_to_preview(range.max() * gain, m * greyLevels);
-                meanBottom  = -AudioLevel::multiplier_to_preview(range.absmean() * gain, m);
-                meanTop     =  AudioLevel::multiplier_to_preview(range.absmean() * gain, m);
+                rangeBottom = -AudioLevel::multiplier_to_preview
+                    (range.min() * gain, m);
+                rangeTop = AudioLevel::multiplier_to_preview
+                    (range.max() * gain, m);
+                meanBottom = -AudioLevel::multiplier_to_preview
+                    (range.absmean() * gain, m);
+                meanTop = AudioLevel::multiplier_to_preview
+                    (range.absmean() * gain, m);
             }
             break;
         }
 
-        rangeBottom = my * greyLevels - rangeBottom;
-        rangeTop    = my * greyLevels - rangeTop;
-        meanBottom  = my - meanBottom;
-        meanTop     = my - meanTop;
-
-        int topFill = (rangeTop % greyLevels);
-        if (topFill > 0) topFill = greyLevels - topFill;
-
-        int bottomFill = (rangeBottom % greyLevels);
-
-        rangeTop = rangeTop / greyLevels;
-        rangeBottom = rangeBottom / greyLevels;
+        rangeBottom = my - rangeBottom;
+        rangeTop = my - rangeTop;
+        meanBottom = my - meanBottom;
+        meanTop = my - meanTop;
 
         bool clipped = false;
 
@@ -951,96 +953,117 @@
         if (rangeBottom < my - m) { rangeBottom = my - m; }
         if (rangeBottom > my + m) { rangeBottom = my + m; }
 
-        if (range.max() <= -1.0 ||
-            range.max() >= 1.0) clipped = true;
+        if (range.max() <= -1.0 || range.max() >= 1.0) {
+            clipped = true;
+        }
             
-        if (meanBottom > rangeBottom) meanBottom = rangeBottom;
-        if (meanTop < rangeTop) meanTop = rangeTop;
+        bool drawMean = m_showMeans;
 
-        bool drawMean = m_showMeans;
-        if (meanTop == rangeTop) {
-            if (meanTop < meanBottom) ++meanTop;
-            else drawMean = false;
+        meanTop = meanTop - 0.5;
+        meanBottom = meanBottom + 0.5;
+        
+        if (meanTop <= rangeTop + 1.0) {
+            meanTop = rangeTop + 1.0;
         }
-        if (meanBottom == rangeBottom && m_scale == LinearScale) {
-            if (meanBottom > meanTop) --meanBottom;
-            else drawMean = false;
+        if (meanBottom >= rangeBottom - 1.0 && m_scale == LinearScale) {
+            meanBottom = rangeBottom - 1.0;
         }
-
-        if (showIndividualSample) {
-            paint->setPen(baseColour);
-            paint->drawRect(x-1, rangeTop-1, 2, 2);
-        }
-        
-        if (x != x0 && prevRangeBottom != -1) {
-            if (prevRangeBottom > rangeBottom + 1 &&
-                prevRangeTop    > rangeBottom + 1) {
-//                    paint->setPen(midColour);
-                paint->setPen(baseColour);
-                paint->drawLine(x-1, prevRangeTop, x, rangeBottom + 1);
-                paint->setPen(prevRangeTopColour);
-                paint->drawPoint(x-1, prevRangeTop);
-            } else if (prevRangeBottom < rangeTop - 1 &&
-                       prevRangeTop    < rangeTop - 1) {
-//                    paint->setPen(midColour);
-                paint->setPen(baseColour);
-                paint->drawLine(x-1, prevRangeBottom, x, rangeTop - 1);
-                paint->setPen(prevRangeBottomColour);
-                paint->drawPoint(x-1, prevRangeBottom);
-            }
-        }
-
-        if (m_model->isReady()) {
-            if (clipped /*!!! ||
-                          range.min() * gain <= -1.0 ||
-                          range.max() * gain >=  1.0 */) {
-                paint->setPen(Qt::red); //!!! getContrastingColour
-            } else {
-                paint->setPen(baseColour);
-            }
-        } else {
-            paint->setPen(midColour);
+        if (meanTop > meanBottom - 1.0) {
+            drawMean = false;
         }
 
 #ifdef DEBUG_WAVEFORM_PAINT_BY_PIXEL
         SVCERR << "range " << rangeBottom << " -> " << rangeTop << ", means " << meanBottom << " -> " << meanTop << ", raw range " << range.min() << " -> " << range.max() << endl;
 #endif
 
-        if (rangeTop == rangeBottom) {
-            paint->drawPoint(x, rangeTop);
-        } else {
-            paint->drawLine(x, rangeBottom, x, rangeTop);
+        double rangeMiddle = (rangeTop + rangeBottom) / 2.0;
+        bool trivialRange = (fabs(rangeTop - rangeBottom) < 1.0);
+        double px = x + 0.5;
+        
+        if (showIndividualSample) {
+            individualSamplePoints.push_back(QPointF(px, rangeTop));
+            if (!trivialRange) {
+                // common e.g. in "butterfly" merging mode
+                individualSamplePoints.push_back(QPointF(px, rangeBottom));
+            }
         }
 
-        prevRangeTopColour = baseColour;
-        prevRangeBottomColour = baseColour;
-
-        if (m_greyscale && (m_scale == LinearScale) && m_model->isReady()) {
-            if (!clipped) {
-                if (rangeTop < rangeBottom) {
-                    if (topFill > 0 &&
-                        (!drawMean || (rangeTop < meanTop - 1))) {
-                        paint->setPen(greys[topFill - 1]);
-                        paint->drawPoint(x, rangeTop);
-                        prevRangeTopColour = greys[topFill - 1];
-                    }
-                    if (bottomFill > 0 && 
-                        (!drawMean || (rangeBottom > meanBottom + 1))) {
-                        paint->setPen(greys[bottomFill - 1]);
-                        paint->drawPoint(x, rangeBottom);
-                        prevRangeBottomColour = greys[bottomFill - 1];
-                    }
-                }
+        bool contiguous = true;
+        if (rangeTop > prevRangeBottom + 0.5 ||
+            rangeBottom < prevRangeTop - 0.5) {
+            contiguous = false;
+        }
+        
+        if (firstPoint || (contiguous && !trivialRange)) {
+            waveformPath.moveTo(QPointF(px, rangeTop));
+            waveformPath.lineTo(QPointF(px, rangeBottom));
+            waveformPath.moveTo(QPointF(px, rangeMiddle));
+        } else {
+            waveformPath.lineTo(QPointF(px, rangeMiddle));
+            if (!trivialRange) {
+                waveformPath.lineTo(QPointF(px, rangeTop));
+                waveformPath.lineTo(QPointF(px, rangeBottom));
+                waveformPath.lineTo(QPointF(px, rangeMiddle));
             }
         }
+
+        firstPoint = false;
+        prevRangeTop = rangeTop;
+        prevRangeBottom = rangeBottom;
         
         if (drawMean) {
-            paint->setPen(midColour);
-            paint->drawLine(x, meanBottom, x, meanTop);
+            meanPath.moveTo(QPointF(px, meanBottom));
+            meanPath.lineTo(QPointF(px, meanTop));
         }
-        
-        prevRangeBottom = rangeBottom;
-        prevRangeTop = rangeTop;
+
+        if (clipped) {
+            if (trivialRange) {
+                clipPath.moveTo(QPointF(px, rangeMiddle));
+                clipPath.lineTo(QPointF(px+1, rangeMiddle));
+            } else {
+                clipPath.moveTo(QPointF(px, rangeBottom));
+                clipPath.lineTo(QPointF(px, rangeTop));
+            }
+        }
+    }
+
+    double penWidth = 1.0;
+    
+    if (m_model->isReady()) {
+        paint->setPen(QPen(baseColour, penWidth));
+    } else {
+        paint->setPen(QPen(midColour, penWidth));
+    }
+    paint->drawPath(waveformPath);
+
+    if (!clipPath.isEmpty()) {
+        paint->save();
+        paint->setPen(QPen(ColourDatabase::getInstance()->
+                           getContrastingColour(m_colour), penWidth));
+        paint->drawPath(clipPath);
+        paint->restore();
+    }
+
+    if (!meanPath.isEmpty()) {
+        paint->save();
+        paint->setPen(QPen(midColour, penWidth));
+        paint->drawPath(meanPath);
+        paint->restore();
+    }
+    
+    if (!individualSamplePoints.empty()) {
+        double sz = PaintAssistant::scaleSize(2.0);
+        if (v->getZoomLevel().zone == ZoomLevel::PixelsPerFrame) {
+            if (v->getZoomLevel().level < 10) {
+                sz = PaintAssistant::scaleSize(1.2);
+            }
+        }
+        paint->save();
+        paint->setPen(QPen(baseColour, penWidth));
+        for (QPointF p: individualSamplePoints) {
+            paint->drawRect(QRectF(p.x() - sz/2, p.y() - sz/2, sz, sz));
+        }
+        paint->restore();
     }
 }
 
@@ -1225,7 +1248,7 @@
         break;
 
     case dBScale:
-        vy = dBscale(value, m);
+        vy = int(dBscale(value, m));
         break;
     }
 
@@ -1497,7 +1520,8 @@
                  "autoNormalize=\"%9\"")
         .arg(m_gain)
         .arg(m_showMeans)
-        .arg(m_greyscale)
+        .arg(true) // Option removed, but effectively always on, so
+                   // retained in the session file for compatibility
         .arg(m_channelMode)
         .arg(m_channel)
         .arg(m_scale)
@@ -1522,10 +1546,6 @@
                       attributes.value("showMeans") == "true");
     setShowMeans(showMeans);
 
-    bool greyscale = (attributes.value("greyscale") == "1" ||
-                      attributes.value("greyscale") == "true");
-    setUseGreyscale(greyscale);
-
     ChannelMode channelMode = (ChannelMode)
         attributes.value("channelMode").toInt(&ok);
     if (ok) setChannelMode(channelMode);
@@ -1541,7 +1561,7 @@
 
     bool aggressive = (attributes.value("aggressive") == "1" ||
                        attributes.value("aggressive") == "true");
-    setUseGreyscale(aggressive);
+    setAggressiveCacheing(aggressive);
 
     bool autoNormalize = (attributes.value("autoNormalize") == "1" ||
                           attributes.value("autoNormalize") == "true");
--- a/layer/WaveformLayer.h	Fri Oct 05 10:25:52 2018 +0100
+++ b/layer/WaveformLayer.h	Tue Nov 06 08:59:03 2018 +0000
@@ -87,19 +87,6 @@
     void setShowMeans(bool);
     bool getShowMeans() const { return m_showMeans; }
 
-    /**
-     * Set whether to use shades of grey (or of the base colour) to
-     * provide additional perceived vertical resolution (i.e. using
-     * half-filled pixels to represent levels that only just meet the
-     * pixel unit boundary).  This provides a small improvement in
-     * waveform quality at a small cost in rendering speed.
-     * 
-     * The default is to use greyscale.
-     */
-    void setUseGreyscale(bool);
-    bool getUseGreyscale() const { return m_greyscale; }
-
-
     enum ChannelMode { SeparateChannels, MixChannels, MergeChannels };
 
     /**
@@ -205,7 +192,7 @@
     virtual bool canExistWithoutModel() const { return true; }
 
 protected:
-    int dBscale(double sample, int m) const;
+    double dBscale(double sample, int m) const;
 
     const RangeSummarisableTimeValueModel *m_model; // I do not own this
 
@@ -247,7 +234,6 @@
     float        m_gain;
     bool         m_autoNormalize;
     bool         m_showMeans;
-    bool         m_greyscale;
     ChannelMode  m_channelMode;
     int          m_channel;
     Scale        m_scale;
--- a/view/Pane.cpp	Fri Oct 05 10:25:52 2018 +0100
+++ b/view/Pane.cpp	Tue Nov 06 08:59:03 2018 +0000
@@ -62,9 +62,6 @@
 
 //#define DEBUG_PANE
 
-
-
-
 QCursor *Pane::m_measureCursor1 = 0;
 QCursor *Pane::m_measureCursor2 = 0;
 
@@ -110,26 +107,11 @@
 
     if (!isVisible()) return;
 
-/*
-    int count = 0;
-    int currentLevel = 1;
-    int level = 1;
-    while (true) {
-        if (getZoomLevel() == level) currentLevel = count;
-        int newLevel = getZoomConstraintBlockSize(level + 1,
-                                                  ZoomConstraint::RoundUp);
-        if (newLevel == level) break;
-        if (newLevel == 131072) break; //!!! just because
-        level = newLevel;
-        ++count;
+    Layer *layer = 0;
+    if (getLayerCount() > 0) {
+        layer = getLayer(getLayerCount() - 1);
     }
 
-    cerr << "Have " << count+1 << " zoom levels" << endl;
-*/
-
-    Layer *layer = 0;
-    if (getLayerCount() > 0) layer = getLayer(getLayerCount() - 1);
-
     if (!m_headsUpDisplay) {
 
         m_headsUpDisplay = new QFrame(this);
@@ -199,70 +181,15 @@
         connect(m_reset, SIGNAL(mouseLeft()), this, SLOT(mouseLeftWidget()));
     }
 
-    int count = 0;
-    int current = 0;
-    ZoomLevel level;
-
-    //!!! pull out into function (presumably in View)
-    bool haveConstraint = false;
-    for (LayerList::const_iterator i = m_layerStack.begin(); i != m_layerStack.end();
-         ++i) {
-        if ((*i)->getZoomConstraint() && !(*i)->supportsOtherZoomLevels()) {
-            haveConstraint = true;
-            break;
-        }
-    }
-
-    SVCERR << "haveConstraint = " << haveConstraint << endl;
-            
-    if (haveConstraint) {
-        while (true) {
-            //!!! this won't terminate if level is in the PixelsPerFrame zone
-            if (getZoomLevel() == level) current = count;
-            ZoomLevel newLevel = getZoomConstraintLevel(level.incremented(),
-                                                        ZoomConstraint::RoundUp);
-            SVCERR << "newLevel = " << newLevel << endl;
-            if (newLevel == level) break;
-            level = newLevel;
-            if (++count == 50) break;
-        }
-    } else {
-        // if we have no particular constraints, we can really spread out
-        //!!! this is nonsense in PixelsPerFrame zone
-        while (true) {
-            using namespace std::rel_ops;
-            if (getZoomLevel() >= level) current = count;
-            int step = level.level / 10;
-            int pwr = 0;
-            while (step > 0) {
-                ++pwr;
-                step /= 2;
-            }
-            step = 1;
-            while (pwr > 0) {
-                step *= 2;
-                --pwr;
-            }
-            cerr << level.level << ", step " << step << endl;
-            level.level += step;
-            if (++count == 100 || level.level > 262144) break;
-        }
-    }
-
-    //!!!
-    SVCERR << "Have " << count << " zoom levels" << endl;
-
-    m_hthumb->setMinimumValue(0);
+    int count = countZoomLevels();
+    int current = getZoomLevelIndex(getZoomLevel());
+    
+    m_hthumb->setMinimumValue(1);
     m_hthumb->setMaximumValue(count);
     m_hthumb->setValue(count - current);
 
-//    cerr << "set value to " << count-current << endl;
-
-//    cerr << "default value is " << m_hthumb->getDefaultValue() << endl;
-
-    if (count != 50 && m_hthumb->getDefaultValue() == 0) {
+    if (m_hthumb->getDefaultValue() == 0) {
         m_hthumb->setDefaultValue(count - current);
-//        cerr << "set default value to " << m_hthumb->getDefaultValue() << endl;
     }
 
     bool haveVThumb = false;
@@ -2441,53 +2368,7 @@
 void
 Pane::horizontalThumbwheelMoved(int value)
 {
-    //!!! dupe with updateHeadsUpDisplay
-
-    int count = 0;
-    ZoomLevel level;
-
-    //!!! pull out into function (presumably in View)
-    bool haveConstraint = false;
-    for (LayerList::const_iterator i = m_layerStack.begin(); i != m_layerStack.end();
-         ++i) {
-        if ((*i)->getZoomConstraint() && !(*i)->supportsOtherZoomLevels()) {
-            haveConstraint = true;
-            break;
-        }
-    }
-
-    if (haveConstraint) {
-        while (true) {
-            //!!! this won't terminate if level is in the PixelsPerFrame zone
-            if (m_hthumb->getMaximumValue() - value == count) break;
-            ZoomLevel newLevel = getZoomConstraintLevel(level.incremented(),
-                                                        ZoomConstraint::RoundUp);
-            if (newLevel == level) break;
-            level = newLevel;
-            if (++count == 50) break;
-        }
-    } else {
-        //!!! this is nonsense in PixelsPerFrame zone
-        while (true) {
-            if (m_hthumb->getMaximumValue() - value == count) break;
-            int step = level.level / 10;
-            int pwr = 0;
-            while (step > 0) {
-                ++pwr;
-                step /= 2;
-            }
-            step = 1;
-            while (pwr > 0) {
-                step *= 2;
-                --pwr;
-            }
-//            cerr << level << endl;
-            level.level += step;
-            if (++count == 100 || level.level > 262144) break;
-        }
-    }
-        
-//    cerr << "new level is " << level << endl;
+    ZoomLevel level = getZoomLevelByIndex(m_hthumb->getMaximumValue() - value);
     setZoomLevel(level);
 }    
 
--- a/view/Pane.h	Fri Oct 05 10:25:52 2018 +0100
+++ b/view/Pane.h	Tue Nov 06 08:59:03 2018 +0000
@@ -13,8 +13,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _PANE_H_
-#define _PANE_H_
+#ifndef SV_PANE_H
+#define SV_PANE_H
 
 #include <QFrame>
 #include <QPoint>
@@ -212,7 +212,7 @@
 
     bool m_playbackFrameMoveScheduled;
     sv_frame_t m_playbackFrameMoveTo;
-
+    
     static QCursor *m_measureCursor1;
     static QCursor *m_measureCursor2;
 };
--- a/view/PaneStack.cpp	Fri Oct 05 10:25:52 2018 +0100
+++ b/view/PaneStack.cpp	Tue Nov 06 08:59:03 2018 +0000
@@ -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/view/View.cpp	Fri Oct 05 10:25:52 2018 +0100
+++ b/view/View.cpp	Tue Nov 06 08:59:03 2018 +0000
@@ -20,13 +20,14 @@
 #include "base/Profiler.h"
 #include "base/Pitch.h"
 #include "base/Preferences.h"
+#include "base/HitCount.h"
 #include "ViewProxy.h"
 
 #include "layer/TimeRulerLayer.h"
 #include "layer/SingleColourLayer.h"
 #include "layer/PaintAssistant.h"
 
-#include "data/model/PowerOfSqrtTwoZoomConstraint.h"
+#include "data/model/RelativelyFineZoomConstraint.h"
 #include "data/model/RangeSummarisableTimeValueModel.h"
 
 #include "widgets/IconLoader.h"
@@ -63,6 +64,7 @@
     m_showProgress(showProgress),
     m_cache(0),
     m_buffer(0),
+    m_cacheValid(false),
     m_cacheCentreFrame(0),
     m_cacheZoomLevel(ZoomLevel::FramesPerPixel, 1024),
     m_selectionCached(false),
@@ -260,8 +262,7 @@
         return;
     }
 
-    delete m_cache;
-    m_cache = 0;
+    m_cacheValid = false;
 
     Layer *selectedLayer = 0;
 
@@ -293,8 +294,7 @@
 void
 View::overlayModeChanged()
 {
-    delete m_cache;
-    m_cache = 0;
+    m_cacheValid = false;
     update();
 }
 
@@ -340,7 +340,7 @@
         
         if (m_zoomLevel.zone == ZoomLevel::PixelsPerFrame) {
 
-#ifdef DEBUG_VIEW_WIDGET_PAINT
+#ifdef DEBUG_VIEW
             SVCERR << "View(" << this << ")::setCentreFrame: in PixelsPerFrame zone, so change must be visible" << endl;
 #endif
             update();
@@ -353,11 +353,18 @@
         
             if (newPixel != formerPixel) {
 
-#ifdef DEBUG_VIEW_WIDGET_PAINT
+#ifdef DEBUG_VIEW
                 SVCERR << "View(" << this << ")::setCentreFrame: newPixel " << newPixel << ", formerPixel " << formerPixel << endl;
 #endif
                 // ensure the centre frame is a multiple of the zoom level
                 m_centreFrame = sv_frame_t(newPixel) * m_zoomLevel.level;
+
+#ifdef DEBUG_VIEW
+                SVCERR << "View(" << this
+                       << ")::setCentreFrame: centre frame rounded to "
+                       << m_centreFrame << " (zoom level is "
+                       << m_zoomLevel.level << ")" << endl;
+#endif
                 
                 update();
                 changeVisible = true;
@@ -365,11 +372,12 @@
         }
 
         if (e) {
-            sv_frame_t rf = alignToReference(f);
+            sv_frame_t rf = alignToReference(m_centreFrame);
 #ifdef DEBUG_VIEW
             cerr << "View[" << this << "]::setCentreFrame(" << f
-                      << "): emitting centreFrameChanged("
-                      << rf << ")" << endl;
+                 << "): m_centreFrame = " << m_centreFrame
+                 << ", emitting centreFrameChanged with aligned frame "
+                 << rf << endl;
 #endif
             emit centreFrameChanged(rf, m_followPan, m_followPlay);
         }
@@ -427,7 +435,7 @@
         result = fdiff + m_centreFrame;
     }
 
-#ifdef DEBUG_VIEW
+#ifdef DEBUG_VIEW_WIDGET_PAINT
     if (x == 0) {
         SVCERR << "getFrameForX(" << x << "): diff = " << diff << ", fdiff = "
                << fdiff << ", m_centreFrame = " << m_centreFrame
@@ -617,8 +625,7 @@
 void
 View::addLayer(Layer *layer)
 {
-    delete m_cache;
-    m_cache = 0;
+    m_cacheValid = false;
 
     SingleColourLayer *scl = dynamic_cast<SingleColourLayer *>(layer);
     if (scl) scl->setDefaultColourFor(this);
@@ -689,8 +696,7 @@
         return;
     }
 
-    delete m_cache;
-    m_cache = 0;
+    m_cacheValid = false;
 
     for (LayerList::iterator i = m_fixedOrderLayers.begin();
          i != m_fixedOrderLayers.end();
@@ -907,8 +913,7 @@
     }
 
     if (recreate) {
-        delete m_cache;
-        m_cache = 0;
+        m_cacheValid = false;
     }
 
     emit layerModelChanged();
@@ -955,8 +960,7 @@
     }
 
     if (recreate) {
-        delete m_cache;
-        m_cache = 0;
+        m_cacheValid = false;
     }
 
     if (startFrame < myStartFrame) startFrame = myStartFrame;
@@ -991,9 +995,7 @@
 #ifdef DEBUG_VIEW_WIDGET_PAINT
     cerr << "View(" << this << ")::modelReplaced()" << endl;
 #endif
-    delete m_cache;
-    m_cache = 0;
-
+    m_cacheValid = false;
     update();
 }
 
@@ -1006,8 +1008,7 @@
     SVDEBUG << "View::layerParametersChanged()" << endl;
 #endif
 
-    delete m_cache;
-    m_cache = 0;
+    m_cacheValid = false;
     update();
 
     if (layer) {
@@ -1203,8 +1204,7 @@
 View::selectionChanged()
 {
     if (m_selectionCached) {
-        delete m_cache;
-        m_cache = 0;
+        m_cacheValid = false;
         m_selectionCached = false;
     }
     update();
@@ -1469,32 +1469,98 @@
 {
     using namespace std::rel_ops;
     
-    ZoomLevel candidate = zoomLevel;
-    bool haveCandidate = false;
-
-    PowerOfSqrtTwoZoomConstraint defaultZoomConstraint;
-
-    for (auto i = m_layerStack.begin(); i != m_layerStack.end(); ++i) {
-
-        const ZoomConstraint *zoomConstraint = (*i)->getZoomConstraint();
-        if (!zoomConstraint) zoomConstraint = &defaultZoomConstraint;
-
+    ZoomLevel candidate =
+        RelativelyFineZoomConstraint().getNearestZoomLevel(zoomLevel, dir);
+
+    for (auto i : m_layerStack) {
+
+        if (i->supportsOtherZoomLevels() || !(i->getZoomConstraint())) {
+            continue;
+        }
+        
         ZoomLevel thisLevel =
-            zoomConstraint->getNearestZoomLevel(zoomLevel, dir);
+            i->getZoomConstraint()->getNearestZoomLevel(zoomLevel, dir);
 
         // Go for the block size that's furthest from the one
         // passed in.  Most of the time, that's what we want.
-        if (!haveCandidate ||
-            (thisLevel > zoomLevel && thisLevel > candidate) ||
+        if ((thisLevel > zoomLevel && thisLevel > candidate) ||
             (thisLevel < zoomLevel && thisLevel < candidate)) {
             candidate = thisLevel;
-            haveCandidate = true;
         }
     }
 
     return candidate;
 }
 
+int
+View::countZoomLevels() const
+{
+    int n = 0;
+    ZoomLevel min = ZoomConstraint().getMinZoomLevel();
+    ZoomLevel max = ZoomConstraint().getMaxZoomLevel();
+    ZoomLevel level = min;
+    while (true) {
+        ++n;
+        if (level == max) {
+            break;
+        }
+        level = getZoomConstraintLevel
+            (level.incremented(), ZoomConstraint::RoundUp);
+    }
+//    cerr << "View::countZoomLevels: " << n << endl;
+    return n;
+}
+
+ZoomLevel
+View::getZoomLevelByIndex(int ix) const
+{
+    int n = 0;
+    ZoomLevel min = ZoomConstraint().getMinZoomLevel();
+    ZoomLevel max = ZoomConstraint().getMaxZoomLevel();
+    ZoomLevel level = min;
+    while (true) {
+        if (n == ix) {
+//            cerr << "View::getZoomLevelByIndex: " << ix << " -> " << level
+//                 << endl;
+            return level;
+        }
+        ++n;
+        if (level == max) {
+            break;
+        }
+        level = getZoomConstraintLevel
+            (level.incremented(), ZoomConstraint::RoundUp);
+    }
+//    cerr << "View::getZoomLevelByIndex: " << ix << " -> " << max << " (max)"
+//         << endl;
+    return max;
+}
+
+int
+View::getZoomLevelIndex(ZoomLevel z) const
+{
+    int n = 0;
+    ZoomLevel min = ZoomConstraint().getMinZoomLevel();
+    ZoomLevel max = ZoomConstraint().getMaxZoomLevel();
+    ZoomLevel level = min;
+    while (true) {
+        if (z == level) {
+//            cerr << "View::getZoomLevelIndex: " << z << " -> " << n
+//                 << endl;
+            return n;
+        }
+        ++n;
+        if (level == max) {
+            break;
+        }
+        level = getZoomConstraintLevel
+            (level.incremented(), ZoomConstraint::RoundUp);
+    }
+//    cerr << "View::getZoomLevelIndex: " << z << " -> " << n << " (max)"
+//         << endl;
+    return n;
+}
+
 bool
 View::areLayerColoursSignificant() const
 {
@@ -1546,6 +1612,12 @@
     }
     if (right) delta = -delta;
 
+#ifdef DEBUG_VIEW
+    SVCERR << "View::scroll(" << right << ", " << lots << ", " << e << "): "
+           << "delta = " << delta << ", m_centreFrame = " << m_centreFrame
+           << endl;
+#endif
+    
     if (m_centreFrame < delta) {
         setCentreFrame(0, e);
     } else if (m_centreFrame - delta >= getModelsEndFrame()) {
@@ -1734,98 +1806,93 @@
     }
 
     // ensure our constraints are met
-
-/*!!! Should we do this only if we have layers that can't support other
-  zoom levels?
-
-    m_zoomLevel = getZoomConstraintBlockSize(m_zoomLevel,
-                                             ZoomConstraint::RoundUp);
-*/
-
-    QPainter paint;
-    bool repaintCache = false;
-    bool paintedCacheRect = false;
-
-    QRect cacheRect(rect());
-
+    m_zoomLevel = getZoomConstraintLevel
+        (m_zoomLevel, ZoomConstraint::RoundNearest);
+
+    // We have a cache, which retains the state of scrollable (back)
+    // layers from one paint to the next, and a buffer, which we paint
+    // onto before copying directly to the widget. Both are at scaled
+    // resolution (e.g. 2x on a pixel-doubled display), whereas the
+    // paint event always comes in at formal (1x) resolution.
+
+    // If we touch the cache, we always leave it in a valid state
+    // across its whole extent. When another method invalidates the
+    // cache, it does so by setting m_cacheValid false, so if that
+    // flag is true on entry, then the cache is valid across its whole
+    // extent - although it may be valid for a different centre frame,
+    // zoom level, or view size from those now in effect.
+
+    // Our process goes:
+    // 
+    // 1. Check whether we have any scrollable (cacheable) layers.  If
+    //    we don't, then invalidate and ignore the cache and go to
+    //    step 5.  Otherwise:
+    // 
+    // 2. Check the cache, scroll as necessary, identify any area that
+    //    needs to be refreshed (this might be the whole cache).
+    //
+    // 3. Paint to cache the area that needs to be refreshed, from the
+    //    stack of scrollable layers.
+    //
+    // 4. Paint to buffer from cache: if there are no non-cached areas
+    //    or selections and the cache has not scrolled, then paint the
+    //    union of the area of cache that has changed and the area
+    //    that the paint event reported as exposed; otherwise paint
+    //    the whole.
+    //
+    // 5. Paint the exposed area to the buffer from the cache plus all
+    //    the layers that haven't been cached, plus selections etc.
+    //
+    // 6. Paint the exposed rect from the buffer.
+    //
+    // Note that all rects except the target for the final step are at
+    // cache (scaled, 2x as applicable) resolution.
+
+    int dpratio = effectiveDevicePixelRatio();
+
+    QRect requestedPaintArea(scaledRect(rect(), dpratio));
     if (e) {
-        cacheRect &= e->rect();
-#ifdef DEBUG_VIEW_WIDGET_PAINT
-        cerr << "paint rect " << cacheRect.width() << "x" << cacheRect.height()
-                  << ", my rect " << width() << "x" << height() << endl;
-#endif
+        // cut down to only the area actually exposed
+        requestedPaintArea &= scaledRect(e->rect(), dpratio);
     }
 
-    QRect nonCacheRect(cacheRect);
-
-    int dpratio = effectiveDevicePixelRatio();
-
     // If not all layers are scrollable, but some of the back layers
     // are, we should store only those in the cache.
 
     bool layersChanged = false;
     LayerList scrollables = getScrollableBackLayers(true, layersChanged);
     LayerList nonScrollables = getNonScrollableFrontLayers(true, layersChanged);
-    bool selectionCacheable = nonScrollables.empty();
-    bool haveSelections = m_manager && !m_manager->getSelections().empty();
-
-    // If all the non-scrollable layers are non-opaque, then we draw
-    // the selection rectangle behind them and cache it.  If any are
-    // opaque, however, or if our device-pixel ratio is not 1 (so we
-    // need to paint direct to the widget), then we can't cache.
-    //
-    if (dpratio == 1) {
-
-        if (!selectionCacheable) {
-            selectionCacheable = true;
-            for (LayerList::const_iterator i = nonScrollables.begin();
-                 i != nonScrollables.end(); ++i) {
-                if ((*i)->isLayerOpaque()) {
-                    selectionCacheable = false;
-                    break;
-                }
-            }
-        }
-
-        if (selectionCacheable) {
-            QPoint localPos;
-            bool closeToLeft, closeToRight;
-            if (shouldIlluminateLocalSelection
-                (localPos, closeToLeft, closeToRight)) {
-                selectionCacheable = false;
-            }
-        }
-
-    } else {
-
-        selectionCacheable = false;
-    }
 
 #ifdef DEBUG_VIEW_WIDGET_PAINT
     cerr << "View(" << this << ")::paintEvent: have " << scrollables.size()
               << " scrollable back layers and " << nonScrollables.size()
               << " non-scrollable front layers" << endl;
-    cerr << "haveSelections " << haveSelections << ", selectionCacheable "
-              << selectionCacheable << ", m_selectionCached " << m_selectionCached << endl;
 #endif
 
-    if (layersChanged || scrollables.empty() ||
-        (haveSelections && (selectionCacheable != m_selectionCached))) {
-        delete m_cache;
-        m_cache = 0;
-        m_selectionCached = false;
+    if (layersChanged || scrollables.empty()) {
+        m_cacheValid = false;
     }
 
-    QSize scaledCacheSize(scaledSize(size(), dpratio));
-    QRect scaledCacheRect(scaledRect(cacheRect, dpratio));
-
-    if (!m_buffer || scaledCacheSize != m_buffer->size()) {
+    QRect wholeArea(scaledRect(rect(), dpratio));
+    QSize wholeSize(scaledSize(size(), dpratio));
+
+    if (!m_buffer || wholeSize != m_buffer->size()) {
         delete m_buffer;
-        m_buffer = new QPixmap(scaledCacheSize);
+        m_buffer = new QPixmap(wholeSize);
     }
+
+    bool shouldUseCache = false;
+    bool shouldRepaintCache = false;
+    QRect cacheAreaToRepaint;
     
+    static HitCount count("View cache");
+
     if (!scrollables.empty()) {
 
+        shouldUseCache = true;
+        shouldRepaintCache = true;
+        cacheAreaToRepaint = wholeArea;
+
 #ifdef DEBUG_VIEW_WIDGET_PAINT
         cerr << "View(" << this << "): cache " << m_cache << ", cache zoom "
                   << m_cacheZoomLevel << ", zoom " << m_zoomLevel << endl;
@@ -1833,209 +1900,185 @@
 
         using namespace std::rel_ops;
     
-        if (!m_cache ||
+        if (!m_cacheValid ||
+            !m_cache ||
             m_cacheZoomLevel != m_zoomLevel ||
-            scaledCacheSize != m_cache->size()) {
-
-            // cache is not valid
-
-            if (cacheRect.width() < width()/10) {
-                delete m_cache;
-                m_cache = 0;
+            m_cache->size() != wholeSize) {
+
+            // cache is not valid at all
+
+            if (requestedPaintArea.width() < wholeSize.width() / 10) {
+
+                m_cacheValid = false;
+                shouldUseCache = false;
+                shouldRepaintCache = false;
+
 #ifdef DEBUG_VIEW_WIDGET_PAINT
-                cerr << "View(" << this << ")::paintEvent: small repaint, not bothering to recreate cache" << endl;
+                cerr << "View(" << this << ")::paintEvent: cache is invalid but only small area requested, will repaint directly instead" << endl;
 #endif
             } else {
-                delete m_cache;
-                m_cache = new QPixmap(scaledCacheSize);
+
+                if (!m_cache ||
+                    m_cache->size() != wholeSize) {
+                    delete m_cache;
+                    m_cache = new QPixmap(wholeSize);
+                }
+
 #ifdef DEBUG_VIEW_WIDGET_PAINT
-                cerr << "View(" << this << ")::paintEvent: recreated cache" << endl;
+                cerr << "View(" << this << ")::paintEvent: cache is invalid, will repaint whole" << endl;
 #endif
-                cacheRect = rect();
-                repaintCache = true;
             }
 
+            count.miss();
+            
         } else if (m_cacheCentreFrame != m_centreFrame) {
 
-            int dx =
-                getXForFrame(m_cacheCentreFrame) -
-                getXForFrame(m_centreFrame);
-
-            if (dx > -width() && dx < width()) {
-                static QPixmap *tmpPixmap = 0;
-                if (!tmpPixmap || tmpPixmap->size() != scaledCacheSize) {
-                    delete tmpPixmap;
-                    tmpPixmap = new QPixmap(scaledCacheSize);
+            int dx = dpratio * (getXForFrame(m_cacheCentreFrame) -
+                                getXForFrame(m_centreFrame));
+
+            if (dx > -m_cache->width() && dx < m_cache->width()) {
+
+                m_cache->scroll(dx, 0, m_cache->rect(), 0);
+
+                if (dx < 0) {
+                    cacheAreaToRepaint = 
+                        QRect(m_cache->width() + dx, 0, -dx, m_cache->height());
+                } else {
+                    cacheAreaToRepaint = 
+                        QRect(0, 0, dx, m_cache->height());
                 }
-                paint.begin(tmpPixmap);
-                paint.drawPixmap(0, 0, *m_cache);
-                paint.end();
-                paint.begin(m_cache);
-                paint.drawPixmap(dx, 0, *tmpPixmap);
-                paint.end();
-                if (dx < 0) {
-                    cacheRect = QRect(width() + dx, 0, -dx, height());
-                } else {
-                    cacheRect = QRect(0, 0, dx, height());
-                }
+
+                count.partial();
+
 #ifdef DEBUG_VIEW_WIDGET_PAINT
                 cerr << "View(" << this << ")::paintEvent: scrolled cache by " << dx << endl;
 #endif
             } else {
-                cacheRect = rect();
+                count.miss();
 #ifdef DEBUG_VIEW_WIDGET_PAINT
                 cerr << "View(" << this << ")::paintEvent: scrolling too far" << endl;
 #endif
             }
-            repaintCache = true;
 
         } else {
 #ifdef DEBUG_VIEW_WIDGET_PAINT
             cerr << "View(" << this << ")::paintEvent: cache is good" << endl;
 #endif
-            paint.begin(m_buffer);
-            paint.drawPixmap(scaledCacheRect, *m_cache, scaledCacheRect);
-            paint.end();
-            QFrame::paintEvent(e);
-            paintedCacheRect = true;
+            count.hit();
+            shouldRepaintCache = false;
         }
-
+    }
+
+#ifdef DEBUG_VIEW_WIDGET_PAINT
+    cerr << "View(" << this << ")::paintEvent: m_cacheValid = " << m_cacheValid << ", shouldUseCache = " << shouldUseCache << ", shouldRepaintCache = " << shouldRepaintCache << ", cacheAreaToRepaint = " << cacheAreaToRepaint.x() << "," << cacheAreaToRepaint.y() << " " << cacheAreaToRepaint.width() << "x" << cacheAreaToRepaint.height() << endl;
+#endif
+
+    if (shouldRepaintCache && !shouldUseCache) {
+        // If we are repainting the cache, then we paint the
+        // scrollables only to the cache, not to the buffer. So if
+        // shouldUseCache is also false, then the scrollables can't
+        // appear because they will only be on the cache
+        throw std::logic_error("ERROR: shouldRepaintCache is true, but shouldUseCache is false: this can't lead to the correct result");
+    }
+
+    // Scrollable (cacheable) items first. If we are repainting the
+    // cache, then we paint these to the cache; otherwise straight to
+    // the buffer.
+
+    ViewProxy proxy(this, dpratio);
+    QRect areaToPaint;
+    QPainter paint;
+
+    if (shouldRepaintCache) {
+        paint.begin(m_cache);
+        areaToPaint = cacheAreaToRepaint;
+    } else {
+        paint.begin(m_buffer);
+        areaToPaint = requestedPaintArea;
+    }
+
+    setPaintFont(paint);
+    paint.setClipRect(areaToPaint);
+
+    paint.setPen(getBackground());
+    paint.setBrush(getBackground());
+    paint.drawRect(areaToPaint);
+
+    paint.setPen(getForeground());
+    paint.setBrush(Qt::NoBrush);
+        
+    for (LayerList::iterator i = scrollables.begin();
+         i != scrollables.end(); ++i) {
+
+        paint.setRenderHint(QPainter::Antialiasing, false);
+        paint.save();
+
+#ifdef DEBUG_VIEW_WIDGET_PAINT
+        cerr << "Painting scrollable layer " << *i << " using proxy with shouldRepaintCache = " << shouldRepaintCache << ", dpratio = " << dpratio << ", areaToPaint = " << areaToPaint.x() << "," << areaToPaint.y() << " " << areaToPaint.width() << "x" << areaToPaint.height() << endl;
+#endif
+
+        (*i)->paint(&proxy, paint, areaToPaint);
+
+        paint.restore();
+    }
+
+    paint.end();
+
+    if (shouldRepaintCache) {
+        // and now we have
+        m_cacheValid = true;
         m_cacheCentreFrame = m_centreFrame;
         m_cacheZoomLevel = m_zoomLevel;
     }
 
-#ifdef DEBUG_VIEW_WIDGET_PAINT
-//    cerr << "View(" << this << ")::paintEvent: cacheRect " << cacheRect << ", nonCacheRect " << (nonCacheRect | cacheRect) << ", repaintCache " << repaintCache << ", paintedCacheRect " << paintedCacheRect << endl;
-#endif
-
-    // Scrollable (cacheable) items first
-
-    ViewProxy proxy(this, dpratio);
-    
-    if (!paintedCacheRect) {
-
-        QRect rectToPaint;
-
-        if (repaintCache) {
-            paint.begin(m_cache);
-            rectToPaint = scaledCacheRect;
-        } else {
-            paint.begin(m_buffer);
-            rectToPaint = scaledCacheRect;
-        }
-
-        setPaintFont(paint);
-        paint.setClipRect(rectToPaint);
-
-        paint.setPen(getBackground());
-        paint.setBrush(getBackground());
-        paint.drawRect(rectToPaint);
-
-        paint.setPen(getForeground());
-        paint.setBrush(Qt::NoBrush);
-        
-        for (LayerList::iterator i = scrollables.begin(); i != scrollables.end(); ++i) {
-            paint.setRenderHint(QPainter::Antialiasing, false);
-            paint.save();
-#ifdef DEBUG_VIEW_WIDGET_PAINT
-            cerr << "Painting scrollable layer " << *i << " using proxy with repaintCache = " << repaintCache << ", dpratio = " << dpratio << ", rectToPaint = " << rectToPaint.x() << "," << rectToPaint.y() << " " << rectToPaint.width() << "x" << rectToPaint.height() << endl;
-#endif
-            (*i)->paint(&proxy, paint, rectToPaint);
-            paint.restore();
-        }
-
-        if (haveSelections && selectionCacheable) {
-            drawSelections(paint);
-            m_selectionCached = repaintCache;
-        }
-        
+    if (shouldUseCache) {
+        paint.begin(m_buffer);
+        paint.drawPixmap(requestedPaintArea, *m_cache, requestedPaintArea);
         paint.end();
-
-        if (repaintCache) {
-            cacheRect |= (e ? e->rect() : rect());
-            scaledCacheRect = scaledRect(cacheRect, dpratio);
-            paint.begin(m_buffer);
-            paint.drawPixmap(scaledCacheRect, *m_cache, scaledCacheRect);
-            paint.end();
-        }
     }
 
-    // Now non-cacheable items.  We always need to redraw the
-    // non-cacheable items across at least the area we drew of the
-    // cacheable items.
-
-    nonCacheRect |= cacheRect;
-
-    QRect scaledNonCacheRect = scaledRect(nonCacheRect, dpratio);
-    
+    // Now non-cacheable items.
+
     paint.begin(m_buffer);
-    paint.setClipRect(scaledNonCacheRect);
+    paint.setClipRect(requestedPaintArea);
     setPaintFont(paint);
     if (scrollables.empty()) {
         paint.setPen(getBackground());
         paint.setBrush(getBackground());
-        paint.drawRect(scaledNonCacheRect);
+        paint.drawRect(requestedPaintArea);
     }
         
     paint.setPen(getForeground());
     paint.setBrush(Qt::NoBrush);
         
-    for (LayerList::iterator i = nonScrollables.begin(); i != nonScrollables.end(); ++i) {
+    for (LayerList::iterator i = nonScrollables.begin(); 
+         i != nonScrollables.end(); ++i) {
+
 //        Profiler profiler2("View::paintEvent non-cacheable");
 #ifdef DEBUG_VIEW_WIDGET_PAINT
-        cerr << "Painting non-scrollable layer " << *i << " without proxy with repaintCache = " << repaintCache << ", dpratio = " << dpratio << ", rectToPaint = " << nonCacheRect.x() << "," << nonCacheRect.y() << " " << nonCacheRect.width() << "x" << nonCacheRect.height() << endl;
+        cerr << "Painting non-scrollable layer " << *i << " without proxy with shouldRepaintCache = " << shouldRepaintCache << ", dpratio = " << dpratio << ", requestedPaintArea = " << requestedPaintArea.x() << "," << requestedPaintArea.y() << " " << requestedPaintArea.width() << "x" << requestedPaintArea.height() << endl;
 #endif
-        (*i)->paint(&proxy, paint, scaledNonCacheRect);
+        (*i)->paint(&proxy, paint, requestedPaintArea);
     }
         
     paint.end();
-    
-    paint.begin(this);
-    QRect finalPaintRect = e ? e->rect() : rect();
-    paint.drawPixmap(finalPaintRect, *m_buffer, scaledRect(finalPaintRect, dpratio));
-    paint.end();
+
+    // Now paint to widget from buffer: target rects from here on,
+    // unlike all the preceding, are at formal (1x) resolution
 
     paint.begin(this);
     setPaintFont(paint);
     if (e) paint.setClipRect(e->rect());
-    if (!m_selectionCached) {
-        drawSelections(paint);
-    }
+
+    QRect finalPaintRect = e ? e->rect() : rect();
+    paint.drawPixmap(finalPaintRect, *m_buffer, 
+                     scaledRect(finalPaintRect, dpratio));
+
+    drawSelections(paint);
+    drawPlayPointer(paint);
+
     paint.end();
 
-    bool showPlayPointer = true;
-    if (m_followPlay == PlaybackScrollContinuous) {
-        showPlayPointer = false;
-    } else if (m_playPointerFrame <= getStartFrame() ||
-               m_playPointerFrame >= getEndFrame()) {
-        showPlayPointer = false;
-    } else if (m_manager && !m_manager->isPlaying()) {
-        if (m_playPointerFrame == getCentreFrame() &&
-            m_manager->shouldShowCentreLine() &&
-            m_followPlay != PlaybackIgnore) {
-            // Don't show the play pointer when it is redundant with
-            // the centre line
-            showPlayPointer = false;
-        }
-    }
-    
-    if (showPlayPointer) {
-
-        paint.begin(this);
-
-        int playx = getXForFrame(m_playPointerFrame);
-        
-        paint.setPen(getForeground());
-        paint.drawLine(playx - 1, 0, playx - 1, height() - 1);
-        paint.drawLine(playx + 1, 0, playx + 1, height() - 1);
-        paint.drawPoint(playx, 0);
-        paint.drawPoint(playx, height() - 1);
-        paint.setPen(getBackground());
-        paint.drawLine(playx, 1, playx, height() - 2);
-
-        paint.end();
-    }
-
     QFrame::paintEvent(e);
 }
 
@@ -2197,6 +2240,40 @@
 }
 
 void
+View::drawPlayPointer(QPainter &paint)
+{
+    bool showPlayPointer = true;
+
+    if (m_followPlay == PlaybackScrollContinuous) {
+        showPlayPointer = false;
+    } else if (m_playPointerFrame <= getStartFrame() ||
+               m_playPointerFrame >= getEndFrame()) {
+        showPlayPointer = false;
+    } else if (m_manager && !m_manager->isPlaying()) {
+        if (m_playPointerFrame == getCentreFrame() &&
+            m_manager->shouldShowCentreLine() &&
+            m_followPlay != PlaybackIgnore) {
+            // Don't show the play pointer when it is redundant with
+            // the centre line
+            showPlayPointer = false;
+        }
+    }
+    
+    if (showPlayPointer) {
+
+        int playx = getXForFrame(m_playPointerFrame);
+        
+        paint.setPen(getForeground());
+        paint.drawLine(playx - 1, 0, playx - 1, height() - 1);
+        paint.drawLine(playx + 1, 0, playx + 1, height() - 1);
+        paint.drawPoint(playx, 0);
+        paint.drawPoint(playx, height() - 1);
+        paint.setPen(getBackground());
+        paint.drawLine(playx, 1, playx, height() - 2);
+    }
+}
+
+void
 View::drawMeasurementRect(QPainter &paint, const Layer *topLayer, QRect r,
                           bool focus) const
 {
--- a/view/View.h	Fri Oct 05 10:25:52 2018 +0100
+++ b/view/View.h	Tue Nov 06 08:59:03 2018 +0000
@@ -440,6 +440,7 @@
     virtual void paintEvent(QPaintEvent *e);
     virtual void drawSelections(QPainter &);
     virtual bool shouldLabelSelections() const { return true; }
+    virtual void drawPlayPointer(QPainter &);
     virtual bool render(QPainter &paint, int x0, sv_frame_t f0, sv_frame_t f1);
     virtual void setPaintFont(QPainter &paint);
 
@@ -457,10 +458,16 @@
     bool areLayersScrollable() const;
     LayerList getScrollableBackLayers(bool testChanged, bool &changed) const;
     LayerList getNonScrollableFrontLayers(bool testChanged, bool &changed) const;
+
     ZoomLevel getZoomConstraintLevel(ZoomLevel level,
                                      ZoomConstraint::RoundingDirection dir =
                                      ZoomConstraint::RoundNearest) const;
 
+    // These three are slow, intended for indexing GUI thumbwheel stuff
+    int countZoomLevels() const;
+    int getZoomLevelIndex(ZoomLevel level) const;
+    ZoomLevel getZoomLevelByIndex(int ix) const;
+    
     // True if the top layer(s) use colours for meaningful things.  If
     // this is the case, selections will be shown using unfilled boxes
     // rather than with a translucent fill.
@@ -493,6 +500,7 @@
 
     QPixmap            *m_cache;  // I own this
     QPixmap            *m_buffer; // I own this
+    bool                m_cacheValid;
     sv_frame_t          m_cacheCentreFrame;
     ZoomLevel           m_cacheZoomLevel;
     bool                m_selectionCached;
--- a/widgets/ColourMapComboBox.cpp	Fri Oct 05 10:25:52 2018 +0100
+++ b/widgets/ColourMapComboBox.cpp	Tue Nov 06 08:59:03 2018 +0000
@@ -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);
--- a/widgets/SubdividingMenu.cpp	Fri Oct 05 10:25:52 2018 +0100
+++ b/widgets/SubdividingMenu.cpp	Tue Nov 06 08:59:03 2018 +0000
@@ -85,7 +85,7 @@
                           return QString::localeAwareCompare(s1, s2) < 0;
                       };
     
-    set<QString, typeof(comparator)> sortedEntries(comparator);
+    set<QString, decltype(comparator)> sortedEntries(comparator);
     sortedEntries.insert(entries.begin(), entries.end());
     
     for (auto j = sortedEntries.begin(); j != sortedEntries.end(); ++j) {
@@ -180,7 +180,7 @@
     auto comparator = [](QString s1, QString s2) -> bool {
                           return QString::localeAwareCompare(s1, s2) < 0;
                       };
-    set<QString, typeof(comparator)> sortedEntries(comparator);
+    set<QString, decltype(comparator)> sortedEntries(comparator);
     for (auto i: m_pendingEntries) {
         sortedEntries.insert(i.first);
     }