337 lines
9.9 KiB
C++
337 lines
9.9 KiB
C++
#ifndef SIGNALSMITH_STFX_STFX2_PARAM_INFO_H
|
|
#define SIGNALSMITH_STFX_STFX2_PARAM_INFO_H
|
|
|
|
#include <vector>
|
|
#include <cmath>
|
|
#include <string>
|
|
#include <sstream>
|
|
#include <iomanip>
|
|
#include <map>
|
|
|
|
namespace stfx {
|
|
class UnitRangeMap {
|
|
double a, b, d;
|
|
public:
|
|
UnitRangeMap() : a(0), b(1), d(0) {} // identity
|
|
UnitRangeMap(double min, double mid, double max) {
|
|
double k = (mid - min)/(max - mid);
|
|
a = min;
|
|
b = max*k - min;
|
|
d = k - 1;
|
|
}
|
|
|
|
double toUnit(double value) const {
|
|
return (value - a)/(b - value*d);
|
|
}
|
|
double fromUnit(double unit) const {
|
|
return (a + unit*b)/(1 + unit*d);
|
|
}
|
|
};
|
|
|
|
struct RangeParamInfo {
|
|
double defaultValue;
|
|
double low, mid, high;
|
|
std::string name, description;
|
|
|
|
RangeParamInfo(double defaultValue) : defaultValue(defaultValue), low(defaultValue), mid(defaultValue), high(defaultValue) {}
|
|
RangeParamInfo(const RangeParamInfo &other) = delete;
|
|
RangeParamInfo(RangeParamInfo &&other) = default;
|
|
|
|
RangeParamInfo & info(std::string pName, std::string pDescription) {
|
|
name = pName;
|
|
description = pDescription;
|
|
return *this;
|
|
}
|
|
RangeParamInfo & range(double pLow, double pHigh) {
|
|
return range(pLow, pHigh, (pLow + pHigh)*0.5);
|
|
}
|
|
RangeParamInfo & range(double pLow, double pMid, double pHigh) {
|
|
low = pLow;
|
|
mid = pMid;
|
|
high = pHigh;
|
|
// sanity check in case we mix the argument order up
|
|
if ((mid < high) != (low < high)) std::swap(mid, high); // middle crosses the high
|
|
if ((low < mid) != (low < high)) std::swap(low, mid); // middle crosses the low
|
|
rangeMap = {low, mid, high};
|
|
return *this;
|
|
}
|
|
|
|
typedef double((*DoubleMap)(double));
|
|
RangeParamInfo & unit(std::string suffix, int precision=2, DoubleMap fromDisplay=identityMap, DoubleMap toDisplay=identityMap, double validLow=doubleLowest, double validHigh=doubleMax) {
|
|
unitOptions.emplace_back(suffix, fromDisplay, toDisplay, validLow, validHigh, precision);
|
|
return *this;
|
|
}
|
|
RangeParamInfo & unit(std::string suffix, DoubleMap fromDisplay, DoubleMap toDisplay, double validLow=doubleLowest, double validHigh=doubleMax) {
|
|
return unit(suffix, 2, fromDisplay, toDisplay, validLow, validHigh);
|
|
}
|
|
RangeParamInfo & unit(std::string suffix, int precision, double validLow, double validHigh) {
|
|
return unit(suffix, precision, identityMap, identityMap, validLow, validHigh);
|
|
}
|
|
RangeParamInfo & unit(std::string suffix, double validLow, double validHigh) {
|
|
return unit(suffix, identityMap, identityMap, validLow, validHigh);
|
|
}
|
|
|
|
RangeParamInfo & exact(double v, std::string valueName) {
|
|
exactOptions.push_back({v, valueName});
|
|
return *this;
|
|
}
|
|
|
|
std::string toString(double value) const {
|
|
for (auto &option : exactOptions) {
|
|
if (value == option.value) return option.name;
|
|
}
|
|
for (const auto &unit : unitOptions) {
|
|
if (unit.valid(value)) return unit.toString(value);
|
|
}
|
|
if (unitOptions.empty()) return std::to_string(value);
|
|
return unitOptions[0].toString(value);
|
|
}
|
|
void toString(double value, std::string &valueString, std::string &unitString) const {
|
|
for (auto &option : exactOptions) {
|
|
if (value == option.value) {
|
|
valueString = option.name;
|
|
unitString = "";
|
|
return;
|
|
}
|
|
}
|
|
for (const auto &unit : unitOptions) {
|
|
if (unit.valid(value)) return unit.toString(value, valueString, unitString);
|
|
}
|
|
if (unitOptions.empty()) {
|
|
valueString = std::to_string(value);
|
|
unitString = "";
|
|
}
|
|
unitOptions[0].toString(value, valueString, unitString);
|
|
}
|
|
double fromString(const std::string &str) const {
|
|
bool hasDecimal = false, hasDigit = false;;
|
|
{ // check for a parseable number
|
|
size_t pos = 0;
|
|
while (str[pos] == '\t' || str[pos] == ' ') ++pos; // strip leading whitespace
|
|
if (str[pos] == '-') ++pos;
|
|
while (pos < str.length()) {
|
|
if (!hasDecimal && (str[pos] == '.' || str[pos] == ',')) {
|
|
hasDecimal = true;
|
|
} else if (str[pos] >= '0' && str[pos] <= '9') {
|
|
hasDigit = true;
|
|
} else {
|
|
break;
|
|
}
|
|
++pos;
|
|
}
|
|
}
|
|
// It's not a number - look for an exact match
|
|
if (!hasDigit) {
|
|
int longestMatch = -1;
|
|
double result = defaultValue;
|
|
for (auto &option : exactOptions) {
|
|
for (size_t i = 0; i < option.name.size() && i < str.size(); ++i) {
|
|
if (option.name[i] == str[i]) {
|
|
if (int(i) > longestMatch) {
|
|
longestMatch = i;
|
|
result = option.value;
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
size_t pos = 0;
|
|
double result = std::stod(str, 0);
|
|
// Skip whitespace after the number
|
|
while (pos < str.length() && (str[pos] == ' ' || str[pos] == '\t')) ++pos;
|
|
size_t end = str.length(); // and at the end
|
|
while (end > pos && end > 0 && (str[end - 1] == ' ' || str[end - 1] == '\t')) --end;
|
|
std::string unit = str.substr(pos, end - pos);
|
|
|
|
for (const auto &u : unitOptions) {
|
|
if (u.unit == unit) {
|
|
return u.fromDisplay(result);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::vector<std::string> getUnits() const {
|
|
std::vector<std::string> result;
|
|
for (size_t i = 0; i < unitOptions.size(); ++i) {
|
|
bool duplicate = false;
|
|
for (size_t j = 0; j < i; ++j) {
|
|
if (unitOptions[i].unit == unitOptions[j].unit) {
|
|
duplicate = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!duplicate) result.push_back(unitOptions[i].unit);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
double toUnit(double value) const {
|
|
return rangeMap.toUnit(value);
|
|
}
|
|
double fromUnit(double unit) const {
|
|
return rangeMap.fromUnit(unit);
|
|
}
|
|
|
|
private:
|
|
static constexpr double doubleLowest = std::numeric_limits<double>::lowest();
|
|
static constexpr double doubleMax = std::numeric_limits<double>::max();
|
|
static double identityMap(double v) {
|
|
return v;
|
|
}
|
|
|
|
// A map from [0, 1] to another range with specified midpoint, based on a 1/x curve
|
|
UnitRangeMap rangeMap;
|
|
|
|
protected:
|
|
struct ExactEntry {
|
|
double value;
|
|
std::string name;
|
|
};
|
|
std::vector<ExactEntry> exactOptions;
|
|
struct UnitEntry {
|
|
std::string fixedDisplay = "";
|
|
std::string unit;
|
|
bool addSpace = false, useFixed = false, keepZeros = false;
|
|
DoubleMap fromDisplay, toDisplay;
|
|
double validLow, validHigh;
|
|
int precision;
|
|
double precisionOffset = 0;
|
|
|
|
UnitEntry(std::string unit, DoubleMap fromDisplay, DoubleMap toDisplay, double validLow, double validHigh, int precision=2) : unit(unit), fromDisplay(fromDisplay), toDisplay(toDisplay), validLow(validLow), validHigh(validHigh), precision(precision) {
|
|
if (validLow > validHigh) {
|
|
std::swap(validLow, validHigh);
|
|
keepZeros = true;
|
|
}
|
|
if (unit[0] == ' ') {
|
|
addSpace = true;
|
|
this->unit = unit.substr(1, unit.size() - 1);
|
|
}
|
|
precisionOffset = 0.4999*std::pow(10, -precision);
|
|
}
|
|
|
|
bool valid(double value) const {
|
|
return value >= validLow && value <= validHigh;
|
|
}
|
|
|
|
void toString(double value, std::string &valueString, std::string &unitString) const {
|
|
std::ostringstream oss;
|
|
oss.precision(precision);
|
|
oss << std::fixed;
|
|
double offset = 0;
|
|
if (precision > 0) {
|
|
oss << toDisplay(value) + offset;
|
|
} else {
|
|
oss << (int)(toDisplay(value) + offset);
|
|
}
|
|
valueString = oss.str();
|
|
// Strip trailing zeroes
|
|
if (!keepZeros) for (int i = 0; i < (int)valueString.size(); ++i) {
|
|
if (valueString[i] == '.') {
|
|
int zeros = valueString.size();
|
|
while (zeros > i && valueString[zeros - 1] == '0') --zeros;
|
|
if (zeros == i + 1) --zeros;
|
|
valueString = valueString.substr(0, zeros);
|
|
break;
|
|
}
|
|
}
|
|
unitString = unit;
|
|
}
|
|
std::string toString(double value) const {
|
|
std::string valueString, unitString;
|
|
toString(value, valueString, unitString);
|
|
return valueString + (addSpace ? " " : "") + unitString;
|
|
}
|
|
};
|
|
std::vector<UnitEntry> unitOptions;
|
|
};
|
|
|
|
struct SteppedParamInfo {
|
|
int defaultValue;
|
|
int low, high;
|
|
std::string name, description;
|
|
|
|
SteppedParamInfo(int defaultValue) : defaultValue(defaultValue), low(defaultValue), high(defaultValue), nameIndex(defaultValue) {}
|
|
SteppedParamInfo(const SteppedParamInfo &other) = delete;
|
|
SteppedParamInfo(SteppedParamInfo &&other) = default;
|
|
|
|
SteppedParamInfo & info(std::string pName, std::string pDescription) {
|
|
name = pName;
|
|
description = pDescription;
|
|
return *this;
|
|
}
|
|
SteppedParamInfo & range(int pLow, int pHigh) {
|
|
low = pLow;
|
|
high = pHigh;
|
|
return *this;
|
|
}
|
|
|
|
SteppedParamInfo & label(const char *n) {
|
|
if (nameIndex > high && high >= low) high = nameIndex;
|
|
if (nameIndex > low && low > high) low = nameIndex;
|
|
nameMap[nameIndex] = n;
|
|
valueMap[n] = nameIndex;
|
|
return *this;
|
|
}
|
|
bool fullyLabelled() const {
|
|
return int(nameMap.size()) == (high - low + 1);
|
|
}
|
|
|
|
template<class ...Args>
|
|
SteppedParamInfo & label(const char *first, Args... others) {
|
|
label(first);
|
|
++nameIndex;
|
|
return label(others...);
|
|
}
|
|
|
|
template<class ...Args>
|
|
SteppedParamInfo & label(int start, const char *first, Args... others) {
|
|
nameIndex = start;
|
|
return label(first, others...);
|
|
}
|
|
|
|
const std::string * getLabel(int value) const {
|
|
auto pair = nameMap.find(value);
|
|
if (pair != nameMap.end()) {
|
|
return &pair->second;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
std::string toString(int value) const {
|
|
const std::string *maybeLabel = getLabel(value);
|
|
if (maybeLabel) return *maybeLabel;
|
|
return std::to_string(value);
|
|
}
|
|
int fromString(const std::string &str) const {
|
|
auto pair = valueMap.find(str);
|
|
if (pair != valueMap.end()) return pair->second;
|
|
|
|
size_t pos = 0;
|
|
if (str[0] == '-') ++pos;
|
|
while (pos < str.length() && str[pos] >= '0' && str[pos] <= '9') ++pos;
|
|
if (pos >= str.length() || pos == 0 || (pos == 1 && str[0] == '-')) {
|
|
return defaultValue;
|
|
}
|
|
int result = std::stoi(str, &pos);
|
|
if (high >= low) {
|
|
return std::max<int>(std::min<int>(high, result), low);
|
|
} else {
|
|
return std::max<int>(std::min<int>(low, result), high);
|
|
}
|
|
}
|
|
|
|
private:
|
|
int nameIndex;
|
|
protected:
|
|
std::map<int, std::string> nameMap;
|
|
std::map<std::string, int> valueMap;
|
|
};
|
|
|
|
} // namespace
|
|
#endif // include guard
|