From 28b888e5a34b9abb761e601d9def86e50cf81f7e Mon Sep 17 00:00:00 2001 From: maruncz Date: Fri, 29 Sep 2023 14:17:24 +0200 Subject: [PATCH] first concept of dps chart --- CMakeLists.txt | 97 +++++++++------- CombatLog/CombatLog.cpp | 49 +++++++- CombatLog/CombatLog.h | 29 +++++ CombatLog/LogLine.cpp | 43 +++++++ CombatLog/LogLine.h | 12 ++ CombatLog/SubEvents.cpp | 4 +- CombatLog/SubEvents.h | 137 ++++++++-------------- CombatLog/defs.h | 6 +- DataModels/ObjectListModel.cpp | 55 +++++++++ DataModels/ObjectListModel.h | 31 +++++ DataModels/healdatamodel.cpp | 90 +++++++++++++++ DataModels/healdatamodel.h | 38 +++++++ filters/SubSampler.cpp | 78 +++++++++++++ filters/SubSampler.h | 36 ++++++ forms/DamagePerSecond.cpp | 202 +++++++++++++++++++++++++++++++++ forms/DamagePerSecond.h | 56 +++++++++ forms/DamagePerSecond.ui | 113 ++++++++++++++++++ forms/SourceSelector.cpp | 14 +++ forms/SourceSelector.h | 24 ++++ forms/SourceSelector.ui | 29 +++++ mainwindow.cpp | 144 +++++++++++++++++++++-- mainwindow.h | 13 +++ mainwindow.ui | 58 +++++++++- tests/CMakeLists.txt | 17 +++ tests/SubSamplerTest.cpp | 114 +++++++++++++++++++ 25 files changed, 1344 insertions(+), 145 deletions(-) create mode 100644 DataModels/ObjectListModel.cpp create mode 100644 DataModels/ObjectListModel.h create mode 100644 DataModels/healdatamodel.cpp create mode 100644 DataModels/healdatamodel.h create mode 100644 filters/SubSampler.cpp create mode 100644 filters/SubSampler.h create mode 100644 forms/DamagePerSecond.cpp create mode 100644 forms/DamagePerSecond.h create mode 100644 forms/DamagePerSecond.ui create mode 100644 forms/SourceSelector.cpp create mode 100644 forms/SourceSelector.h create mode 100644 forms/SourceSelector.ui create mode 100644 tests/CMakeLists.txt create mode 100644 tests/SubSamplerTest.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 8dd2e6d..0d2116e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,53 +9,68 @@ set(CMAKE_AUTORCC ON) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets) -find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets) +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +find_package(Qt6 REQUIRED COMPONENTS Widgets Charts) + +option(BUILD_TESTS OFF) + + + +add_library(CombatLog STATIC + CombatLog/LogLine.h CombatLog/LogLine.cpp + CombatLog/defs.h + CombatLog/SubEvents.h CombatLog/SubEvents.cpp + CombatLog/LineParser.h CombatLog/LineParser.cpp + CombatLog/exceptions.h CombatLog/exceptions.cpp + CombatLog/CombatLog.h CombatLog/CombatLog.cpp +) + +target_link_libraries(CombatLog PUBLIC Qt6::Core) + +add_library(DataModels STATIC + DataModels/healdatamodel.h DataModels/healdatamodel.cpp + DataModels/ObjectListModel.h DataModels/ObjectListModel.cpp +) + +target_link_libraries(DataModels + PUBLIC Qt6::Core + PRIVATE CombatLog) + +add_library(filters STATIC + filters/SubSampler.h filters/SubSampler.cpp +) + +target_link_libraries(filters PUBLIC Qt6::Core) + +add_library(forms STATIC + forms/DamagePerSecond.h forms/DamagePerSecond.cpp forms/DamagePerSecond.ui + forms/SourceSelector.h forms/SourceSelector.cpp forms/SourceSelector.ui +) + +target_link_libraries(forms PUBLIC Qt6::Widgets Qt6::Charts + PRIVATE DataModels filters) set(PROJECT_SOURCES - main.cpp - mainwindow.cpp - mainwindow.h - mainwindow.ui - CombatLog/LogLine.h CombatLog/LogLine.cpp + main.cpp + mainwindow.cpp + mainwindow.h + mainwindow.ui ) -if(${QT_VERSION_MAJOR} GREATER_EQUAL 6) - qt_add_executable(WoWCombatAnalyzer - MANUAL_FINALIZATION - ${PROJECT_SOURCES} - CombatLog/defs.h - CombatLog/SubEvents.h CombatLog/SubEvents.cpp - CombatLog/LineParser.h CombatLog/LineParser.cpp - CombatLog/exceptions.h CombatLog/exceptions.cpp - CombatLog/CombatLog.h CombatLog/CombatLog.cpp - - ) -# Define target properties for Android with Qt 6 as: -# set_property(TARGET WoWCombatAnalyzer APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR -# ${CMAKE_CURRENT_SOURCE_DIR}/android) -# For more information, see https://doc.qt.io/qt-6/qt-add-executable.html#target-creation -else() - if(ANDROID) - add_library(WoWCombatAnalyzer SHARED - ${PROJECT_SOURCES} - ) -# Define properties for Android with Qt 5 after find_package() calls as: -# set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android") - else() - add_executable(WoWCombatAnalyzer - ${PROJECT_SOURCES} - ) - endif() -endif() -target_link_libraries(WoWCombatAnalyzer PRIVATE Qt${QT_VERSION_MAJOR}::Widgets) +qt_add_executable(WoWCombatAnalyzer + MANUAL_FINALIZATION + ${PROJECT_SOURCES} +) + +target_link_libraries(WoWCombatAnalyzer PRIVATE Qt6::Widgets Qt6::Charts CombatLog DataModels filters forms) # Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1. # If you are developing for iOS or macOS you should consider setting an # explicit, fixed bundle identifier manually though. -if(${QT_VERSION} VERSION_LESS 6.1.0) - set(BUNDLE_ID_OPTION MACOSX_BUNDLE_GUI_IDENTIFIER com.example.WoWCombatAnalyzer) +if("${QT_VERSION}" VERSION_LESS 6.1.0) + set(BUNDLE_ID_OPTION MACOSX_BUNDLE_GUI_IDENTIFIER com.example.WoWCombatAnalyzer) endif() set_target_properties(WoWCombatAnalyzer PROPERTIES ${BUNDLE_ID_OPTION} @@ -72,6 +87,8 @@ install(TARGETS WoWCombatAnalyzer RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} ) -if(QT_VERSION_MAJOR EQUAL 6) - qt_finalize_executable(WoWCombatAnalyzer) +qt_finalize_executable(WoWCombatAnalyzer) + +if("${BUILD_TESTS}") + add_subdirectory(tests) endif() diff --git a/CombatLog/CombatLog.cpp b/CombatLog/CombatLog.cpp index 933e71e..1114941 100644 --- a/CombatLog/CombatLog.cpp +++ b/CombatLog/CombatLog.cpp @@ -22,9 +22,54 @@ CombatLog CombatLog::fromFile(QString filename) throw CombatLogParserException(QString("Line too long: %1").arg(line.size()).toStdString()); } - ret.lines.append(LogLine::fromRawData(line)); + ret.append(LogLine::fromRawData(line)); } - + ret.finalize(); return ret; } + +const QList &CombatLog::getLines() const +{ + return lines; +} + +void CombatLog::append(LogLine line) +{ + lines.append(line); + + { + auto sourceName = line.getSourceObject().name; + if(!sourceNames.contains(sourceName)) + { + sourceNames.append(sourceName); + } + } + { + auto targetName = line.getDestObject().name; + if(!targetNames.contains(targetName)) + { + targetNames.append(targetName); + } + } +} + +void CombatLog::finalize() +{ + sourceNames.sort(); + targetNames.sort(); +} + + + +const QStringList& CombatLog::getTargetNames() const +{ + return targetNames; +} + + + +const QStringList& CombatLog::getSourceNames() const +{ + return sourceNames; +} diff --git a/CombatLog/CombatLog.h b/CombatLog/CombatLog.h index fcf7eca..6667fb0 100644 --- a/CombatLog/CombatLog.h +++ b/CombatLog/CombatLog.h @@ -2,7 +2,9 @@ #define COMBATLOG_H #include +#include #include "LogLine.h" +#include class CombatLog { @@ -10,8 +12,35 @@ class CombatLog [[nodiscard]] static CombatLog fromFile(QString filename); + [[nodiscard]] const QList& getLines() const; + + [[nodiscard]] const QStringList& getSourceNames() const; + + [[nodiscard]] const QStringList& getTargetNames() const; + + template + [[nodiscard]] CombatLog filter(const T& filt) const + { + CombatLog ret; + for(const auto &e : lines) + { + if(filt(e)) + { + ret.append(e); + } + } + ret.finalize(); + return ret; + } + private: + void append(LogLine line); + void finalize(); + + QList lines; + QStringList sourceNames; + QStringList targetNames; }; #endif // COMBATLOG_H diff --git a/CombatLog/LogLine.cpp b/CombatLog/LogLine.cpp index f2300bb..88993e9 100644 --- a/CombatLog/LogLine.cpp +++ b/CombatLog/LogLine.cpp @@ -154,6 +154,18 @@ SubEvent LogLine::subeventTypeFromString(QString s) { return SubEvent::SPELL_PERIODIC_LEECH; } + else if(s == "SPELL_CREATE") + { + return SubEvent::SPELL_CREATE; + } + else if(s == "RANGE_DAMAGE") + { + return SubEvent::RANGE_DAMAGE; + } + else if(s == "SPELL_INSTAKILL") + { + return SubEvent::SPELL_INSTAKILL; + } throw CombatLogParserException(QString("unknown subEvent: %1").arg(s).toStdString()); @@ -214,6 +226,37 @@ variant_t LogLine::subeventValueFromString(SubEvent type, QStringList list) return {DamageShieldMissed{list}}; case SubEvent::SPELL_PERIODIC_LEECH: return {SpellPeriodicLeech{list}}; + case SubEvent::SPELL_CREATE: + return {SpellCreate{list}}; + case SubEvent::RANGE_DAMAGE: + return {RangeDamage{list}}; + case SubEvent::SPELL_INSTAKILL: + return {SpellInstakill{list}}; } throw CombatLogParserException(QString("unhandled subevent: %1").arg(list.at(0)).toStdString()); } + +variant_t LogLine::getSubEventValue() const +{ + return subEventValue; +} + +SubEvent LogLine::getSubeventType() const +{ + return subeventType; +} + +Object LogLine::getDestObject() const +{ + return destObject; +} + +Object LogLine::getSourceObject() const +{ + return sourceObject; +} + +QDateTime LogLine::getTimestamp() const +{ + return timestamp; +} diff --git a/CombatLog/LogLine.h b/CombatLog/LogLine.h index 64b76bc..83afddf 100644 --- a/CombatLog/LogLine.h +++ b/CombatLog/LogLine.h @@ -11,6 +11,18 @@ class LogLine public: [[nodiscard]] static LogLine fromRawData(QString s); + [[nodiscard]] bool filter() const; + + QDateTime getTimestamp() const; + + Object getSourceObject() const; + + Object getDestObject() const; + + SubEvent getSubeventType() const; + + variant_t getSubEventValue() const; + private: [[nodiscard]] static QStringList parseTokens(QString s); [[nodiscard]] static QDateTime parseTimestamp(QString s); diff --git a/CombatLog/SubEvents.cpp b/CombatLog/SubEvents.cpp index 2775855..2f3e272 100644 --- a/CombatLog/SubEvents.cpp +++ b/CombatLog/SubEvents.cpp @@ -4,10 +4,10 @@ UnitFlags UnitFlags::fromNum(uint32_t n) { - static constexpr uint32_t unusedBits{Reaction::unused | Type::unused | Special::unused}; + static constexpr uint32_t unusedBits{Reaction::unused | Type::unused}; if(n & unusedBits) { - throw CombatLogParserException(QString("undefined bits set: %1").arg(n & unusedBits, 8,16).toStdString()); + throw CombatLogParserException(QString("undefined bits set: 0x%1").arg(n & unusedBits, 8,16).toStdString()); } UnitFlags ret; diff --git a/CombatLog/SubEvents.h b/CombatLog/SubEvents.h index 550ca30..4c0b373 100644 --- a/CombatLog/SubEvents.h +++ b/CombatLog/SubEvents.h @@ -54,10 +54,6 @@ struct UnitFlags static constexpr uint32_t none{0x80000000}; static constexpr uint32_t mask{0xFFFF0000}; - - static constexpr uint32_t unknownArthasCullongOfStratholme{0x400000}; - - static constexpr uint32_t unused{0x7FB00000}; }; @@ -111,13 +107,11 @@ namespace detail namespace prefix { -class Swing{}; +struct Swing{}; -class Range +struct Range { -public: Range(QStringList list) : spell{list} {} -private: Spell spell; }; @@ -125,7 +119,7 @@ using Spell = Range; using SpellPeriodic = Range; using SpellBuilding = Range; -class Environmental +struct Environmental { EnvironmentalType type; }; @@ -135,11 +129,9 @@ class Environmental namespace suffix { -class Damage +struct Damage { -public: Damage(QStringList list); -private: uint32_t amount; uint32_t overkill; SpellSchool spelschool; @@ -152,29 +144,25 @@ class Damage //bool isOffHand; }; -class Missed +struct Missed { -public: Missed(QStringList list); -private: MissType type; //bool isOffHand; uint32_t amountMissed{0}; //bool critical; }; -class Heal +struct Heal { -public: Heal(QStringList list); -private: uint32_t amount; uint32_t overhealing; uint32_t absorbed; bool critical; }; -class HealAbsorbed +struct HealAbsorbed { Object extra; Spell extraSpell; @@ -182,24 +170,20 @@ class HealAbsorbed uint32_t totalAmount; }; -class Absorbed{}; +struct Absorbed{}; -class Energize +struct Energize { -public: Energize(QStringList list); -private: uint32_t amount; //uint32_t overEnergize; PowerType type; //uint32_t maxPower; }; -class Drain +struct Drain { -public: Drain(QStringList list); -private: uint32_t amount; PowerType type; uint32_t extraAmount; @@ -208,19 +192,15 @@ class Drain using Leech = Drain; -class Interrupt +struct Interrupt { -public: Interrupt(QStringList list); -private: Spell extraSpell; }; -class Dispell +struct Dispell { -public: Dispell(QStringList list); -private: Spell extraSpell; AuraType aura; }; @@ -228,26 +208,22 @@ class Dispell using DispellFailed = Interrupt; using Stolen = Dispell; -class ExtraAttacks +struct ExtraAttacks { uint32_t amount; }; -class AuraApplied +struct AuraApplied { -public: AuraApplied(QStringList list); -private: AuraType type; //int amount; }; using AuraRemoved = AuraApplied; -class AuraAppliedDose : public AuraApplied +struct AuraAppliedDose : public AuraApplied { -public: AuraAppliedDose(QStringList list); -private: uint32_t amount; }; @@ -264,7 +240,7 @@ class AuraRefresh using AuraBroken = AuraRefresh; -class AuraBrokenSpell +struct AuraBrokenSpell { Spell extra; AuraType type; @@ -272,11 +248,9 @@ class AuraBrokenSpell using CastStart = Absorbed; using CastSucces = Absorbed; -class CastFailed +struct CastFailed { -public: CastFailed(QStringList list); -private: QString type; }; @@ -286,6 +260,8 @@ using DurabilityDamageAll = Absorbed; using Create = Absorbed; using Summon = Absorbed; using Resurrect = Absorbed; +using Instakill = Absorbed; + } @@ -299,78 +275,67 @@ using Resurrect = Absorbed; -class SpellCastSucces : public detail::prefix::Spell, public detail::suffix::CastSucces +struct SpellCastSucces : public detail::prefix::Spell, public detail::suffix::CastSucces { -public: SpellCastSucces(QStringList list) : detail::prefix::Spell{list} {} }; -class SpellDamage : public detail::prefix::Spell, public detail::suffix::Damage +struct SpellDamage : public detail::prefix::Spell, public detail::suffix::Damage { -public: SpellDamage(QStringList list) : detail::prefix::Spell{list.mid(0,3)} , detail::suffix::Damage{list.mid(3)} {} }; -class SpellPeriodicDamage : public detail::prefix::SpellPeriodic, public detail::suffix::Damage +struct SpellPeriodicDamage : public detail::prefix::SpellPeriodic, public detail::suffix::Damage { -public: SpellPeriodicDamage(QStringList list) : detail::prefix::SpellPeriodic{list.mid(0,3)} , detail::suffix::Damage{list.mid(3)} {} }; -class SpellAuraApplied : public detail::prefix::Spell , public detail::suffix::AuraApplied +struct SpellAuraApplied : public detail::prefix::Spell , public detail::suffix::AuraApplied { -public: SpellAuraApplied(QStringList list) : detail::prefix::Spell{list.mid(0,3)} , detail::suffix::AuraApplied{list.mid(3)} {} }; -class SpellAuraRemoved : public detail::prefix::Spell , public detail::suffix::AuraRemoved +struct SpellAuraRemoved : public detail::prefix::Spell , public detail::suffix::AuraRemoved { -public: SpellAuraRemoved(QStringList list) : detail::prefix::Spell{list.mid(0,3)} , detail::suffix::AuraRemoved{list.mid(3)} {} }; -class SpellAuraRefresh : public detail::prefix::Spell , public detail::suffix::AuraRefresh +struct SpellAuraRefresh : public detail::prefix::Spell , public detail::suffix::AuraRefresh { -public: SpellAuraRefresh(QStringList list) : detail::prefix::Spell{list.mid(0,3)} , detail::suffix::AuraRefresh{list.mid(3)} {} }; -class SpellEnergize : public detail::prefix::Spell, public detail::suffix::Energize +struct SpellEnergize : public detail::prefix::Spell, public detail::suffix::Energize { -public: SpellEnergize(QStringList list) : detail::prefix::Spell{list.mid(0,3)} , detail::suffix::Energize{list.mid(3)} {} }; -class SwingDamage : public detail::prefix::Swing, public detail::suffix::Damage +struct SwingDamage : public detail::prefix::Swing, public detail::suffix::Damage { -public: SwingDamage(QStringList list) : detail::prefix::Swing{} , detail::suffix::Damage{list} {} }; -class SpellPeriodicHeal : public detail::prefix::SpellPeriodic, public detail::suffix::Heal +struct SpellPeriodicHeal : public detail::prefix::SpellPeriodic, public detail::suffix::Heal { -public: SpellPeriodicHeal(QStringList list) : detail::prefix::SpellPeriodic{list.mid(0,3)} , detail::suffix::Heal{list.mid(3)} {} }; -class SpellAuraAppliedDose : public detail::prefix::Spell , public detail::suffix::AuraAppliedDose +struct SpellAuraAppliedDose : public detail::prefix::Spell , public detail::suffix::AuraAppliedDose { -public: SpellAuraAppliedDose(QStringList list) : detail::prefix::Spell{list.mid(0,3)} , detail::suffix::AuraAppliedDose{list.mid(3)} {} }; -class SpellCastStart : public detail::prefix::Spell, public detail::suffix::CastStart +struct SpellCastStart : public detail::prefix::Spell, public detail::suffix::CastStart { -public: SpellCastStart(QStringList list) : detail::prefix::Spell{list} { @@ -378,38 +343,33 @@ class SpellCastStart : public detail::prefix::Spell, public detail::suffix::Cast }; -class SpellPeriodicEnergize : public detail::prefix::SpellPeriodic, public detail::suffix::Energize +struct SpellPeriodicEnergize : public detail::prefix::SpellPeriodic, public detail::suffix::Energize { -public: SpellPeriodicEnergize(QStringList list) : detail::prefix::SpellPeriodic{list.mid(0,3)} , detail::suffix::Energize{list.mid(3)} {} }; -class SpellHeal : public detail::prefix::Spell, public detail::suffix::Heal +struct SpellHeal : public detail::prefix::Spell, public detail::suffix::Heal { -public: SpellHeal(QStringList list) : detail::prefix::Spell{list.mid(0,3)} , detail::suffix::Heal{list.mid(3)} {} }; -class SwingMissed : public detail::prefix::Swing, public detail::suffix::Missed +struct SwingMissed : public detail::prefix::Swing, public detail::suffix::Missed { -public: SwingMissed(QStringList list) : detail::prefix::Swing{}, detail::suffix::Missed{list} { } }; -class PartyKill +struct PartyKill { -public: PartyKill(QStringList list); }; -class SpellCastFailed : public detail::prefix::Spell, public detail::suffix::CastFailed +struct SpellCastFailed : public detail::prefix::Spell, public detail::suffix::CastFailed { -public: SpellCastFailed(QStringList list) : detail::prefix::Spell{list.mid(0,3)}, detail::suffix::CastFailed{list.mid(3)} { @@ -417,43 +377,37 @@ class SpellCastFailed : public detail::prefix::Spell, public detail::suffix::Cas }; -class EnchantApplied +struct EnchantApplied { -public: EnchantApplied(QStringList list); -private: QString name; Item item; }; -class SpellMissed : public detail::prefix::Spell, public detail::suffix::Missed +struct SpellMissed : public detail::prefix::Spell, public detail::suffix::Missed { -public: SpellMissed(QStringList list) : detail::prefix::Spell{list.mid(0,3)}, detail::suffix::Missed{list.mid(3)} { } }; -class SpellInterrupt : public detail::prefix::Spell, public detail::suffix::Interrupt +struct SpellInterrupt : public detail::prefix::Spell, public detail::suffix::Interrupt { -public: SpellInterrupt(QStringList list) : detail::prefix::Spell{list.mid(0,3)}, detail::suffix::Interrupt{list.mid(3)} { } }; -class SpellPeriodicMissed : public detail::prefix::SpellPeriodic, public detail::suffix::Missed +struct SpellPeriodicMissed : public detail::prefix::SpellPeriodic, public detail::suffix::Missed { -public: SpellPeriodicMissed(QStringList list) : detail::prefix::SpellPeriodic{list.mid(0,3)} , detail::suffix::Missed{list.mid(3)} {} }; -class SpellDispel : public detail::prefix::Spell, public detail::suffix::Dispell +struct SpellDispel : public detail::prefix::Spell, public detail::suffix::Dispell { -public: SpellDispel(QStringList list) : detail::prefix::Spell{list.mid(0,3)}, detail::suffix::Dispell{list.mid(3)} { @@ -461,18 +415,16 @@ class SpellDispel : public detail::prefix::Spell, public detail::suffix::Dispell }; -class SpellSummon : public detail::prefix::Spell, public detail::suffix::Summon +struct SpellSummon : public detail::prefix::Spell, public detail::suffix::Summon { -public: SpellSummon(QStringList list) : detail::prefix::Spell{list} , detail::suffix::Summon{} { } }; -class SpellPeriodicLeech : public detail::prefix::SpellPeriodic, public detail::suffix::Leech +struct SpellPeriodicLeech : public detail::prefix::SpellPeriodic, public detail::suffix::Leech { -public: SpellPeriodicLeech(QStringList list) : detail::prefix::SpellPeriodic{list.mid(0,3)} , detail::suffix::Leech{list.mid(3)} { @@ -480,6 +432,8 @@ class SpellPeriodicLeech : public detail::prefix::SpellPeriodic, public detail:: }; + + using variant_t = std::variant #include - enum class SubEvent { SPELL_CAST_SUCCESS, @@ -32,7 +31,10 @@ SPELL_CAST_SUCCESS, SPELL_DISPEL, SPELL_SUMMON, DAMAGE_SHIELD_MISSED, - SPELL_PERIODIC_LEECH + SPELL_PERIODIC_LEECH, + SPELL_CREATE, + RANGE_DAMAGE, + SPELL_INSTAKILL }; #if 0 diff --git a/DataModels/ObjectListModel.cpp b/DataModels/ObjectListModel.cpp new file mode 100644 index 0000000..1305082 --- /dev/null +++ b/DataModels/ObjectListModel.cpp @@ -0,0 +1,55 @@ +#include "ObjectListModel.h" + +ObjectListModel::ObjectListModel(QObject *parent) + : QAbstractListModel(parent) +{ + +} + +int ObjectListModel::rowCount(const QModelIndex &parent) const +{ + // For list models only the root node (an invalid parent) should return the list's size. For all + // other (valid) parents, rowCount() should return 0 so that it does not become a tree model. + if (parent.isValid()) + return 0; + + if(!list) + { + return 0; + } + + return list->size(); +} + +QVariant ObjectListModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if(!list) + { + return {}; + } + + + if(role == Qt::DisplayRole) + { + return list->at(index.row()); + } + + + + + return {}; +} + +void ObjectListModel::setList(const QStringList *newList) +{ + auto oldRows{0}; + if(list) + { + oldRows = list->size(); + } + list = newList; + emit dataChanged(QAbstractItemModel::createIndex(0,0), QAbstractItemModel::createIndex(oldRows,0)); +} diff --git a/DataModels/ObjectListModel.h b/DataModels/ObjectListModel.h new file mode 100644 index 0000000..5d823c6 --- /dev/null +++ b/DataModels/ObjectListModel.h @@ -0,0 +1,31 @@ +#ifndef OBJECTLISTMODEL_H +#define OBJECTLISTMODEL_H + +#include +#include +#include + + +class ObjectListModel : public QAbstractListModel +{ + Q_OBJECT + +public: + explicit ObjectListModel(QObject *parent = nullptr); + + // Basic functionality: + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + void setList(const QStringList *newList); + +private: + + + + + const QStringList *list{nullptr}; +}; + +#endif // OBJECTLISTMODEL_H diff --git a/DataModels/healdatamodel.cpp b/DataModels/healdatamodel.cpp new file mode 100644 index 0000000..5783859 --- /dev/null +++ b/DataModels/healdatamodel.cpp @@ -0,0 +1,90 @@ +#include "healdatamodel.h" +#include "../CombatLog/CombatLog.h" + +HealDataModel::HealDataModel(CombatLog *log, QObject *parent) + : QAbstractTableModel(parent), combatlog{log} +{ +} + +QVariant HealDataModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if((role == Qt::DisplayRole) && (orientation == Qt::Horizontal)) + { + return QString::fromStdString(std::string{header.at(section)}); + } + + return {}; +} + +int HealDataModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + + return combatlog->getLines().size(); +} + +int HealDataModel::columnCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + + return header.size(); +} + +QVariant HealDataModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if(role == Qt::DisplayRole) + { + const auto& line = combatlog->getLines().at(index.row()); + switch(index.column()) + { + case 0: + { + return line.getTimestamp(); + } + case 1: + { + return line.getSourceObject().name; + } + case 2: + { + return line.getDestObject().name; + } + case 3: + { + return std::visit([](auto &&arg) -> QString { + using T = std::decay_t; + if constexpr (std::is_base_of_v) + { + return arg.spell.name; + } + else + { + throw std::runtime_error("unhandled case"); + } + },line.getSubEventValue()); + } + case 4: + { + return std::visit([](auto &&arg) -> uint32_t { + using T = std::decay_t; + if constexpr (std::is_base_of_v) + { + return arg.amount; + } + else + { + throw std::runtime_error("unhandled case"); + } + },line.getSubEventValue()); + } + } + } + + + return QVariant(); +} diff --git a/DataModels/healdatamodel.h b/DataModels/healdatamodel.h new file mode 100644 index 0000000..7ffef24 --- /dev/null +++ b/DataModels/healdatamodel.h @@ -0,0 +1,38 @@ +#ifndef HEALDATAMODEL_H +#define HEALDATAMODEL_H + +#include +#include +#include +#include + +class LogLine; +class CombatLog; + +class HealDataModel : public QAbstractTableModel +{ + Q_OBJECT + +public: + explicit HealDataModel(CombatLog * log, QObject *parent = nullptr); + + // Header: + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + + // Basic functionality: + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + +private: + static constexpr auto header = std::to_array({"Timestamp" + , "Source" + , "Target" + , "Spell", + "Amount"}); + + CombatLog *combatlog{nullptr}; +}; + +#endif // HEALDATAMODEL_H diff --git a/filters/SubSampler.cpp b/filters/SubSampler.cpp new file mode 100644 index 0000000..03d4754 --- /dev/null +++ b/filters/SubSampler.cpp @@ -0,0 +1,78 @@ +#include "SubSampler.h" +#include +#include + +std::optional > > SubSampler::addValue(qreal x, qreal y) +{ + auto timepoint = std::floor(x/samplePeriod) * samplePeriod; + if(timepoint < currentTimepoint) + { + accumulator.append(y); + return std::nullopt; + } + else + { + auto ret = advance(timepoint); + accumulator.append(y); + return ret; + } + return std::nullopt; +} + +std::pair SubSampler::finalize() +{ + std::pair ret; + auto mean = std::reduce(accumulator.begin(), accumulator.end()) / (samplePeriod/1000); + updateMinMaxY(mean); + ret = {currentTimepoint, mean}; + minmaxX.second = currentTimepoint; + return ret; +} + +QList> SubSampler::advance(qreal timepoint) +{ + QList> ret; + + while(currentTimepoint <= timepoint) + { + if(accumulator.empty()) + { + updateMinMaxY(0); + ret.append({currentTimepoint,0}); + } + else + { + auto mean = std::reduce(accumulator.begin(), accumulator.end()) / (samplePeriod/1000); + updateMinMaxY(mean); + ret.append({currentTimepoint, mean}); + } + accumulator.clear(); + currentTimepoint+=samplePeriod; + } + + minmaxX.second = currentTimepoint; + return ret; +} + +void SubSampler::updateMinMaxY(qreal val) +{ + if(val < minmaxY.first) + { + minmaxY.first = val; + } + if(val > minmaxY.second) + { + minmaxY.second = val; + } +} + +std::pair SubSampler::getMinmaxY() const +{ + return minmaxY; +} + +std::pair SubSampler::getMinmaxX() const +{ + return minmaxX; +} + diff --git a/filters/SubSampler.h b/filters/SubSampler.h new file mode 100644 index 0000000..4475bd0 --- /dev/null +++ b/filters/SubSampler.h @@ -0,0 +1,36 @@ +#ifndef SUBSAMPLER_H +#define SUBSAMPLER_H + +#include +#include +#include + +class SubSampler +{ +public: + SubSampler(qreal period, qreal startTime) : samplePeriod(period) + { + currentTimepoint = (std::floor(startTime/samplePeriod) * samplePeriod) + samplePeriod; + minmaxX.first = currentTimepoint; + } + + std::optional>> addValue(qreal x, qreal y); + + std::pair finalize(); + + std::pair getMinmaxX() const; + + std::pair getMinmaxY() const; + +private: + QList> advance(qreal timepoint); + void updateMinMaxY(qreal val); + + qreal samplePeriod; + qreal currentTimepoint{0}; + QList accumulator; + std::pair minmaxX{0,0}; + std::pair minmaxY{0,0}; +}; + +#endif // SUBSAMPLER_H diff --git a/forms/DamagePerSecond.cpp b/forms/DamagePerSecond.cpp new file mode 100644 index 0000000..9b0ae08 --- /dev/null +++ b/forms/DamagePerSecond.cpp @@ -0,0 +1,202 @@ +#include "DamagePerSecond.h" +#include "ui_DamagePerSecond.h" +#include "../CombatLog/CombatLog.h" +#include "../filters/SubSampler.h" +#include +#include +#include + +DamagePerSecond::DamagePerSecond(QWidget *parent) : + QWidget(parent), + ui(new Ui::DamagePerSecond) +{ + ui->setupUi(this); + ui->listSource->setModel(&sourceModel); + ui->listTarget->setModel(&targetModel); + ui->chartView->setChart(&chart); + chart.addAxis(&axisX, Qt::AlignBottom); + chart.addAxis(&axisY, Qt::AlignLeft); + + +} + +DamagePerSecond::~DamagePerSecond() +{ + delete ui; +} + +void DamagePerSecond::updateChart() +{ + if(!originalLog) + { + throw std::runtime_error("log ptr null"); + } + + chart.removeAllSeries(); + + axisX.setRange(0,0); + axisY.setRange(0,0); + + auto selectedSources = ui->listSource->selectionModel()->selectedRows(); + + auto timeOffset = originalLog->getLines().front().getTimestamp().toMSecsSinceEpoch(); + + for(const auto &e : selectedSources) + { + auto sourceName = sourceNamesList.at(e.row()); + + auto filter = [sourceName](const LogLine &l) + { + if(l.getSourceObject().name != sourceName) + { + return false; + } + + if((l.getSubeventType() == SubEvent::SPELL_DAMAGE) + || (l.getSubeventType() == SubEvent::SPELL_PERIODIC_DAMAGE) + || (l.getSubeventType() == SubEvent::SWING_DAMAGE)) + { + return true; + } + return false; + }; + + auto temporaryLog = originalLog->filter(filter); + + + + if(temporaryLog.getLines().empty()) + { + continue; + } + + auto lineOffset = temporaryLog.getLines().front().getTimestamp().toMSecsSinceEpoch() - timeOffset; + + auto line = new QLineSeries(this); + SubSampler sampler{ui->doubleSpinBoxFilter->value()*1000, static_cast(lineOffset)}; + + for(const auto &e : temporaryLog.getLines()) + { +#if 0 + qDebug() << "in:" << e.getTimestamp().toMSecsSinceEpoch() - timeOffset << ',' << std::visit([](auto &&arg) -> qreal { + using T = std::decay_t; + if constexpr (std::is_base_of_v) + { + return arg.amount; + } + else + { + throw std::runtime_error("unhandled case"); + } + },e.getSubEventValue()); +#endif + + auto samples = sampler.addValue(e.getTimestamp().toMSecsSinceEpoch() - timeOffset, + std::visit([](auto &&arg) -> qreal { + using T = std::decay_t; + if constexpr (std::is_base_of_v) + { + return arg.amount; + } + else + { + throw std::runtime_error("unhandled case"); + } + },e.getSubEventValue())); + if(samples.has_value()) + { + for(const auto &sample : samples.value()) + { + line->append(sample.first, sample.second); + } + //qDebug() << "out:" << sample->first << ',' << sample->second; + } + } + + { + auto sample = sampler.finalize(); + line->append(sample.first, sample.second); + //qDebug() << "out:" << sample.first << ',' << sample.second; + adjustYRange(sampler.getMinmaxY().first,sampler.getMinmaxY().second); + adjustXRange(sampler.getMinmaxX().first,sampler.getMinmaxX().second); + } + + + + chart.addSeries(line); + line->attachAxis(&axisX); + line->attachAxis(&axisY); + line->setName(sourceName); + + } + + + + +} + +void DamagePerSecond::adjustXRange(qreal min, qreal max) +{ + auto oldMin = axisX.min(); + if(std::abs(oldMin) < 1) + { + axisX.setMin(min); + } + else if(oldMin > min) + { + axisX.setMin(min); + } + + auto oldMax = axisX.max(); + if(std::abs(oldMax) < 1) + { + axisX.setMax(max); + } + else if(oldMax < max) + { + axisX.setMax(max); + } +} + +void DamagePerSecond::adjustYRange(qreal min, qreal max) +{ + auto oldMin = axisY.min(); + if(oldMin > min) + { + axisY.setMin(min); + } + + auto oldMax = axisY.max(); + if(oldMax < max) + { + axisY.setMax(max); + } +} + + +void DamagePerSecond::setLog(CombatLog *newLog) +{ + originalLog = newLog; + ui->listSource->selectionModel()->clearSelection(); + + auto filterSources = [](const LogLine &l) + { + return (l.getSourceObject().flags.value & UnitFlags::Control::player) != 0; + }; + sourceNamesList = originalLog->filter(filterSources).getSourceNames(); + sourceModel.setList(&sourceNamesList); + + auto filterTargets = [](const LogLine &l) + { + return (l.getDestObject().flags.value & UnitFlags::Control::npc) != 0; + }; + targetNamesList = originalLog->filter(filterTargets).getTargetNames(); + targetModel.setList(&targetNamesList); + +} + +void DamagePerSecond::on_pushRefresh_clicked() +{ + updateChart(); +} + diff --git a/forms/DamagePerSecond.h b/forms/DamagePerSecond.h new file mode 100644 index 0000000..6f1418f --- /dev/null +++ b/forms/DamagePerSecond.h @@ -0,0 +1,56 @@ +#ifndef DAMAGEPERSECOND_H +#define DAMAGEPERSECOND_H + +#include +#include +#include +#include +#include +#include +#include "../DataModels/ObjectListModel.h" + +namespace Ui { +class DamagePerSecond; +} + +class CombatLog; + + +class DamagePerSecond : public QWidget +{ + Q_OBJECT + +public: + explicit DamagePerSecond(QWidget *parent = nullptr); + ~DamagePerSecond(); + + void setLog(CombatLog *newLog); + +public slots: + + void updateChart(); + +private slots: + void on_pushRefresh_clicked(); + +private: + + void adjustXRange(qreal min, qreal max); + void adjustYRange(qreal min, qreal max); + + Ui::DamagePerSecond *ui; + + ObjectListModel sourceModel; + ObjectListModel targetModel; + + QStringList sourceNamesList; + QStringList targetNamesList; + + + CombatLog *originalLog; + QChart chart; + QValueAxis axisX; + QValueAxis axisY; +}; + +#endif // DAMAGEPERSECOND_H diff --git a/forms/DamagePerSecond.ui b/forms/DamagePerSecond.ui new file mode 100644 index 0000000..1ef349b --- /dev/null +++ b/forms/DamagePerSecond.ui @@ -0,0 +1,113 @@ + + + DamagePerSecond + + + + 0 + 0 + 558 + 300 + + + + Form + + + + + + QAbstractItemView::MultiSelection + + + QAbstractItemView::SelectRows + + + + + + + QAbstractItemView::MultiSelection + + + QAbstractItemView::SelectRows + + + + + + + + + + Overkill + + + + + + + Resisted + + + + + + + Absorbed + + + + + + + 1.000000000000000 + + + 1000.000000000000000 + + + 5.000000000000000 + + + + + + + Blocked + + + + + + + Refresh + + + + + + + Avg period + + + + + + + + + + + + + + QChartView + QWidget +
qchartview.h
+ 1 +
+
+ + +
diff --git a/forms/SourceSelector.cpp b/forms/SourceSelector.cpp new file mode 100644 index 0000000..f14f61a --- /dev/null +++ b/forms/SourceSelector.cpp @@ -0,0 +1,14 @@ +#include "SourceSelector.h" +#include "ui_SourceSelector.h" + +SourceSelector::SourceSelector(QWidget *parent) : + QWidget(parent), + ui(new Ui::SourceSelector) +{ + ui->setupUi(this); +} + +SourceSelector::~SourceSelector() +{ + delete ui; +} diff --git a/forms/SourceSelector.h b/forms/SourceSelector.h new file mode 100644 index 0000000..2c171bc --- /dev/null +++ b/forms/SourceSelector.h @@ -0,0 +1,24 @@ +#ifndef SOURCESELECTOR_H +#define SOURCESELECTOR_H + +#include + +namespace Ui { +class SourceSelector; +} + +class SourceSelector : public QWidget +{ + Q_OBJECT + +public: + explicit SourceSelector(QWidget *parent = nullptr); + ~SourceSelector(); + +private: + Ui::SourceSelector *ui; + + +}; + +#endif // SOURCESELECTOR_H diff --git a/forms/SourceSelector.ui b/forms/SourceSelector.ui new file mode 100644 index 0000000..e67cfb5 --- /dev/null +++ b/forms/SourceSelector.ui @@ -0,0 +1,29 @@ + + + SourceSelector + + + + 0 + 0 + 670 + 557 + + + + Form + + + + + 50 + 20 + 256 + 192 + + + + + + + diff --git a/mainwindow.cpp b/mainwindow.cpp index 3ae68fe..8e4d614 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -1,30 +1,118 @@ #include "mainwindow.h" #include "./ui_mainwindow.h" -#include "CombatLog/CombatLog.h" #include "CombatLog/exceptions.h" #include +#include +#include + + MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui->setupUi(this); - +#if 0 //auto ret = LogLine::fromRawData("9/23 09:11:59.611 SPELL_AURA_REMOVED,0x000000000002FCA4,\"Marund\",0x511,0x000000000002FCA4,\"Marund\",0x511,57399,\"Well Fed\",0x1,BUFF"); //auto ret = LogLine::fromRawData("9/22 17:35:04.139 SPELL_CAST_SUCCESS,0x0000000000026428,\"Blackintos\",0x512,0x0000000000000000,nil,0x80000000,49222,\"Bone Shield\",0x8"); try { - auto log = CombatLog::fromFile("C:/Users/martin/Documents/wow-logy/WoWCombatLog-cos.txt"); + FilterObject filter; + filter.setSourceName("Marund"); + + + + //filter.addSubevent(SubEvent::SPELL_AURA_APPLIED); + //filter.addSubevent(SubEvent::SPELL_AURA_APPLIED_DOSE); + //filter.addSubevent(SubEvent::SPELL_AURA_REMOVED); + //filter.addSubevent(SubEvent::SPELL_AURA_REFRESH); + //filter.addSubevent(SubEvent::SPELL_DAMAGE); + //filter.addSubevent(SubEvent::SPELL_ENERGIZE); + //filter.addSubevent(SubEvent::SPELL_PERIODIC_DAMAGE); + //filter.addSubevent(SubEvent::SWING_DAMAGE); + //filter.addSubevent(SubEvent::DAMAGE_SHIELD); + filter.addSubevent(SubEvent::SPELL_PERIODIC_HEAL); + //filter.addSubevent(SubEvent::SPELL_CAST_START); + //filter.addSubevent(SubEvent::SPELL_PERIODIC_ENERGIZE); + filter.addSubevent(SubEvent::SPELL_HEAL); + //filter.addSubevent(SubEvent::SWING_MISSED); + //filter.addSubevent(SubEvent::PARTY_KILL); + //filter.addSubevent(SubEvent::UNIT_DIED); + //filter.addSubevent(SubEvent::SPELL_CAST_FAILED); + //filter.addSubevent(SubEvent::ENCHANT_APPLIED); + //filter.addSubevent(SubEvent::SPELL_MISSED); + //filter.addSubevent(SubEvent::SPELL_INTERRUPT); + //filter.addSubevent(SubEvent::SPELL_PERIODIC_MISSED); + //filter.addSubevent(SubEvent::SPELL_DISPEL); + //filter.addSubevent(SubEvent::SPELL_SUMMON); + //filter.addSubevent(SubEvent::DAMAGE_SHIELD_MISSED); + //filter.addSubevent(SubEvent::SPELL_PERIODIC_LEECH); + + + log = CombatLog::fromFile("C:/Users/martin/Documents/wow-logy/WoWCombatLog-cos.txt",filter); + + std::cout << "parsed" << std::endl; + + + + //ui->tableView->setModel(&model); + + + //ui->widget->setChart(&chart); + + chart.addSeries(&line); + + auto offset = log.getLines().front().getTimestamp().toMSecsSinceEpoch(); + auto end = log.getLines().back().getTimestamp().toMSecsSinceEpoch() - offset; + + axisX.setRange(0, end); + + axisY.setRange(0,10000); + + chart.addAxis(&axisX, Qt::AlignBottom); + chart.addAxis(&axisY, Qt::AlignLeft); + line.attachAxis(&axisX); + line.attachAxis(&axisY); + + SubSampler sampler{5000,&line}; + + for(const auto &e : log.getLines()) + { + sampler.addValue(e.getTimestamp().toMSecsSinceEpoch() - offset, std::visit([](auto &&arg) -> qreal { + using T = std::decay_t; + if constexpr (std::is_base_of_v) + { + return arg.amount; + } + else + { + throw std::runtime_error("unhandled case"); + } + },e.getSubEventValue())); + } + + sampler.finalize(); + + + { + auto [min, max] = sampler.getMinmax(); + axisY.setRange(min,max); + } + + + } catch (const CombatLogParserException &e) { std::cerr << "file: " << e.getPlace().file_name() << '(' - << e.getPlace().line() << ':' - << e.getPlace().column() << ") `" - << e.getPlace().function_name() << "`: " - << e.what() << '\n'; + << e.getPlace().line() << ':' + << e.getPlace().column() << ") `" + << e.getPlace().function_name() << "`: " + << e.what() << '\n'; } +#endif + } @@ -33,3 +121,45 @@ MainWindow::~MainWindow() delete ui; } + + + +void MainWindow::on_pushFileBrowse_clicked() +{ + auto filename = QFileDialog::getOpenFileName(this,"Combat log",".","*.txt"); + + ui->lineFileName->setText(filename); + +} + + +void MainWindow::on_pushFileLoad_clicked() +{ + try + { + log = CombatLog::fromFile(ui->lineFileName->text()); + + } + catch (const CombatLogParserException &e) + { + + auto msg = QString{"Exception:\nfile: %1 (%2:%3) '%4': %5"} + .arg(e.getPlace().file_name()) + .arg(e.getPlace().line()).arg(e.getPlace().column()) + .arg(e.getPlace().function_name(),e.what()); + + std::cerr << msg.toStdString() << '\n'; + + QMessageBox::critical(this,"Error loading file",msg); + } + auto * widget = qobject_cast(ui->tabWidget->currentWidget()); + + if(!widget) + { + throw std::runtime_error("casting error"); + } + + widget->setLog(&log); + +} + diff --git a/mainwindow.h b/mainwindow.h index 4643e32..a57b08f 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -2,6 +2,8 @@ #define MAINWINDOW_H #include +#include "CombatLog/CombatLog.h" + QT_BEGIN_NAMESPACE namespace Ui { class MainWindow; } @@ -15,7 +17,18 @@ class MainWindow : public QMainWindow MainWindow(QWidget *parent = nullptr); ~MainWindow(); +private slots: + + void on_pushFileBrowse_clicked(); + + void on_pushFileLoad_clicked(); + private: Ui::MainWindow *ui; + CombatLog log; + + + + }; #endif // MAINWINDOW_H diff --git a/mainwindow.ui b/mainwindow.ui index b232854..7efba1f 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -13,10 +13,64 @@ MainWindow - - + + + + + + + + + load + + + + + + + browse + + + + + + + 0 + + + + Damage + + + + + Heal + + + + + + + + + + 0 + 0 + 800 + 21 + + + + + + DamagePerSecond + QWidget +
forms/DamagePerSecond.h
+ 1 +
+
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..5c03b28 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.11 FATAL_ERROR) + +include(FetchContent) + +FetchContent_Declare( + ut + GIT_REPOSITORY https://github.com/boost-ext/ut + GIT_TAG cd12498 +) + +FetchContent_MakeAvailable(ut) + + + +add_executable(SubSamplerTest SubSamplerTest.cpp) +target_link_libraries(SubSamplerTest PRIVATE Boost::ut filters) + diff --git a/tests/SubSamplerTest.cpp b/tests/SubSamplerTest.cpp new file mode 100644 index 0000000..1b1f153 --- /dev/null +++ b/tests/SubSamplerTest.cpp @@ -0,0 +1,114 @@ +#include +#include "../filters/SubSampler.h" +#include +#include +#include +#include + +struct param +{ + std::vector> in; + std::vector> out; + qreal period; + qreal start; +}; + +namespace +{ +param p1{{{100,100}},{{1000,100}},1000,0}; + +param p2{{{100,100}},{{2000,50}},2000,0}; + +param p3{{{100,100},{200,100}},{{1000,200}},1000,0}; + +param p4{{{100,100},{500,100},{1000,100}, {1500,100}} + ,{{1000,200}, {2000,200}},1000,0}; + +param p5{{{30100,100},{30500,100},{31000,100}, {31500,100}} + ,{{31000,200}, {32000,200}},1000,30000}; + +param p10{{{65618,3361},{65618,2373},{65618,474},{67125,4673},{68424,2716},{68424,543},{68528,1962},{68528,1737},{69036,3897},{70236,7421},{70236,2938},{71243,7069},{71243,1413},{71840,5014},{73341,2543},{74438,4193},{74438,838},{75639,9456},{75639,3744}} + ,{{70000,4347.2},{75000,6285.8},{80000,2640}} + ,5000,65618}; + +param p11{{{65618,3361},{65618,2373},{65618,474},{67125,4673},{68424,2716},{68424,543},{68528,1962},{68528,1737},{69036,3897},{70236,7421},{70236,2938},{71243,7069},{71243,1413},{71840,5014},{73341,2543},{74438,4193},{74438,838},{75639,9456},{75639,3744}} + ,{{66000,3104},{68000,2336.5},{70000,5427.5},{72000,11927.5},{74000,1271.5},{76000,9115.5}} + ,2000,65618}; + +param p12{{{65618,3361},{65618,2373},{65618,474},{67125,4673},{68424,2716},{68424,543},{68528,1962},{68528,1737},{69036,3897},{70236,7421},{70236,2938},{71243,7069},{71243,1413},{71840,5014},{73341,2543},{74438,4193},{74438,838},{75639,9456},{75639,3744}} + ,{ + {66000,6208*2},{66500,0}, + {67000,0},{67500,4673*2}, + {68000,0},{68500,3259*2}, + {69000,3699*2},{69500,3897*2}, + {70000,0},{70500,10359*2}, + {71000,0},{71500,8482*2}, + {72000,5014*2},{72500,0}, + {73000,0} ,{73500,2543*2}, + {74000,0},{74500,5031*2}, + {75000,0},{75500,0}, + {76000,13200*2}, + } + ,500,65618}; + + +} + +void range_compare(std::span> l, std::span> r) +{ + boost::ut::expect(l.size() == r.size()) << "range size mismatch, l: " << l.size() << ", r: " << r.size();// << boost::ut::fatal; + auto li = l.begin(); + auto ri = r.begin(); + for( ; li != l.end();++li,++ri) + { + boost::ut::expect(std::abs(li->first - ri->first) <= (li->first/1000)) << "mismatched timestamps l: " << li->first << ", r: " << ri->first;// << boost::ut::fatal; + boost::ut::expect(std::abs(li->second - ri->second) <= (li->second/1000)) << "mismatched values l: " << li->second << ", r: " << ri->second;// << boost::ut::fatal; + } +} + +std::vector> test(std::span> data, qreal period, qreal start) +{ + std::vector> ret; + SubSampler sampler{period, start}; + + for(const auto &[x,y] : data) + { + auto samples = sampler.addValue(x,y); + if(samples.has_value()) + { + for(const auto &sample : samples.value()) + { + ret.emplace_back(sample); + } + } + } + + ret.emplace_back(sampler.finalize()); + + return ret; +} + +void test_check(const param& p) +{ + auto ret = test(p.in,p.period,p.start); + range_compare(p.out, ret); +} + + + + +int main() +{ + test_check(p1); + test_check(p2); + test_check(p3); + test_check(p4); + test_check(p5); + test_check(p10); + test_check(p11); + test_check(p12); + + + return 0; +} +