Chris@376: /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ Chris@376: Chris@376: /* Chris@376: Sonic Visualiser Chris@376: An audio file viewer and annotation editor. Chris@376: Centre for Digital Music, Queen Mary, University of London. Chris@1199: This file copyright 2006-2016 Chris Cannam and QMUL. Chris@376: Chris@376: This program is free software; you can redistribute it and/or Chris@376: modify it under the terms of the GNU General Public License as Chris@376: published by the Free Software Foundation; either version 2 of the Chris@376: License, or (at your option) any later version. See the file Chris@376: COPYING included with this distribution for more information. Chris@376: */ Chris@376: Chris@376: #include "ColourMapper.h" Chris@376: Chris@376: #include Chris@376: Chris@376: #include Chris@376: Chris@682: #include "base/Debug.h" Chris@682: Chris@1012: #include Chris@1012: Chris@1199: #include Chris@1199: Chris@1012: using namespace std; Chris@1012: Chris@1362: static vector convertStrings(const vector &strs, Chris@1362: bool reversed) Chris@1012: { Chris@1012: vector converted; Chris@1012: for (const auto &s: strs) converted.push_back(QColor(s)); Chris@1362: if (reversed) { Chris@1362: reverse(converted.begin(), converted.end()); Chris@1362: } Chris@1012: return converted; Chris@1012: } Chris@1012: Chris@1017: static vector ice = convertStrings({ Chris@1015: // Based on ColorBrewer ylGnBu Chris@1015: "#ffffff", "#ffff00", "#f7fcf0", "#e0f3db", "#ccebc5", "#a8ddb5", Chris@1015: "#7bccc4", "#4eb3d3", "#2b8cbe", "#0868ac", "#084081", "#042040" Chris@1362: }, Chris@1362: true); Chris@1012: Chris@1017: static vector cherry = convertStrings({ Chris@1016: "#f7f7f7", "#fddbc7", "#f4a582", "#d6604d", "#b2182b", "#dd3497", Chris@1016: "#ae017e", "#7a0177", "#49006a" Chris@1362: }, Chris@1362: true); Chris@1362: Chris@1362: static vector magma = convertStrings({ Chris@1362: "#FCFFB2", "#FCDF96", "#FBC17D", "#FBA368", "#FA8657", "#F66B4D", Chris@1362: "#ED504A", "#E03B50", "#C92D59", "#B02363", "#981D69", "#81176D", Chris@1362: "#6B116F", "#57096E", "#43006A", "#300060", "#1E0848", "#110B2D", Chris@1362: "#080616", "#000005" Chris@1362: }, Chris@1362: true); Chris@1362: Chris@1362: static vector cividis = convertStrings({ Chris@1362: "#00204c", "#00204e", "#002150", "#002251", "#002353", "#002355", Chris@1362: "#002456", "#002558", "#00265a", "#00265b", "#00275d", "#00285f", Chris@1362: "#002861", "#002963", "#002a64", "#002a66", "#002b68", "#002c6a", Chris@1362: "#002d6c", "#002d6d", "#002e6e", "#002e6f", "#002f6f", "#002f6f", Chris@1362: "#00306f", "#00316f", "#00316f", "#00326e", "#00336e", "#00346e", Chris@1362: "#00346e", "#01356e", "#06366e", "#0a376d", "#0e376d", "#12386d", Chris@1362: "#15396d", "#17396d", "#1a3a6c", "#1c3b6c", "#1e3c6c", "#203c6c", Chris@1362: "#223d6c", "#243e6c", "#263e6c", "#273f6c", "#29406b", "#2b416b", Chris@1362: "#2c416b", "#2e426b", "#2f436b", "#31446b", "#32446b", "#33456b", Chris@1362: "#35466b", "#36466b", "#37476b", "#38486b", "#3a496b", "#3b496b", Chris@1362: "#3c4a6b", "#3d4b6b", "#3e4b6b", "#404c6b", "#414d6b", "#424e6b", Chris@1362: "#434e6b", "#444f6b", "#45506b", "#46506b", "#47516b", "#48526b", Chris@1362: "#49536b", "#4a536b", "#4b546b", "#4c556b", "#4d556b", "#4e566b", Chris@1362: "#4f576c", "#50586c", "#51586c", "#52596c", "#535a6c", "#545a6c", Chris@1362: "#555b6c", "#565c6c", "#575d6d", "#585d6d", "#595e6d", "#5a5f6d", Chris@1362: "#5b5f6d", "#5c606d", "#5d616e", "#5e626e", "#5f626e", "#5f636e", Chris@1362: "#60646e", "#61656f", "#62656f", "#63666f", "#64676f", "#65676f", Chris@1362: "#666870", "#676970", "#686a70", "#686a70", "#696b71", "#6a6c71", Chris@1362: "#6b6d71", "#6c6d72", "#6d6e72", "#6e6f72", "#6f6f72", "#6f7073", Chris@1362: "#707173", "#717273", "#727274", "#737374", "#747475", "#757575", Chris@1362: "#757575", "#767676", "#777776", "#787876", "#797877", "#7a7977", Chris@1362: "#7b7a77", "#7b7b78", "#7c7b78", "#7d7c78", "#7e7d78", "#7f7e78", Chris@1362: "#807e78", "#817f78", "#828078", "#838178", "#848178", "#858278", Chris@1362: "#868378", "#878478", "#888578", "#898578", "#8a8678", "#8b8778", Chris@1362: "#8c8878", "#8d8878", "#8e8978", "#8f8a78", "#908b78", "#918c78", Chris@1362: "#928c78", "#938d78", "#948e78", "#958f78", "#968f77", "#979077", Chris@1362: "#989177", "#999277", "#9a9377", "#9b9377", "#9c9477", "#9d9577", Chris@1362: "#9e9676", "#9f9776", "#a09876", "#a19876", "#a29976", "#a39a75", Chris@1362: "#a49b75", "#a59c75", "#a69c75", "#a79d75", "#a89e74", "#a99f74", Chris@1362: "#aaa074", "#aba174", "#aca173", "#ada273", "#aea373", "#afa473", Chris@1362: "#b0a572", "#b1a672", "#b2a672", "#b4a771", "#b5a871", "#b6a971", Chris@1362: "#b7aa70", "#b8ab70", "#b9ab70", "#baac6f", "#bbad6f", "#bcae6e", Chris@1362: "#bdaf6e", "#beb06e", "#bfb16d", "#c0b16d", "#c1b26c", "#c2b36c", Chris@1362: "#c3b46c", "#c5b56b", "#c6b66b", "#c7b76a", "#c8b86a", "#c9b869", Chris@1362: "#cab969", "#cbba68", "#ccbb68", "#cdbc67", "#cebd67", "#d0be66", Chris@1362: "#d1bf66", "#d2c065", "#d3c065", "#d4c164", "#d5c263", "#d6c363", Chris@1362: "#d7c462", "#d8c561", "#d9c661", "#dbc760", "#dcc860", "#ddc95f", Chris@1362: "#deca5e", "#dfcb5d", "#e0cb5d", "#e1cc5c", "#e3cd5b", "#e4ce5b", Chris@1362: "#e5cf5a", "#e6d059", "#e7d158", "#e8d257", "#e9d356", "#ebd456", Chris@1362: "#ecd555", "#edd654", "#eed753", "#efd852", "#f0d951", "#f1da50", Chris@1362: "#f3db4f", "#f4dc4e", "#f5dd4d", "#f6de4c", "#f7df4b", "#f9e049", Chris@1362: "#fae048", "#fbe147", "#fce246", "#fde345", "#ffe443", "#ffe542", Chris@1362: "#ffe642", "#ffe743", "#ffe844", "#ffe945" Chris@1362: }, Chris@1362: false); Chris@1362: Chris@1012: static void Chris@1012: mapDiscrete(double norm, vector &colours, double &r, double &g, double &b) Chris@1012: { Chris@1015: int n = int(colours.size()); Chris@1012: double m = norm * (n-1); Chris@1408: if (m >= n-1) { colours[n-1].getRgbF(&r, &g, &b, nullptr); return; } Chris@1408: if (m <= 0) { colours[0].getRgbF(&r, &g, &b, nullptr); return; } Chris@1012: int base(int(floor(m))); Chris@1012: double prop0 = (base + 1.0) - m, prop1 = m - base; Chris@1012: QColor c0(colours[base]), c1(colours[base+1]); Chris@1012: r = c0.redF() * prop0 + c1.redF() * prop1; Chris@1012: g = c0.greenF() * prop0 + c1.greenF() * prop1; Chris@1012: b = c0.blueF() * prop0 + c1.blueF() * prop1; Chris@1012: } Chris@1012: Chris@1362: ColourMapper::ColourMapper(int map, bool inverted, double min, double max) : Chris@376: m_map(map), Chris@1362: m_inverted(inverted), Chris@376: m_min(min), Chris@376: m_max(max) Chris@376: { Chris@376: if (m_min == m_max) { Chris@1265: SVCERR << "WARNING: ColourMapper: min == max (== " << m_min Chris@682: << "), adjusting" << endl; Chris@376: m_max = m_min + 1; Chris@376: } Chris@376: } Chris@376: Chris@376: ColourMapper::~ColourMapper() Chris@376: { Chris@376: } Chris@376: Chris@376: int Chris@376: ColourMapper::getColourMapCount() Chris@376: { Chris@1362: return 15; Chris@376: } Chris@376: Chris@376: QString Chris@1362: ColourMapper::getColourMapLabel(int n) Chris@376: { Chris@1362: // When adding a map, be sure to also update getColourMapCount() Chris@1362: Chris@1071: if (n >= getColourMapCount()) return QObject::tr(""); Chris@1362: ColourMap map = (ColourMap)n; Chris@376: Chris@376: switch (map) { Chris@1071: case Green: return QObject::tr("Green"); Chris@1071: case WhiteOnBlack: return QObject::tr("White on Black"); Chris@1071: case BlackOnWhite: return QObject::tr("Black on White"); Chris@1071: case Cherry: return QObject::tr("Cherry"); Chris@1071: case Wasp: return QObject::tr("Wasp"); Chris@1071: case Ice: return QObject::tr("Ice"); Chris@1071: case Sunset: return QObject::tr("Sunset"); Chris@1071: case FruitSalad: return QObject::tr("Fruit Salad"); Chris@1071: case Banded: return QObject::tr("Banded"); Chris@1071: case Highlight: return QObject::tr("Highlight"); Chris@1071: case Printer: return QObject::tr("Printer"); Chris@1071: case HighGain: return QObject::tr("High Gain"); Chris@1362: case BlueOnBlack: return QObject::tr("Blue on Black"); Chris@1362: case Cividis: return QObject::tr("Cividis"); Chris@1362: case Magma: return QObject::tr("Magma"); Chris@376: } Chris@376: Chris@1071: return QObject::tr(""); Chris@376: } Chris@376: Chris@1362: QString Chris@1362: ColourMapper::getColourMapId(int n) Chris@1362: { Chris@1362: if (n >= getColourMapCount()) return ""; Chris@1362: ColourMap map = (ColourMap)n; Chris@1362: Chris@1362: switch (map) { Chris@1362: case Green: return "Green"; Chris@1362: case WhiteOnBlack: return "White on Black"; Chris@1362: case BlackOnWhite: return "Black on White"; Chris@1362: case Cherry: return "Cherry"; Chris@1362: case Wasp: return "Wasp"; Chris@1362: case Ice: return "Ice"; Chris@1362: case Sunset: return "Sunset"; Chris@1362: case FruitSalad: return "Fruit Salad"; Chris@1362: case Banded: return "Banded"; Chris@1362: case Highlight: return "Highlight"; Chris@1362: case Printer: return "Printer"; Chris@1362: case HighGain: return "High Gain"; Chris@1362: case BlueOnBlack: return "Blue on Black"; Chris@1362: case Cividis: return "Cividis"; Chris@1362: case Magma: return "Magma"; Chris@1362: } Chris@1362: Chris@1362: return ""; Chris@1362: } Chris@1362: Chris@1362: int Chris@1362: ColourMapper::getColourMapById(QString id) Chris@1362: { Chris@1362: ColourMap map = (ColourMap)getColourMapCount(); Chris@1362: Chris@1362: if (id == "Green") { map = Green; } Chris@1362: else if (id == "White on Black") { map = WhiteOnBlack; } Chris@1362: else if (id == "Black on White") { map = BlackOnWhite; } Chris@1362: else if (id == "Cherry") { map = Cherry; } Chris@1362: else if (id == "Wasp") { map = Wasp; } Chris@1362: else if (id == "Ice") { map = Ice; } Chris@1362: else if (id == "Sunset") { map = Sunset; } Chris@1362: else if (id == "Fruit Salad") { map = FruitSalad; } Chris@1362: else if (id == "Banded") { map = Banded; } Chris@1362: else if (id == "Highlight") { map = Highlight; } Chris@1362: else if (id == "Printer") { map = Printer; } Chris@1362: else if (id == "High Gain") { map = HighGain; } Chris@1362: else if (id == "Blue on Black") { map = BlueOnBlack; } Chris@1362: else if (id == "Cividis") { map = Cividis; } Chris@1362: else if (id == "Magma") { map = Magma; } Chris@1362: Chris@1362: if (map == (ColourMap)getColourMapCount()) { Chris@1362: return -1; Chris@1362: } else { Chris@1362: return int(map); Chris@1362: } Chris@1362: } Chris@1362: Chris@1362: int Chris@1362: ColourMapper::getBackwardCompatibilityColourMap(int n) Chris@1362: { Chris@1362: /* Returned value should be an index into the series Chris@1362: * (Default/Green, Sunset, WhiteOnBlack, BlackOnWhite, RedOnBlue, Chris@1362: * YellowOnBlack, BlueOnBlack, FruitSalad, Banded, Highlight, Chris@1362: * Printer, HighGain). Minimum 0, maximum 11. Chris@1362: */ Chris@1362: Chris@1362: if (n >= getColourMapCount()) return 0; Chris@1362: ColourMap map = (ColourMap)n; Chris@1362: Chris@1362: switch (map) { Chris@1362: case Green: return 0; Chris@1362: case WhiteOnBlack: return 2; Chris@1362: case BlackOnWhite: return 3; Chris@1362: case Cherry: return 4; Chris@1362: case Wasp: return 5; Chris@1362: case Ice: return 6; Chris@1362: case Sunset: return 1; Chris@1362: case FruitSalad: return 7; Chris@1362: case Banded: return 8; Chris@1362: case Highlight: return 9; Chris@1362: case Printer: return 10; Chris@1362: case HighGain: return 11; Chris@1362: case BlueOnBlack: return 6; Chris@1362: case Cividis: return 6; Chris@1362: case Magma: return 1; Chris@1362: } Chris@1362: Chris@1362: return 0; Chris@1362: } Chris@1362: Chris@376: QColor Chris@902: ColourMapper::map(double value) const Chris@376: { Chris@902: double norm = (value - m_min) / (m_max - m_min); Chris@902: if (norm < 0.0) norm = 0.0; Chris@902: if (norm > 1.0) norm = 1.0; Chris@1362: Chris@1362: if (m_inverted) { Chris@1362: norm = 1.0 - norm; Chris@1362: } Chris@376: Chris@902: double h = 0.0, s = 0.0, v = 0.0, r = 0.0, g = 0.0, b = 0.0; Chris@376: bool hsv = true; Chris@376: Chris@911: double blue = 0.6666, pieslice = 0.3333; Chris@376: Chris@376: if (m_map >= getColourMapCount()) return Qt::black; Chris@1362: ColourMap map = (ColourMap)m_map; Chris@376: Chris@376: switch (map) { Chris@376: Chris@1017: case Green: Chris@902: h = blue - norm * 2.0 * pieslice; Chris@902: s = 0.5f + norm/2.0; Chris@376: v = norm; Chris@376: break; Chris@376: Chris@376: case WhiteOnBlack: Chris@376: r = g = b = norm; Chris@376: hsv = false; Chris@376: break; Chris@376: Chris@376: case BlackOnWhite: Chris@902: r = g = b = 1.0 - norm; Chris@376: hsv = false; Chris@376: break; Chris@376: Chris@1017: case Cherry: Chris@1015: hsv = false; Chris@1017: mapDiscrete(norm, cherry, r, g, b); Chris@376: break; Chris@376: Chris@1017: case Wasp: Chris@902: h = 0.15; Chris@902: s = 1.0; Chris@376: v = norm; Chris@376: break; Chris@1362: Chris@1362: case BlueOnBlack: Chris@1362: h = blue; Chris@1362: s = 1.0; Chris@1362: v = norm * 2.0; Chris@1362: if (v > 1.0) { Chris@1362: v = 1.0; Chris@1362: s = 1.0 - (sqrt(norm) - 0.707) * 3.413; Chris@1362: if (s < 0.0) s = 0.0; Chris@1362: if (s > 1.0) s = 1.0; Chris@1362: } Chris@1362: break; Chris@376: Chris@376: case Sunset: Chris@902: r = (norm - 0.24) * 2.38; Chris@902: if (r > 1.0) r = 1.0; Chris@902: if (r < 0.0) r = 0.0; Chris@902: g = (norm - 0.64) * 2.777; Chris@902: if (g > 1.0) g = 1.0; Chris@902: if (g < 0.0) g = 0.0; Chris@376: b = (3.6f * norm); Chris@902: if (norm > 0.277) b = 2.0 - b; Chris@902: if (b > 1.0) b = 1.0; Chris@902: if (b < 0.0) b = 0.0; Chris@376: hsv = false; Chris@376: break; Chris@376: Chris@376: case FruitSalad: Chris@902: h = blue + (pieslice/6.0) - norm; Chris@902: if (h < 0.0) h += 1.0; Chris@902: s = 1.0; Chris@902: v = 1.0; Chris@376: break; Chris@376: Chris@376: case Banded: Chris@376: if (norm < 0.125) return Qt::darkGreen; Chris@376: else if (norm < 0.25) return Qt::green; Chris@376: else if (norm < 0.375) return Qt::darkBlue; Chris@376: else if (norm < 0.5) return Qt::blue; Chris@376: else if (norm < 0.625) return Qt::darkYellow; Chris@376: else if (norm < 0.75) return Qt::yellow; Chris@376: else if (norm < 0.875) return Qt::darkRed; Chris@376: else return Qt::red; Chris@376: break; Chris@376: Chris@376: case Highlight: Chris@376: if (norm > 0.99) return Qt::white; Chris@376: else return Qt::darkBlue; Chris@376: Chris@376: case Printer: Chris@376: if (norm > 0.8) { Chris@902: r = 1.0; Chris@376: } else if (norm > 0.7) { Chris@902: r = 0.9; Chris@376: } else if (norm > 0.6) { Chris@902: r = 0.8; Chris@376: } else if (norm > 0.5) { Chris@902: r = 0.7; Chris@376: } else if (norm > 0.4) { Chris@902: r = 0.6; Chris@376: } else if (norm > 0.3) { Chris@902: r = 0.5; Chris@376: } else if (norm > 0.2) { Chris@902: r = 0.4; Chris@376: } else { Chris@902: r = 0.0; Chris@376: } Chris@902: r = g = b = 1.0 - r; Chris@376: hsv = false; Chris@376: break; Chris@536: Chris@536: case HighGain: Chris@902: if (norm <= 1.0 / 256.0) { Chris@902: norm = 0.0; Chris@536: } else { Chris@904: norm = 0.1f + (pow(((norm - 0.5) * 2.0), 3.0) + 1.0) / 2.081; Chris@536: } Chris@536: // now as for Sunset Chris@902: r = (norm - 0.24) * 2.38; Chris@902: if (r > 1.0) r = 1.0; Chris@902: if (r < 0.0) r = 0.0; Chris@902: g = (norm - 0.64) * 2.777; Chris@902: if (g > 1.0) g = 1.0; Chris@902: if (g < 0.0) g = 0.0; Chris@536: b = (3.6f * norm); Chris@902: if (norm > 0.277) b = 2.0 - b; Chris@902: if (b > 1.0) b = 1.0; Chris@902: if (b < 0.0) b = 0.0; Chris@536: hsv = false; Chris@536: /* Chris@902: if (r > 1.0) r = 1.0; Chris@902: r = g = b = 1.0 - r; Chris@536: hsv = false; Chris@536: */ Chris@536: break; Chris@1012: Chris@1017: case Ice: Chris@1012: hsv = false; Chris@1017: mapDiscrete(norm, ice, r, g, b); Chris@1362: break; Chris@1362: Chris@1362: case Cividis: Chris@1362: hsv = false; Chris@1362: mapDiscrete(norm, cividis, r, g, b); Chris@1362: break; Chris@1362: Chris@1362: case Magma: Chris@1362: hsv = false; Chris@1362: mapDiscrete(norm, magma, r, g, b); Chris@1362: break; Chris@376: } Chris@376: Chris@376: if (hsv) { Chris@376: return QColor::fromHsvF(h, s, v); Chris@376: } else { Chris@376: return QColor::fromRgbF(r, g, b); Chris@376: } Chris@376: } Chris@376: Chris@376: QColor Chris@376: ColourMapper::getContrastingColour() const Chris@376: { Chris@376: if (m_map >= getColourMapCount()) return Qt::white; Chris@1362: ColourMap map = (ColourMap)m_map; Chris@376: Chris@376: switch (map) { Chris@376: Chris@1017: case Green: Chris@376: return QColor(255, 150, 50); Chris@376: Chris@376: case WhiteOnBlack: Chris@376: return Qt::red; Chris@376: Chris@376: case BlackOnWhite: Chris@376: return Qt::darkGreen; Chris@376: Chris@1017: case Cherry: Chris@376: return Qt::green; Chris@376: Chris@1017: case Wasp: Chris@376: return QColor::fromHsv(240, 255, 255); Chris@376: Chris@1017: case Ice: Chris@376: return Qt::red; Chris@376: Chris@376: case Sunset: Chris@376: return Qt::white; Chris@376: Chris@376: case FruitSalad: Chris@376: return Qt::white; Chris@376: Chris@376: case Banded: Chris@376: return Qt::cyan; Chris@376: Chris@376: case Highlight: Chris@376: return Qt::red; Chris@376: Chris@376: case Printer: Chris@376: return Qt::red; Chris@536: Chris@536: case HighGain: Chris@536: return Qt::red; Chris@1362: Chris@1362: case BlueOnBlack: Chris@1362: return Qt::red; Chris@1362: Chris@1362: case Cividis: Chris@1362: return Qt::white; Chris@1362: Chris@1362: case Magma: Chris@1362: return Qt::white; Chris@376: } Chris@376: Chris@376: return Qt::white; Chris@376: } Chris@376: Chris@376: bool Chris@376: ColourMapper::hasLightBackground() const Chris@376: { Chris@376: if (m_map >= getColourMapCount()) return false; Chris@1362: ColourMap map = (ColourMap)m_map; Chris@376: Chris@376: switch (map) { Chris@376: Chris@376: case BlackOnWhite: Chris@376: case Printer: Chris@536: case HighGain: Chris@376: return true; Chris@376: Chris@1017: case Green: Chris@805: case Sunset: Chris@805: case WhiteOnBlack: Chris@1017: case Cherry: Chris@1017: case Wasp: Chris@1017: case Ice: Chris@805: case FruitSalad: Chris@805: case Banded: Chris@805: case Highlight: Chris@1362: case BlueOnBlack: Chris@1362: case Cividis: Chris@1362: case Magma: Chris@805: Chris@376: default: Chris@376: return false; Chris@376: } Chris@376: } Chris@376: Chris@1199: QPixmap Chris@1199: ColourMapper::getExamplePixmap(QSize size) const Chris@1199: { Chris@1199: QPixmap pmap(size); Chris@1199: pmap.fill(Qt::white); Chris@1199: QPainter paint(&pmap); Chris@376: Chris@1199: int w = size.width(), h = size.height(); Chris@1199: Chris@1199: int margin = 2; Chris@1199: if (w < 4 || h < 4) margin = 0; Chris@1199: else if (w < 8 || h < 8) margin = 1; Chris@1199: Chris@1199: int n = w - margin*2; Chris@1199: Chris@1199: for (int x = 0; x < n; ++x) { Chris@1199: double value = m_min + ((m_max - m_min) * x) / (n-1); Chris@1199: QColor colour(map(value)); Chris@1199: paint.setPen(colour); Chris@1199: paint.drawLine(x + margin, margin, x + margin, h - margin); Chris@1199: } Chris@1199: Chris@1199: return pmap; Chris@1199: } Chris@1199: Chris@1199: