changeset 880:b4787b595db3

Implement and test the interpolating and auto range mappers
author Chris Cannam
date Fri, 31 Jan 2014 17:09:02 +0000
parents eb6b6a88faed
children 816c751a7979
files base/RangeMapper.cpp base/RangeMapper.h base/test/RangeMapperTest.h
diffstat 3 files changed, 361 insertions(+), 2 deletions(-) [+]
line wrap: on
line diff
--- a/base/RangeMapper.cpp	Fri Jan 31 13:39:37 2014 +0000
+++ b/base/RangeMapper.cpp	Fri Jan 31 17:09:02 2014 +0000
@@ -125,3 +125,146 @@
     return value;
 }
 
+InterpolatingRangeMapper::InterpolatingRangeMapper(CoordMap pointMappings,
+                                                   QString unit) :
+    m_mappings(pointMappings),
+    m_unit(unit)
+{
+    for (CoordMap::const_iterator i = m_mappings.begin(); 
+         i != m_mappings.end(); ++i) {
+        m_reverse[i->second] = i->first;
+    }
+}
+
+int
+InterpolatingRangeMapper::getPositionForValue(float value) const
+{
+    CoordMap::const_iterator i = m_mappings.lower_bound(value);
+    CoordMap::const_iterator j = i;
+    --j;
+
+    if (i == m_mappings.end()) return j->second;
+    if (i == m_mappings.begin()) return i->second;
+    if (i->first == value) return i->second;
+
+    return lrintf(((value - j->first) / (i->first - j->first)) *
+                  float(i->second - j->second) + j->second);
+}
+
+float
+InterpolatingRangeMapper::getValueForPosition(int position) const
+{
+    std::map<int, float>::const_iterator i = m_reverse.lower_bound(position);
+    std::map<int, float>::const_iterator j = i;
+    --j;
+
+    if (i == m_reverse.end()) return j->second;
+    if (i == m_reverse.begin()) return i->second;
+    if (i->first == position) return i->second;
+
+    return ((float(position) - j->first) / (i->first - j->first)) *
+        (i->second - j->second) + j->second;
+}
+
+AutoRangeMapper::AutoRangeMapper(CoordMap pointMappings,
+                                 QString unit) :
+    m_mappings(pointMappings),
+    m_unit(unit)
+{
+    m_type = chooseMappingTypeFor(m_mappings);
+
+    CoordMap::const_iterator first = m_mappings.begin();
+    CoordMap::const_iterator last = m_mappings.end();
+    --last;
+
+    switch (m_type) {
+    case StraightLine:
+        m_mapper = new LinearRangeMapper(first->second, last->second,
+                                         first->first, last->first,
+                                         unit, false);
+        break;
+    case Logarithmic:
+        m_mapper = new LogRangeMapper(first->second, last->second,
+                                      first->first, last->first,
+                                      unit, false);
+        break;
+    case Interpolating:
+        m_mapper = new InterpolatingRangeMapper(m_mappings, unit);
+        break;
+    }
+}
+
+AutoRangeMapper::~AutoRangeMapper()
+{
+    delete m_mapper;
+}
+
+AutoRangeMapper::MappingType
+AutoRangeMapper::chooseMappingTypeFor(const CoordMap &mappings)
+{
+    // how do we work out whether a linear/log mapping is "close enough"?
+
+    CoordMap::const_iterator first = mappings.begin();
+    CoordMap::const_iterator last = mappings.end();
+    --last;
+
+    LinearRangeMapper linm(first->second, last->second,
+                           first->first, last->first,
+                           "", false);
+
+    bool inadequate = false;
+
+    for (CoordMap::const_iterator i = mappings.begin();
+         i != mappings.end(); ++i) {
+        int candidate = linm.getPositionForValue(i->first);
+        int diff = candidate - i->second;
+        if (diff < 0) diff = -diff;
+        if (diff > 1) {
+            cerr << "AutoRangeMapper::chooseMappingTypeFor: diff = " << diff
+                 << ", straight-line mapping inadequate" << endl;
+            inadequate = true;
+            break;
+        }
+    }
+
+    if (!inadequate) {
+        return StraightLine;
+    }
+
+    LogRangeMapper logm(first->second, last->second,
+                        first->first, last->first,
+                        "", false);
+
+    inadequate = false;
+
+    for (CoordMap::const_iterator i = mappings.begin();
+         i != mappings.end(); ++i) {
+        int candidate = logm.getPositionForValue(i->first);
+        int diff = candidate - i->second;
+        if (diff < 0) diff = -diff;
+        if (diff > 1) {
+            cerr << "AutoRangeMapper::chooseMappingTypeFor: diff = " << diff
+                 << ", log mapping inadequate" << endl;
+            inadequate = true;
+            break;
+        }
+    }
+
+    if (!inadequate) {
+        return Logarithmic;
+    }
+
+    return Interpolating;
+}
+
+int
+AutoRangeMapper::getPositionForValue(float value) const
+{
+    return m_mapper->getPositionForValue(value);
+}
+
+float
+AutoRangeMapper::getValueForPosition(int position) const
+{
+    return m_mapper->getValueForPosition(position);
+}
--- a/base/RangeMapper.h	Fri Jan 31 13:39:37 2014 +0000
+++ b/base/RangeMapper.h	Fri Jan 31 17:09:02 2014 +0000
@@ -19,7 +19,7 @@
 #include <QString>
 
 #include "Debug.h"
