Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 96 additions & 2 deletions src/application/ScenarioBatchResultWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <QComboBox>
#include <QEvent>
#include <QFrame>
#include <QHeaderView>
#include <QHBoxLayout>
#include <QLabel>
#include <QMouseEvent>
Expand All @@ -24,6 +25,8 @@
#include <QSizePolicy>
#include <QSlider>
#include <QTabWidget>
#include <QTableWidget>
#include <QTableWidgetItem>
#include <QTimer>
#include <QVBoxLayout>

Expand Down Expand Up @@ -73,6 +76,41 @@ QString formatPercent(std::size_t numerator, std::size_t denominator) {
return QString("%1%").arg(ratio * 100.0, 0, 'f', 0);
}

QString formatPressureScore(double score) {
return QString::number(score, 'f', 1);
}

QTableWidget* createComparisonTable(const QStringList& headers, QWidget* parent) {
auto* table = new QTableWidget(0, headers.size(), parent);
table->setHorizontalHeaderLabels(headers);
table->verticalHeader()->setVisible(false);
table->horizontalHeader()->setStretchLastSection(true);
table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
table->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
table->setWordWrap(false);
table->setEditTriggers(QAbstractItemView::NoEditTriggers);
table->setSelectionMode(QAbstractItemView::NoSelection);
table->setFocusPolicy(Qt::NoFocus);
table->setAlternatingRowColors(true);
table->setStyleSheet(
"QTableWidget { background: #ffffff; border: 1px solid #d7e0ea; gridline-color: #e4ebf3; }"
"QHeaderView::section { background: #eef3f8; border: 0; border-bottom: 1px solid #d7e0ea; padding: 6px; color: #4f5d6b; }"
"QTableWidget::item { padding: 6px; }");
return table;
}

QTableWidgetItem* tableItem(const QString& text, bool emphasized = false) {
auto* item = new QTableWidgetItem(text);
item->setFlags(item->flags() & ~Qt::ItemIsEditable);
if (emphasized) {
auto font = item->font();
font.setBold(true);
item->setFont(font);
item->setBackground(QColor("#eef5ff"));
}
return item;
}

