diff --git a/src/application/ScenarioRunWidget.cpp b/src/application/ScenarioRunWidget.cpp index 6f3c955..0825d20 100644 --- a/src/application/ScenarioRunWidget.cpp +++ b/src/application/ScenarioRunWidget.cpp @@ -2,6 +2,8 @@ #include #include +#include +#include #include #include #include @@ -9,17 +11,21 @@ #include #include #include +#include #include +#include #include #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -37,6 +43,7 @@ namespace { constexpr double kSimulationDeltaSeconds = 1.0 / 30.0; constexpr double kResultCalculationChunkSeconds = 1.0; constexpr int kPlaybackTimerIntervalMs = 33; +constexpr int kMaxUiSeed = 2147483647; int normalizedRunIndex(int index, std::size_t runCount) { if (runCount == 0) { @@ -45,6 +52,18 @@ int normalizedRunIndex(int index, std::size_t runCount) { return std::clamp(index, 0, static_cast(runCount) - 1); } +int firstRunIndexForSourceScenario( + const safecrowd::domain::ScenarioBatchRunner& batchRunner, + std::size_t sourceScenarioIndex, + int fallbackIndex) { + for (std::size_t index = 0; index < batchRunner.size(); ++index) { + if (batchRunner.run(index).sourceScenarioIndex == sourceScenarioIndex) { + return static_cast(index); + } + } + return normalizedRunIndex(fallbackIndex, batchRunner.size()); +} + enum class TransportIconKind { Play, Pause, @@ -657,6 +676,49 @@ QWidget* ScenarioRunWidget::createRunPanel() { layout->addWidget(congestionLabel_); layout->addWidget(bottleneckLabel_); + auto* settingsTitle = createLabel("Run settings", panel, ui::FontRole::Caption); + settingsTitle->setStyleSheet(ui::mutedTextStyleSheet()); + layout->addWidget(settingsTitle); + + auto* settingsGroup = new QWidget(panel); + auto* settingsForm = new QFormLayout(settingsGroup); + settingsForm->setContentsMargins(0, 4, 0, 0); + settingsForm->setSpacing(6); + + timeLimitSpin_ = new QDoubleSpinBox(settingsGroup); + timeLimitSpin_->setRange(1.0, 3600.0); + timeLimitSpin_->setDecimals(0); + timeLimitSpin_->setSuffix(" s"); + timeLimitSpin_->setToolTip("Simulation time limit"); + settingsForm->addRow("Time limit", timeLimitSpin_); + + sampleIntervalSpin_ = new QDoubleSpinBox(settingsGroup); + sampleIntervalSpin_->setRange(0.1, 10.0); + sampleIntervalSpin_->setSingleStep(0.1); + sampleIntervalSpin_->setDecimals(2); + sampleIntervalSpin_->setSuffix(" s"); + sampleIntervalSpin_->setToolTip("Result sample interval"); + settingsForm->addRow("Sample interval", sampleIntervalSpin_); + + repeatSpin_ = new QSpinBox(settingsGroup); + repeatSpin_->setRange(1, static_cast(safecrowd::domain::kScenarioExecutionMaxRepeatCount)); + repeatSpin_->setSuffix(" runs"); + repeatSpin_->setToolTip("Repeat count"); + settingsForm->addRow("Repeats", repeatSpin_); + + seedSpin_ = new QSpinBox(settingsGroup); + seedSpin_->setRange(1, kMaxUiSeed); + seedSpin_->setToolTip("Base random seed"); + settingsForm->addRow("Seed", seedSpin_); + + applySettingsButton_ = new QPushButton("Apply selected & restart", settingsGroup); + applySettingsButton_->setFont(ui::font(ui::FontRole::Caption)); + applySettingsButton_->setStyleSheet(ui::secondaryButtonStyleSheet()); + settingsForm->addRow(applySettingsButton_); + + layout->addWidget(settingsGroup); + syncRunSettingsControls(); + layout->addStretch(1); auto* transportLayout = new QHBoxLayout(); @@ -704,6 +766,9 @@ QWidget* ScenarioRunWidget::createRunPanel() { connect(resultButton_, &QPushButton::clicked, this, [this]() { showResults(); }); + connect(applySettingsButton_, &QPushButton::clicked, this, [this]() { + applyRunSettings(); + }); return panel; } @@ -917,7 +982,112 @@ void ScenarioRunWidget::selectRun(int index) { } selectedRunIndex_ = index; scenario_ = batchRunner_.run(static_cast(selectedRunIndex_)).scenario; + syncRunSettingsControls(); + refreshStatus(); +} + +void ScenarioRunWidget::syncRunSettingsControls() { + if (scenarios_.empty()) { + return; + } + const auto sourceIndex = selectedSourceScenarioIndex(); + if (sourceIndex >= scenarios_.size()) { + return; + } + const auto& execution = scenarios_[sourceIndex].execution; + if (timeLimitSpin_ != nullptr) { + timeLimitSpin_->setValue(execution.timeLimitSeconds > 0.0 ? execution.timeLimitSeconds : 600.0); + } + if (sampleIntervalSpin_ != nullptr) { + sampleIntervalSpin_->setValue(execution.sampleIntervalSeconds > 0.0 ? execution.sampleIntervalSeconds : 0.5); + } + if (repeatSpin_ != nullptr) { + repeatSpin_->setValue(std::clamp( + static_cast(execution.repeatCount), + 1, + static_cast(safecrowd::domain::kScenarioExecutionMaxRepeatCount))); + } + if (seedSpin_ != nullptr) { + seedSpin_->setValue(execution.baseSeed == 0 + ? 1 + : static_cast(std::min( + execution.baseSeed, + static_cast(kMaxUiSeed)))); + } +} + +void ScenarioRunWidget::applyRunSettings() { + if (scenarios_.empty() + || timeLimitSpin_ == nullptr + || sampleIntervalSpin_ == nullptr + || repeatSpin_ == nullptr + || seedSpin_ == nullptr) { + return; + } + + const auto sourceIndex = selectedSourceScenarioIndex(); + if (sourceIndex >= scenarios_.size()) { + return; + } + + const bool wouldDiscardResults = hasCachedResults() || (batchRunner_.complete() && !batchRunner_.empty()); + if (wouldDiscardResults + && QMessageBox::question( + this, + "Apply run settings", + "Re-running with the new settings discards the current simulation results.\n\nContinue?", + QMessageBox::Yes | QMessageBox::Cancel, + QMessageBox::Cancel) + != QMessageBox::Yes) { + return; + } + + auto& execution = scenarios_[sourceIndex].execution; + execution.timeLimitSeconds = timeLimitSpin_->value(); + execution.sampleIntervalSeconds = sampleIntervalSpin_->value(); + execution.repeatCount = static_cast(repeatSpin_->value()); + execution.baseSeed = static_cast(seedSpin_->value()); + syncReturnAuthoringScenarioExecution(sourceIndex); + + playbackSpeedMultiplier_ = 1; + paused_ = false; + cachedResults_.clear(); + if (timer_ != nullptr) { + timer_->stop(); + } + batchRunner_.reset(layout_, scenarios_); + selectedRunIndex_ = firstRunIndexForSourceScenario(batchRunner_, sourceIndex, selectedRunIndex_); + if (!batchRunner_.empty()) { + scenario_ = batchRunner_.run(static_cast(selectedRunIndex_)).scenario; + } + if (shell_ != nullptr) { + shell_->setCanvas(createRunCanvas()); + } refreshStatus(); + if (timer_ != nullptr) { + timer_->start(); + } +} + +void ScenarioRunWidget::syncReturnAuthoringScenarioExecution(std::size_t sourceIndex) { + if (!returnAuthoringState_.has_value() || sourceIndex >= scenarios_.size()) { + return; + } + + const auto& sourceScenario = scenarios_[sourceIndex]; + auto& authoringScenarios = returnAuthoringState_->scenarios; + auto it = std::find_if(authoringScenarios.begin(), authoringScenarios.end(), [&](const auto& scenario) { + return scenario.draft.scenarioId == sourceScenario.scenarioId; + }); + if (it == authoringScenarios.end() && sourceIndex < authoringScenarios.size()) { + it = std::next(authoringScenarios.begin(), static_cast(sourceIndex)); + } + if (it == authoringScenarios.end()) { + return; + } + + it->draft.execution = sourceScenario.execution; + it->stagedForRun = true; } void ScenarioRunWidget::cycleFastForwardMode() { diff --git a/src/application/ScenarioRunWidget.h b/src/application/ScenarioRunWidget.h index cf5155d..e96d10d 100644 --- a/src/application/ScenarioRunWidget.h +++ b/src/application/ScenarioRunWidget.h @@ -13,9 +13,11 @@ #include "domain/ScenarioAuthoring.h" #include "domain/ScenarioBatchRunner.h" +class QDoubleSpinBox; class QLabel; class QProgressBar; class QPushButton; +class QSpinBox; class QTimer; namespace safecrowd::application { @@ -89,6 +91,9 @@ class ScenarioRunWidget : public QWidget { void refreshStatus(); void selectRun(int index); std::size_t selectedSourceScenarioIndex() const; + void applyRunSettings(); + void syncReturnAuthoringScenarioExecution(std::size_t sourceIndex); + void syncRunSettingsControls(); void showResults(); void setPlaybackSpeedMultiplier(int multiplier); void stopRun(); @@ -126,6 +131,11 @@ class ScenarioRunWidget : public QWidget { QPushButton* speed3Button_{nullptr}; QPushButton* speed5Button_{nullptr}; QPushButton* resultButton_{nullptr}; + QDoubleSpinBox* timeLimitSpin_{nullptr}; + QDoubleSpinBox* sampleIntervalSpin_{nullptr}; + QSpinBox* repeatSpin_{nullptr}; + QSpinBox* seedSpin_{nullptr}; + QPushButton* applySettingsButton_{nullptr}; int selectedRunIndex_{0}; int playbackSpeedMultiplier_{1}; bool paused_{false};