-
+#include <map>
 
 class RangeMapper 
 {
@@ -58,10 +58,16 @@
     bool m_inverted;
 };
 
-
 class LogRangeMapper : public RangeMapper
 {
 public:
+    /**
+     * Map values in range minval->maxval into integer range
+     * minpos->maxpos such that logs of the values are mapped
+     * linearly. minval and minpos must be less than maxval and maxpos
+     * respectively. If inverted is true, the range will be mapped
+     * "backwards" (minval to maxpos and maxval to minpos).
+     */
     LogRangeMapper(int minpos, int maxpos,
                    float minval, float maxval,
                    QString m_unit = "", bool inverted = false);
@@ -89,5 +95,111 @@
     bool m_inverted;
 };
 
+class InterpolatingRangeMapper : public RangeMapper
+{
+public:
+    typedef std::map<float, int> CoordMap;
+
+    /**
+     * Given a series of (value, position) coordinate mappings,
+     * construct a range mapper that maps arbitrary values, in the
+     * range between minimum and maximum of the provided values, onto
+     * coordinates using linear interpolation between the supplied
+     * points.
+     *
+     *!!! todo: Cubic -- more generally useful than linear interpolation
+     *!!! todo: inverted flag
+     *
+     * The set of provided mappings must contain at least two
+     * coordinates.
+     *
+     * It is expected that the values and positions in the coordinate
+     * mappings will both be monotonically increasing (i.e. no
+     * inflections in the mapping curve). Behaviour is undefined if
+     * this is not the case.
+     */
+    InterpolatingRangeMapper(CoordMap pointMappings,
+                             QString unit);
+
+    virtual int getPositionForValue(float value) const;
+    virtual float getValueForPosition(int position) const;
+
+    virtual QString getUnit() const { return m_unit; }
+
+protected:
+    CoordMap m_mappings;
+    std::map<int, float> m_reverse;
+    QString m_unit;
+};
+
+class AutoRangeMapper : public RangeMapper
+{
+public:
+    enum MappingType {
+        Interpolating,
+        StraightLine,
+        Logarithmic,
+    };
+
+    typedef std::map<float, int> CoordMap;
+
+    /**
+     * Given a series of (value, position) coordinate mappings,
+     * construct a range mapper that maps arbitrary values, in the
+     * range between minimum and maximum of the provided values, onto
+     * coordinates. 
+     *
+     * The mapping used may be
+     * 
+     *    Interpolating -- an InterpolatingRangeMapper will be used
+     * 
+     *    StraightLine -- a LinearRangeMapper from the minimum to
+     *    maximum value coordinates will be used, ignoring all other
+     *    supplied coordinate mappings
+     * 
+     *    Logarithmic -- a LogRangeMapper from the minimum to
+     *    maximum value coordinates will be used, ignoring all other
+     *    supplied coordinate mappings
+     *
+     * The mapping will be chosen automatically by looking at the
+     * supplied coordinates. If the supplied coordinates fall on a
+     * straight line, a StraightLine mapping will be used; if they
+     * fall on a log curve, a Logarithmic mapping will be used;
+     * otherwise an Interpolating mapping will be used.
+     *
+     *!!! todo: inverted flag
+     *
+     * The set of provided mappings must contain at least two
+     * coordinates, or at least three if the points are not supposed
+     * to be in a straight line.
+     *
+     * It is expected that the values and positions in the coordinate
+     * mappings will both be monotonically increasing (i.e. no
+     * inflections in the mapping curve). Behaviour is undefined if
+     * this is not the case.
+     */
+    AutoRangeMapper(CoordMap pointMappings,
+                    QString unit);
+
+    ~AutoRangeMapper();
+
+    /**
+     * Return the mapping type in use.
+     */
+    MappingType getType() const { return m_type; }
+
+    virtual int getPositionForValue(float value) const;
+    virtual float getValueForPosition(int position) const;
+
+    virtual QString getUnit() const { return m_unit; }
+
+protected:
+    MappingType m_type;
+    CoordMap m_mappings;
+    QString m_unit;
+    RangeMapper *m_mapper;
+
+    MappingType chooseMappingTypeFor(const CoordMap &);
+};
 
 #endif
