#ifndef SIGNALSMITH_STFX_STFX2_PARAM_INFO_H #define SIGNALSMITH_STFX_STFX2_PARAM_INFO_H #include #include #include #include #include #include namespace stfx { 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 fromUnit=identityMap, DoubleMap toUnit=identityMap, double validLow=doubleLowest, double validHigh=doubleMax) { unitOptions.emplace_back(suffix, fromUnit, toUnit, validLow, validHigh, precision); return *this; } RangeParamInfo & unit(std::string suffix, DoubleMap fromUnit, DoubleMap toUnit, double validLow=doubleLowest, double validHigh=doubleMax) { return unit(suffix, 2, fromUnit, toUnit, 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.fromUnit(result); } } return result; } std::vector getUnits() const { std::vector 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.toUnitRange(value); } double fromUnit(double unit) const { return rangeMap.fromUnitRange(unit); } private: static constexpr double doubleLowest = std::numeric_limits::lowest(); static constexpr double doubleMax = std::numeric_limits::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 class UnitRangeMapReciprocal { double vMin, vTopFactor, vBottomFactor; public: UnitRangeMapReciprocal() : vMin(0), vTopFactor(1), vBottomFactor(0) {} // identity UnitRangeMapReciprocal(double min, double mid, double max) { vMin = min; double k = (mid - min)/(max - mid); vTopFactor = max*k - min; vBottomFactor = k - 1; } double toUnitRange(double value) const { return (value - vMin)/(vTopFactor - value*vBottomFactor); } double fromUnitRange(double unit) const { return (vMin + unit*vTopFactor)/(1 + unit*vBottomFactor); } }; UnitRangeMapReciprocal rangeMap; protected: struct ExactEntry { double value; std::string name; }; std::vector exactOptions; struct UnitEntry { std::string fixedDisplay = ""; std::string unit; bool addSpace = false, useFixed = false, keepZeros = false; DoubleMap fromUnit, toUnit; double validLow, validHigh; int precision; double precisionOffset = 0; UnitEntry(std::string unit, DoubleMap fromUnit, DoubleMap toUnit, double validLow, double validHigh, int precision=2) : unit(unit), fromUnit(fromUnit), toUnit(toUnit), 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 << toUnit(value) + offset; } else { oss << (int)(toUnit(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 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 SteppedParamInfo & label(const char *first, Args... others) { label(first); ++nameIndex; return label(others...); } template 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(std::min(high, result), low); } else { return std::max(std::min(low, result), high); } } double toUnit(int value) const { return (value - low)/double(high - low); } int fromUnit(double unit) const { double value = low + unit*(high - low); return int(std::round(value)); } private: int nameIndex; protected: std::map nameMap; std::map valueMap; }; } // namespace #endif // include guard