QString scenarioRoleLabel(safecrowd::domain::ScenarioRole role) {
switch (role) {
case safecrowd::domain::ScenarioRole::Baseline:
Expand Down Expand Up @@ -658,6 +696,7 @@ QWidget* ScenarioBatchResultWidget::createCanvasPanel() {

overlayCombo_ = new QComboBox(selectorBar);
overlayCombo_->addItem("Density", static_cast<int>(OverlayMode::Density));
overlayCombo_->addItem("Pressure", static_cast<int>(OverlayMode::Pressure));
overlayCombo_->addItem("Hotspots", static_cast<int>(OverlayMode::Hotspots));
overlayCombo_->addItem("Bottlenecks", static_cast<int>(OverlayMode::Bottlenecks));
overlayCombo_->addItem("None", static_cast<int>(OverlayMode::None));
Expand Down Expand Up @@ -704,13 +743,15 @@ QWidget* ScenarioBatchResultWidget::createCanvasPanel() {
auto* tabs = new QTabWidget(graphPanel);
remainingChart_ = new ComparisonGraphWidget(ComparisonGraphMode::Remaining, tabs);
exitsChart_ = new ComparisonGraphWidget(ComparisonGraphMode::Exits, tabs);
pressureTable_ = createComparisonTable({"Scenario", "Peak score", "Exposed / Critical", "Hotspots", "Events", "Peak at"}, tabs);
static_cast<ComparisonGraphWidget*>(remainingChart_)->setResults(results_, selectedCompareIndices_, currentResultIndex_);
static_cast<ComparisonGraphWidget*>(exitsChart_)->setResults(results_, selectedCompareIndices_, currentResultIndex_);
static_cast<ComparisonGraphWidget*>(remainingChart_)->setTimingMarkerActivatedHandler([this](double seconds) {
seekToTimingMarkerSeconds(seconds);
});
tabs->addTab(remainingChart_, "Remaining");
tabs->addTab(exitsChart_, "Exits");
tabs->addTab(pressureTable_, "Pressure");
graphLayout->addWidget(tabs, 1);
layout->addWidget(graphPanel, 1);

Expand Down Expand Up @@ -769,7 +810,7 @@ QWidget* ScenarioBatchResultWidget::createSummaryPanel() {
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(12);

auto* intro = createLabel("Choose which completed scenarios appear together in the Remaining and Exits graphs.", content, ui::FontRole::Caption);
auto* intro = createLabel("Choose which completed scenarios appear together in the comparison graphs and pressure summary table.", content, ui::FontRole::Caption);
intro->setStyleSheet(ui::mutedTextStyleSheet());
layout->addWidget(intro);

Expand Down Expand Up @@ -921,6 +962,12 @@ void ScenarioBatchResultWidget::applyReplayFrameData(const safecrowd::domain::Si
? result.artifacts.densitySummary.peakCells
: result.artifacts.densitySummary.peakField.cells,
result.artifacts.densitySummary.highDensityThresholdPeoplePerSquareMeter);
canvas_->setPressureOverlay(result.artifacts.pressureSummary.peakField.cells.empty()
? result.artifacts.pressureSummary.peakCells
: result.artifacts.pressureSummary.peakField.cells,
std::max(
result.artifacts.pressureSummary.hotspotScoreThreshold,
result.artifacts.pressureSummary.peakPressureScore));
applyOverlayModeToCanvas();
}
if (replaySlider_ != nullptr && replaySlider_->value() != replayFrameIndex_) {
Expand All @@ -946,6 +993,9 @@ void ScenarioBatchResultWidget::applyOverlayModeToCanvas() {
return;
}
switch (overlayMode_) {
case OverlayMode::Pressure:
canvas_->setResultOverlayMode(ResultOverlayMode::Pressure);
break;
case OverlayMode::Hotspots:
canvas_->setResultOverlayMode(ResultOverlayMode::Hotspots);
break;
Expand Down Expand Up @@ -1069,6 +1119,44 @@ void ScenarioBatchResultWidget::refreshComparisonSelection() {
if (exitsChart_ != nullptr) {
static_cast<ComparisonGraphWidget*>(exitsChart_)->setResults(results_, selectedCompareIndices_, currentResultIndex_);
}
refreshPressureComparisonTable();
}

void ScenarioBatchResultWidget::refreshPressureComparisonTable() {
if (pressureTable_ == nullptr) {
return;
}

std::vector<int> visibleIndices = selectedCompareIndices_;
if (visibleIndices.empty() && currentResultIndex_ >= 0 && currentResultIndex_ < static_cast<int>(results_.size())) {
visibleIndices.push_back(currentResultIndex_);
}

pressureTable_->setRowCount(static_cast<int>(visibleIndices.size()));
for (int row = 0; row < static_cast<int>(visibleIndices.size()); ++row) {
const auto index = visibleIndices[static_cast<std::size_t>(row)];
if (index < 0 || index >= static_cast<int>(results_.size())) {
continue;
}

const auto& result = results_[static_cast<std::size_t>(index)];
const auto& summary = result.artifacts.pressureSummary;
const bool emphasized = index == currentResultIndex_;
pressureTable_->setItem(row, 0, tableItem(QString::fromStdString(result.scenario.name), emphasized));
pressureTable_->setItem(row, 1, tableItem(formatPressureScore(summary.peakPressureScore), emphasized));
pressureTable_->setItem(
row,
2,
tableItem(
QString("%1 / %2")
.arg(static_cast<int>(summary.peakExposedAgentCount))
.arg(static_cast<int>(summary.peakCriticalAgentCount)),
emphasized));
pressureTable_->setItem(row, 3, tableItem(QString::number(static_cast<int>(summary.peakHotspots.size())), emphasized));
pressureTable_->setItem(row, 4, tableItem(QString::number(static_cast<int>(summary.criticalEvents.size())), emphasized));
pressureTable_->setItem(row, 5, tableItem(formatSeconds(summary.peakAtSeconds), emphasized));
}
pressureTable_->resizeRowsToContents();
}

void ScenarioBatchResultWidget::refreshResultNavigationPanel() {
Expand Down Expand Up @@ -1151,6 +1239,7 @@ void ScenarioBatchResultWidget::refreshSelectedResult() {
if (exitsChart_ != nullptr) {
static_cast<ComparisonGraphWidget*>(exitsChart_)->setResults(results_, selectedCompareIndices_, currentResultIndex_);
}
refreshPressureComparisonTable();
refreshResultNavigationPanel();
if (detailLabel_ != nullptr) {
const auto selectedFinalSeconds = finalSeconds(result);
Expand All @@ -1159,7 +1248,7 @@ void ScenarioBatchResultWidget::refreshSelectedResult() {
? QString("Baseline")
: formatDeltaSeconds(selectedFinalSeconds - finalSeconds(results_[static_cast<std::size_t>(baselineIndex)])))
: QString("No baseline");
detailLabel_->setText(QString("%1 (%2)\nFinal: %3\nDelta vs baseline: %4\nEvacuated: %5 / %6 (%7)\nRisk: %8\nHotspots: %9\nBottlenecks: %10\nT90 / T95: %11 / %12")
detailLabel_->setText(QString("%1 (%2)\nFinal: %3\nDelta vs baseline: %4\nEvacuated: %5 / %6 (%7)\nRisk: %8\nHotspots: %9\nBottlenecks: %10\nPressure hotspots: %11\nCritical pressure: %12 agents / %13 events\nPeak pressure: %14 at %15\nT90 / T95: %16 / %17")
.arg(QString::fromStdString(result.scenario.name))
.arg(scenarioRoleLabel(result.scenario.role))
.arg(formatSeconds(selectedFinalSeconds))
Expand All @@ -1170,6 +1259,11 @@ void ScenarioBatchResultWidget::refreshSelectedResult() {
.arg(safecrowd::domain::scenarioRiskLevelLabel(result.risk.completionRisk))
.arg(static_cast<int>(result.risk.hotspots.size()))
.arg(static_cast<int>(result.risk.bottlenecks.size()))
.arg(static_cast<int>(result.artifacts.pressureSummary.peakHotspots.size()))
.arg(static_cast<int>(result.artifacts.pressureSummary.peakCriticalAgentCount))
.arg(static_cast<int>(result.artifacts.pressureSummary.criticalEvents.size()))
.arg(formatPressureScore(result.artifacts.pressureSummary.peakPressureScore))
.arg(formatSeconds(result.artifacts.pressureSummary.peakAtSeconds))
.arg(formatSeconds(result.artifacts.timingSummary.t90Seconds))
.arg(formatSeconds(result.artifacts.timingSummary.t95Seconds)));
}
Expand Down
10 changes: 7 additions & 3 deletions src/application/ScenarioBatchResultWidget.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class QCheckBox;
class QComboBox;
class QPushButton;
class QSlider;
class QTableWidget;
class QTimer;

namespace safecrowd::application {
Expand Down Expand Up @@ -45,9 +46,10 @@ class ScenarioBatchResultWidget : public QWidget {
private:
enum class OverlayMode {
Density = 0,
Hotspots = 1,
Bottlenecks = 2,
None = 3,
Pressure = 1,
Hotspots = 2,
Bottlenecks = 3,
None = 4,
};

QWidget* createCanvasPanel();
Expand All @@ -61,6 +63,7 @@ class ScenarioBatchResultWidget : public QWidget {
void navigateToAuthoring();
void pauseReplay();
void refreshComparisonSelection();
void refreshPressureComparisonTable();
void refreshResultNavigationPanel();
void refreshSelectedResult();
void rerunBatch();
Expand Down Expand Up @@ -89,6 +92,7 @@ class ScenarioBatchResultWidget : public QWidget {
QSlider* replaySlider_{nullptr};
QLabel* replayTimeLabel_{nullptr};
QLabel* detailLabel_{nullptr};
QTableWidget* pressureTable_{nullptr};
std::vector<QCheckBox*> compareCheckBoxes_{};
QWidget* remainingChart_{nullptr};
QWidget* exitsChart_{nullptr};
Expand Down
107 changes: 105 additions & 2 deletions src/application/ScenarioResultWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
#include <QTimer>
#include <QToolButton>
#include <QVBoxLayout>
#include <QWidget>

#include "application/ScenarioAuthoringWidget.h"
#include "application/ScenarioCanvasWidget.h"
Expand Down Expand Up @@ -71,6 +72,10 @@ QString formatDensity(double density) {
return QString("%1 / m2").arg(density, 0, 'f', 1);
}

QString formatPressureScore(double score) {
return QString::number(score, 'f', 1);
}

QString formatPercent(double ratio) {
return QString("%1%").arg(std::clamp(ratio, 0.0, 1.0) * 100.0, 0, 'f', 0);
}
Expand Down Expand Up @@ -372,6 +377,60 @@ class DensityLegendWidget final : public QWidget {
double peakDensity_{0.0};
};

class PressureLegendWidget final : public QWidget {
public:
explicit PressureLegendWidget(
const safecrowd::domain::PressureSummary& summary,
QWidget* parent = nullptr)
: QWidget(parent),
threshold_(summary.hotspotScoreThreshold),
peakScore_(summary.peakPressureScore) {
setFixedSize(300, 34);
setToolTip("Pressure heatmap scale uses the pressure hotspot score threshold and the observed peak score.");
}

protected:
void paintEvent(QPaintEvent* event) override {
(void)event;

QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);

const QRectF ramp(0, 4, width(), 9);
QLinearGradient gradient(ramp.left(), ramp.center().y(), ramp.right(), ramp.center().y());
gradient.setColorAt(0.0, QColor("#facc15"));
gradient.setColorAt(0.25, QColor("#f97316"));
gradient.setColorAt(0.55, QColor("#ef4444"));
gradient.setColorAt(1.0, QColor("#991b1b"));
painter.setPen(Qt::NoPen);
painter.setBrush(gradient);
painter.drawRoundedRect(ramp, 4, 4);

painter.setFont(ui::font(ui::FontRole::Caption));
painter.setPen(QColor("#687789"));
if (peakScore_ > 0.0 && threshold_ > 0.0) {
const auto peakX = ramp.left()
+ (std::clamp(peakScore_ / threshold_, 0.0, 1.0) * ramp.width());
painter.setPen(QPen(QColor("#405063"), 1));
painter.drawLine(QPointF(peakX, ramp.top() - 2), QPointF(peakX, ramp.bottom() + 2));
painter.setPen(QColor("#687789"));
}
painter.drawText(QRectF(0, 16, 40, 16), Qt::AlignLeft | Qt::AlignVCenter, "0");
painter.drawText(
QRectF(42, 16, 178, 16),
Qt::AlignCenter,
QString("Hotspot %1+").arg(threshold_, 0, 'f', 1));
painter.drawText(
QRectF(width() - 98, 16, 98, 16),
Qt::AlignRight | Qt::AlignVCenter,
QString("Peak %1").arg(peakScore_, 0, 'f', 1));
}

private:
double threshold_{0.0};
double peakScore_{0.0};
};

QFrame* createMetricCard(const QString& title, const QString& value, QWidget* parent, const QString& tooltip = {}) {
auto* card = new QFrame(parent);
card->setStyleSheet(ui::panelStyleSheet());
Expand Down Expand Up @@ -400,6 +459,8 @@ QString resultCriteriaTooltip(const safecrowd::domain::ScenarioResultArtifacts&
return QStringList{
QString("High density: %1 / m2 or higher for accumulated duration.")
.arg(artifacts.densitySummary.highDensityThresholdPeoplePerSquareMeter, 0, 'f', 1),
QString("Pressure hotspot: score %1 or higher in a crowded cell.")
.arg(artifacts.pressureSummary.hotspotScoreThreshold, 0, 'f', 1),
safecrowd::domain::scenarioStalledDefinition(),
safecrowd::domain::scenarioBottleneckDefinition(),
}.join("\n\n");
Expand Down Expand Up @@ -744,23 +805,36 @@ QWidget* createResultCanvasPanel(
overlayLabel->setStyleSheet(ui::mutedTextStyleSheet());
auto* overlayCombo = new QComboBox(overlayBar);
overlayCombo->addItem("Peak Density", static_cast<int>(ResultOverlayMode::Density));
overlayCombo->addItem("Pressure", static_cast<int>(ResultOverlayMode::Pressure));
overlayCombo->addItem("Bottlenecks", static_cast<int>(ResultOverlayMode::Bottlenecks));
overlayCombo->addItem("Hotspots", static_cast<int>(ResultOverlayMode::Hotspots));
overlayCombo->addItem("None", static_cast<int>(ResultOverlayMode::None));
overlayCombo->setToolTip("Switch between result map overlays.");
overlayLayout->addWidget(overlayLabel);
overlayLayout->addWidget(overlayCombo);
overlayLayout->addSpacing(10);
overlayLayout->addWidget(new DensityLegendWidget(artifacts.densitySummary, overlayBar));
auto* densityLegend = new DensityLegendWidget(artifacts.densitySummary, overlayBar);
auto* pressureLegend = new PressureLegendWidget(artifacts.pressureSummary, overlayBar);
pressureLegend->setVisible(false);
overlayLayout->addWidget(densityLegend);
overlayLayout->addWidget(pressureLegend);
overlayLayout->addStretch(1);
layout->addWidget(overlayBar);
canvas->setDensityOverlay(artifacts.densitySummary.peakField.cells.empty()
? artifacts.densitySummary.peakCells
: artifacts.densitySummary.peakField.cells,
artifacts.densitySummary.highDensityThresholdPeoplePerSquareMeter);
canvas->setPressureOverlay(artifacts.pressureSummary.peakField.cells.empty()
? artifacts.pressureSummary.peakCells
: artifacts.pressureSummary.peakField.cells,
std::max(
artifacts.pressureSummary.hotspotScoreThreshold,
artifacts.pressureSummary.peakPressureScore));
canvas->setResultOverlayMode(ResultOverlayMode::Density);
QObject::connect(overlayCombo, &QComboBox::currentIndexChanged, panel, [canvas, overlayCombo](int index) {
QObject::connect(overlayCombo, &QComboBox::currentIndexChanged, panel, [canvas, overlayCombo, densityLegend, pressureLegend](int index) {
const auto mode = static_cast<ResultOverlayMode>(overlayCombo->itemData(index).toInt());
densityLegend->setVisible(mode == ResultOverlayMode::Density);
pressureLegend->setVisible(mode == ResultOverlayMode::Pressure);
canvas->setResultOverlayMode(mode);
});
layout->addWidget(canvas, 1);
Expand Down Expand Up @@ -893,6 +967,14 @@ QWidget* createResultPanel(
const auto slowestGroup = artifacts.placementCompletion.empty()
? QString("Pending")
: QString::fromStdString(artifacts.placementCompletion.front().placementId);
const auto peakPressureTooltip = QString(
"Highest pressure hotspot score observed during the run.%1%2")
.arg(artifacts.pressureSummary.peakAtSeconds.has_value()
? QString("\n\nPeak at %1 sec.").arg(*artifacts.pressureSummary.peakAtSeconds, 0, 'f', 1)
: QString())
.arg(artifacts.pressureSummary.peakCell.has_value()
? QString("\nCell floor: %1").arg(QString::fromStdString(artifacts.pressureSummary.peakCell->floorId))
: QString());
metricsGrid->addWidget(createMetricCard(
"Completion",
formatSecondsValue(completionTime),
Expand Down Expand Up @@ -939,6 +1021,27 @@ QWidget* createResultPanel(
formatOptionalSeconds(artifacts.timingSummary.t95Seconds),
panel,
"Time at which 95% of occupants completed evacuation."), 4, 0);
metricsGrid->addWidget(createMetricCard(
"Peak Pressure",
formatPressureScore(artifacts.pressureSummary.peakPressureScore),
panel,
peakPressureTooltip), 4, 1);
metricsGrid->addWidget(createMetricCard(
"Pressure Hotspots",
QString::number(static_cast<int>(artifacts.pressureSummary.peakHotspots.size())),
panel,
"Peak number of stored pressure hotspot locations from the run."), 5, 0);
metricsGrid->addWidget(createMetricCard(
"Pressure Events",
QString::number(static_cast<int>(artifacts.pressureSummary.criticalEvents.size())),
panel,
"Stored sustained critical pressure events that met the duration and agent-count thresholds."), 5, 1);
metricsGrid->addWidget(createMetricCard(
"Critical Pressure",
QString("%1 agents").arg(static_cast<int>(artifacts.pressureSummary.peakCriticalAgentCount)),
panel,
QString("Peak simultaneously critical agents during the run.\nExposed peak: %1 agents.")
.arg(static_cast<int>(artifacts.pressureSummary.peakExposedAgentCount))), 6, 0);
layout->addLayout(metricsGrid);
layout->addStretch(1);

Expand Down
Loading
Loading