--- a/base/test/RangeMapperTest.h	Fri Jan 31 13:39:37 2014 +0000
+++ b/base/test/RangeMapperTest.h	Fri Jan 31 17:09:02 2014 +0000
@@ -131,6 +131,110 @@
 	QCOMPARE(rm.getValueForPosition(0), rm.getValueForPosition(3));
 	QCOMPARE(rm.getValueForPosition(9), rm.getValueForPosition(7));
     }
+
+    void interpolatingForward()
+    {
+	InterpolatingRangeMapper::CoordMap mappings;
+	mappings[1] = 10;
+	mappings[3] = 30;
+	mappings[5] = 70;
+	InterpolatingRangeMapper rm(mappings, "x");
+	QCOMPARE(rm.getUnit(), QString("x"));
+	QCOMPARE(rm.getPositionForValue(1.0), 10);
+	QCOMPARE(rm.getPositionForValue(0.0), 10);
+	QCOMPARE(rm.getPositionForValue(5.0), 70);
+	QCOMPARE(rm.getPositionForValue(6.0), 70);
+	QCOMPARE(rm.getPositionForValue(3.0), 30);
+	QCOMPARE(rm.getPositionForValue(2.5), 25);
+	QCOMPARE(rm.getPositionForValue(4.5), 60);
+    }
+
+    void interpolatingBackward()
+    {
+	InterpolatingRangeMapper::CoordMap mappings;
+	mappings[1] = 10;
+	mappings[3] = 30;
+	mappings[5] = 70;
+	InterpolatingRangeMapper rm(mappings, "x");
+	QCOMPARE(rm.getUnit(), QString("x"));
+	QCOMPARE(rm.getValueForPosition(10), 1.0);
+	QCOMPARE(rm.getValueForPosition(9), 1.0);
+	QCOMPARE(rm.getValueForPosition(70), 5.0);
+	QCOMPARE(rm.getValueForPosition(80), 5.0);
+	QCOMPARE(rm.getValueForPosition(30), 3.0);
+	QCOMPARE(rm.getValueForPosition(25), 2.5);
+	QCOMPARE(rm.getValueForPosition(60), 4.5);
+    }
+
+    void autoLinearForward()
+    {
+	AutoRangeMapper::CoordMap mappings;
+	mappings[0.5] = 1;
+	mappings[4.0] = 8;
+	AutoRangeMapper rm1(mappings, "x");
+	QCOMPARE(rm1.getUnit(), QString("x"));
+	QCOMPARE(rm1.getType(), AutoRangeMapper::StraightLine);
+	QCOMPARE(rm1.getPositionForValue(0.5), 1);
+	QCOMPARE(rm1.getPositionForValue(4.0), 8);
+	QCOMPARE(rm1.getPositionForValue(3.0), 6);
+	QCOMPARE(rm1.getPositionForValue(3.1), 6);
+	mappings[3.0] = 6;
+	mappings[3.5] = 7;
+	AutoRangeMapper rm2(mappings, "x");
+	QCOMPARE(rm2.getUnit(), QString("x"));
+	QCOMPARE(rm2.getType(), AutoRangeMapper::StraightLine);
+	QCOMPARE(rm2.getPositionForValue(0.5), 1);
+	QCOMPARE(rm2.getPositionForValue(4.0), 8);
+	QCOMPARE(rm2.getPositionForValue(3.0), 6);
+	QCOMPARE(rm2.getPositionForValue(3.1), 6);
+    }
+
+    void autoLogForward()
+    {
+	AutoRangeMapper::CoordMap mappings;
+	mappings[10] = 3;
+	mappings[1000] = 5;
+	mappings[100000] = 7;
+	AutoRangeMapper rm1(mappings, "x");
+	QCOMPARE(rm1.getUnit(), QString("x"));
+	QCOMPARE(rm1.getType(), AutoRangeMapper::Logarithmic);
+	QCOMPARE(rm1.getPositionForValue(10.0), 3);
+	QCOMPARE(rm1.getPositionForValue(100000.0), 7);
+	QCOMPARE(rm1.getPositionForValue(1.0), 3);
+	QCOMPARE(rm1.getPositionForValue(1000000.0), 7);
+	QCOMPARE(rm1.getPositionForValue(1000.0), 5);
+	QCOMPARE(rm1.getPositionForValue(900.0), 5);
+	QCOMPARE(rm1.getPositionForValue(20000), 6);
+	mappings[100] = 4;
+	AutoRangeMapper rm2(mappings, "x");
+	QCOMPARE(rm2.getUnit(), QString("x"));
+	QCOMPARE(rm2.getType(), AutoRangeMapper::Logarithmic);
+	QCOMPARE(rm2.getPositionForValue(10.0), 3);
+	QCOMPARE(rm2.getPositionForValue(100000.0), 7);
+	QCOMPARE(rm2.getPositionForValue(1.0), 3);
+	QCOMPARE(rm2.getPositionForValue(1000000.0), 7);
+	QCOMPARE(rm2.getPositionForValue(1000.0), 5);
+	QCOMPARE(rm2.getPositionForValue(900.0), 5);
+	QCOMPARE(rm2.getPositionForValue(20000), 6);
+    }
+
+    void autoInterpolatingForward()
+    {
+	AutoRangeMapper::CoordMap mappings;
+	mappings[1] = 10;
+	mappings[3] = 30;
+	mappings[5] = 70;
+	AutoRangeMapper rm(mappings, "x");
+	QCOMPARE(rm.getUnit(), QString("x"));
+	QCOMPARE(rm.getType(), AutoRangeMapper::Interpolating);
+	QCOMPARE(rm.getPositionForValue(1.0), 10);
+	QCOMPARE(rm.getPositionForValue(0.0), 10);
+	QCOMPARE(rm.getPositionForValue(5.0), 70);
+	QCOMPARE(rm.getPositionForValue(6.0), 70);
+	QCOMPARE(rm.getPositionForValue(3.0), 30);
+	QCOMPARE(rm.getPositionForValue(2.5), 25);
+	QCOMPARE(rm.getPositionForValue(4.5), 60);
+    }
 };
 
 #endif