|
33 | 33 | #include <QTimer> |
34 | 34 | #include <QToolButton> |
35 | 35 | #include <QVBoxLayout> |
| 36 | +#include <QWidget> |
36 | 37 |
|
37 | 38 | #include "application/ScenarioAuthoringWidget.h" |
38 | 39 | #include "application/ScenarioCanvasWidget.h" |
@@ -71,6 +72,10 @@ QString formatDensity(double density) { |
71 | 72 | return QString("%1 / m2").arg(density, 0, 'f', 1); |
72 | 73 | } |
73 | 74 |
|
| 75 | +QString formatPressureScore(double score) { |
| 76 | + return QString::number(score, 'f', 1); |
| 77 | +} |
| 78 | + |
74 | 79 | QString formatPercent(double ratio) { |
75 | 80 | return QString("%1%").arg(std::clamp(ratio, 0.0, 1.0) * 100.0, 0, 'f', 0); |
76 | 81 | } |
@@ -372,6 +377,60 @@ class DensityLegendWidget final : public QWidget { |
372 | 377 | double peakDensity_{0.0}; |
373 | 378 | }; |
374 | 379 |
|
| 380 | +class PressureLegendWidget final : public QWidget { |
| 381 | +public: |
| 382 | + explicit PressureLegendWidget( |
| 383 | + const safecrowd::domain::PressureSummary& summary, |
| 384 | + QWidget* parent = nullptr) |
| 385 | + : QWidget(parent), |
| 386 | + threshold_(summary.hotspotScoreThreshold), |
| 387 | + peakScore_(summary.peakPressureScore) { |
| 388 | + setFixedSize(300, 34); |
| 389 | + setToolTip("Pressure heatmap scale uses the pressure hotspot score threshold and the observed peak score."); |
| 390 | + } |
| 391 | + |
| 392 | +protected: |
| 393 | + void paintEvent(QPaintEvent* event) override { |
| 394 | + (void)event; |
| 395 | + |
| 396 | + QPainter painter(this); |
| 397 | + painter.setRenderHint(QPainter::Antialiasing, true); |
| 398 | + |
| 399 | + const QRectF ramp(0, 4, width(), 9); |
| 400 | + QLinearGradient gradient(ramp.left(), ramp.center().y(), ramp.right(), ramp.center().y()); |
| 401 | + gradient.setColorAt(0.0, QColor("#facc15")); |
| 402 | + gradient.setColorAt(0.25, QColor("#f97316")); |
| 403 | + gradient.setColorAt(0.55, QColor("#ef4444")); |
| 404 | + gradient.setColorAt(1.0, QColor("#991b1b")); |
| 405 | + painter.setPen(Qt::NoPen); |
| 406 | + painter.setBrush(gradient); |
| 407 | + painter.drawRoundedRect(ramp, 4, 4); |
| 408 | + |
| 409 | + painter.setFont(ui::font(ui::FontRole::Caption)); |
| 410 | + painter.setPen(QColor("#687789")); |
| 411 | + if (peakScore_ > 0.0 && threshold_ > 0.0) { |
| 412 | + const auto peakX = ramp.left() |
| 413 | + + (std::clamp(peakScore_ / threshold_, 0.0, 1.0) * ramp.width()); |
| 414 | + painter.setPen(QPen(QColor("#405063"), 1)); |
| 415 | + painter.drawLine(QPointF(peakX, ramp.top() - 2), QPointF(peakX, ramp.bottom() + 2)); |
| 416 | + painter.setPen(QColor("#687789")); |
| 417 | + } |
| 418 | + painter.drawText(QRectF(0, 16, 40, 16), Qt::AlignLeft | Qt::AlignVCenter, "0"); |
| 419 | + painter.drawText( |
| 420 | + QRectF(42, 16, 178, 16), |
| 421 | + Qt::AlignCenter, |
| 422 | + QString("Hotspot %1+").arg(threshold_, 0, 'f', 1)); |
| 423 | + painter.drawText( |
| 424 | + QRectF(width() - 98, 16, 98, 16), |
| 425 | + Qt::AlignRight | Qt::AlignVCenter, |
| 426 | + QString("Peak %1").arg(peakScore_, 0, 'f', 1)); |
| 427 | + } |
| 428 | + |
| 429 | +private: |
| 430 | + double threshold_{0.0}; |
| 431 | + double peakScore_{0.0}; |
| 432 | +}; |
| 433 | + |
375 | 434 | QFrame* createMetricCard(const QString& title, const QString& value, QWidget* parent, const QString& tooltip = {}) { |
376 | 435 | auto* card = new QFrame(parent); |
377 | 436 | card->setStyleSheet(ui::panelStyleSheet()); |
@@ -400,6 +459,8 @@ QString resultCriteriaTooltip(const safecrowd::domain::ScenarioResultArtifacts& |
400 | 459 | return QStringList{ |
401 | 460 | QString("High density: %1 / m2 or higher for accumulated duration.") |
402 | 461 | .arg(artifacts.densitySummary.highDensityThresholdPeoplePerSquareMeter, 0, 'f', 1), |
| 462 | + QString("Pressure hotspot: score %1 or higher in a crowded cell.") |
| 463 | + .arg(artifacts.pressureSummary.hotspotScoreThreshold, 0, 'f', 1), |
403 | 464 | safecrowd::domain::scenarioStalledDefinition(), |
404 | 465 | safecrowd::domain::scenarioBottleneckDefinition(), |
405 | 466 | }.join("\n\n"); |
@@ -744,23 +805,36 @@ QWidget* createResultCanvasPanel( |
744 | 805 | overlayLabel->setStyleSheet(ui::mutedTextStyleSheet()); |
745 | 806 | auto* overlayCombo = new QComboBox(overlayBar); |
746 | 807 | overlayCombo->addItem("Peak Density", static_cast<int>(ResultOverlayMode::Density)); |
| 808 | + overlayCombo->addItem("Pressure", static_cast<int>(ResultOverlayMode::Pressure)); |
747 | 809 | overlayCombo->addItem("Bottlenecks", static_cast<int>(ResultOverlayMode::Bottlenecks)); |
748 | 810 | overlayCombo->addItem("Hotspots", static_cast<int>(ResultOverlayMode::Hotspots)); |
749 | 811 | overlayCombo->addItem("None", static_cast<int>(ResultOverlayMode::None)); |
750 | 812 | overlayCombo->setToolTip("Switch between result map overlays."); |
751 | 813 | overlayLayout->addWidget(overlayLabel); |
752 | 814 | overlayLayout->addWidget(overlayCombo); |
753 | 815 | overlayLayout->addSpacing(10); |
754 | | - overlayLayout->addWidget(new DensityLegendWidget(artifacts.densitySummary, overlayBar)); |
| 816 | + auto* densityLegend = new DensityLegendWidget(artifacts.densitySummary, overlayBar); |
| 817 | + auto* pressureLegend = new PressureLegendWidget(artifacts.pressureSummary, overlayBar); |
| 818 | + pressureLegend->setVisible(false); |
| 819 | + overlayLayout->addWidget(densityLegend); |
| 820 | + overlayLayout->addWidget(pressureLegend); |
755 | 821 | overlayLayout->addStretch(1); |
756 | 822 | layout->addWidget(overlayBar); |
757 | 823 | canvas->setDensityOverlay(artifacts.densitySummary.peakField.cells.empty() |
758 | 824 | ? artifacts.densitySummary.peakCells |
759 | 825 | : artifacts.densitySummary.peakField.cells, |
760 | 826 | artifacts.densitySummary.highDensityThresholdPeoplePerSquareMeter); |
| 827 | + canvas->setPressureOverlay(artifacts.pressureSummary.peakField.cells.empty() |
| 828 | + ? artifacts.pressureSummary.peakCells |
| 829 | + : artifacts.pressureSummary.peakField.cells, |
| 830 | + std::max( |
| 831 | + artifacts.pressureSummary.hotspotScoreThreshold, |
| 832 | + artifacts.pressureSummary.peakPressureScore)); |
761 | 833 | canvas->setResultOverlayMode(ResultOverlayMode::Density); |
762 | | - QObject::connect(overlayCombo, &QComboBox::currentIndexChanged, panel, [canvas, overlayCombo](int index) { |
| 834 | + QObject::connect(overlayCombo, &QComboBox::currentIndexChanged, panel, [canvas, overlayCombo, densityLegend, pressureLegend](int index) { |
763 | 835 | const auto mode = static_cast<ResultOverlayMode>(overlayCombo->itemData(index).toInt()); |
| 836 | + densityLegend->setVisible(mode == ResultOverlayMode::Density); |
| 837 | + pressureLegend->setVisible(mode == ResultOverlayMode::Pressure); |
764 | 838 | canvas->setResultOverlayMode(mode); |
765 | 839 | }); |
766 | 840 | layout->addWidget(canvas, 1); |
@@ -893,6 +967,14 @@ QWidget* createResultPanel( |
893 | 967 | const auto slowestGroup = artifacts.placementCompletion.empty() |
894 | 968 | ? QString("Pending") |
895 | 969 | : QString::fromStdString(artifacts.placementCompletion.front().placementId); |
| 970 | + const auto peakPressureTooltip = QString( |
| 971 | + "Highest pressure hotspot score observed during the run.%1%2") |
| 972 | + .arg(artifacts.pressureSummary.peakAtSeconds.has_value() |
| 973 | + ? QString("\n\nPeak at %1 sec.").arg(*artifacts.pressureSummary.peakAtSeconds, 0, 'f', 1) |
| 974 | + : QString()) |
| 975 | + .arg(artifacts.pressureSummary.peakCell.has_value() |
| 976 | + ? QString("\nCell floor: %1").arg(QString::fromStdString(artifacts.pressureSummary.peakCell->floorId)) |
| 977 | + : QString()); |
896 | 978 | metricsGrid->addWidget(createMetricCard( |
897 | 979 | "Completion", |
898 | 980 | formatSecondsValue(completionTime), |
@@ -939,6 +1021,27 @@ QWidget* createResultPanel( |
939 | 1021 | formatOptionalSeconds(artifacts.timingSummary.t95Seconds), |
940 | 1022 | panel, |
941 | 1023 | "Time at which 95% of occupants completed evacuation."), 4, 0); |
| 1024 | + metricsGrid->addWidget(createMetricCard( |
| 1025 | + "Peak Pressure", |
| 1026 | + formatPressureScore(artifacts.pressureSummary.peakPressureScore), |
| 1027 | + panel, |
| 1028 | + peakPressureTooltip), 4, 1); |
| 1029 | + metricsGrid->addWidget(createMetricCard( |
| 1030 | + "Pressure Hotspots", |
| 1031 | + QString::number(static_cast<int>(artifacts.pressureSummary.peakHotspots.size())), |
| 1032 | + panel, |
| 1033 | + "Peak number of stored pressure hotspot locations from the run."), 5, 0); |
| 1034 | + metricsGrid->addWidget(createMetricCard( |
| 1035 | + "Pressure Events", |
| 1036 | + QString::number(static_cast<int>(artifacts.pressureSummary.criticalEvents.size())), |
| 1037 | + panel, |
| 1038 | + "Stored sustained critical pressure events that met the duration and agent-count thresholds."), 5, 1); |
| 1039 | + metricsGrid->addWidget(createMetricCard( |
| 1040 | + "Critical Pressure", |
| 1041 | + QString("%1 agents").arg(static_cast<int>(artifacts.pressureSummary.peakCriticalAgentCount)), |
| 1042 | + panel, |
| 1043 | + QString("Peak simultaneously critical agents during the run.\nExposed peak: %1 agents.") |
| 1044 | + .arg(static_cast<int>(artifacts.pressureSummary.peakExposedAgentCount))), 6, 0); |
942 | 1045 | layout->addLayout(metricsGrid); |
943 | 1046 | layout->addStretch(1); |
944 | 1047 |
|
|
0 commit comments