Skip to content

Commit 0634d08

Browse files
Show hazard exposure results
1 parent 618288b commit 0634d08

8 files changed

Lines changed: 633 additions & 7 deletions

src/application/ProjectWorkspaceState.h

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,12 @@ enum class SavedRightPanelMode {
3131
};
3232

3333
enum class SavedResultNavigationView {
34-
Bottleneck,
35-
Hotspot,
36-
Zone,
37-
Groups,
38-
Recommendations,
34+
Bottleneck = 0,
35+
Hotspot = 1,
36+
Zone = 2,
37+
Groups = 3,
38+
Recommendations = 4,
39+
HazardExposure = 5,
3940
};
4041

4142
struct SavedScenarioState {

src/application/ResultArtifactsCodec.cpp

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,107 @@ safecrowd::domain::PlacementCompletionMetric placementCompletionMetricFromJson(c
300300
return placement;
301301
}
302302

303+
QString hazardExposureKindToJson(safecrowd::domain::EnvironmentHazardKind kind) {
304+
switch (kind) {
305+
case safecrowd::domain::EnvironmentHazardKind::Smoke:
306+
return "smoke";
307+
case safecrowd::domain::EnvironmentHazardKind::Fire:
308+
default:
309+
return "fire";
310+
}
311+
}
312+
313+
safecrowd::domain::EnvironmentHazardKind hazardExposureKindFromJson(const QJsonValue& value) {
314+
const auto text = value.toString();
315+
if (text == "smoke") {
316+
return safecrowd::domain::EnvironmentHazardKind::Smoke;
317+
}
318+
if (text == "fire") {
319+
return safecrowd::domain::EnvironmentHazardKind::Fire;
320+
}
321+
return static_cast<safecrowd::domain::EnvironmentHazardKind>(value.toInt(0));
322+
}
323+
324+
QString hazardExposureSeverityToJson(safecrowd::domain::ScenarioElementSeverity severity) {
325+
switch (severity) {
326+
case safecrowd::domain::ScenarioElementSeverity::Low:
327+
return "low";
328+
case safecrowd::domain::ScenarioElementSeverity::High:
329+
return "high";
330+
case safecrowd::domain::ScenarioElementSeverity::Medium:
331+
default:
332+
return "medium";
333+
}
334+
}
335+
336+
safecrowd::domain::ScenarioElementSeverity hazardExposureSeverityFromJson(const QJsonValue& value) {
337+
const auto text = value.toString();
338+
if (text == "low") {
339+
return safecrowd::domain::ScenarioElementSeverity::Low;
340+
}
341+
if (text == "high") {
342+
return safecrowd::domain::ScenarioElementSeverity::High;
343+
}
344+
if (text == "medium") {
345+
return safecrowd::domain::ScenarioElementSeverity::Medium;
346+
}
347+
return static_cast<safecrowd::domain::ScenarioElementSeverity>(value.toInt(1));
348+
}
349+
350+
QJsonObject hazardExposureMetricToJson(const safecrowd::domain::HazardExposureMetric& metric) {
351+
QJsonObject object;
352+
object["hazardId"] = QString::fromStdString(metric.hazardId);
353+
object["hazardName"] = QString::fromStdString(metric.hazardName);
354+
object["kind"] = hazardExposureKindToJson(metric.kind);
355+
object["severity"] = hazardExposureSeverityToJson(metric.severity);
356+
object["affectedZoneId"] = QString::fromStdString(metric.affectedZoneId);
357+
object["floorId"] = QString::fromStdString(metric.floorId);
358+
object["position"] = pointArray(metric.position);
359+
object["exposedAgentSeconds"] = metric.exposedAgentSeconds;
360+
object["peakExposedAgentCount"] = static_cast<qint64>(metric.peakExposedAgentCount);
361+
object["firstExposureSeconds"] = optionalDoubleToJson(metric.firstExposureSeconds);
362+
object["peakAtSeconds"] = optionalDoubleToJson(metric.peakAtSeconds);
363+
object["exposureScore"] = metric.exposureScore;
364+
return object;
365+
}
366+
367+
safecrowd::domain::HazardExposureMetric hazardExposureMetricFromJson(const QJsonObject& object) {
368+
safecrowd::domain::HazardExposureMetric metric;
369+
metric.hazardId = object.value("hazardId").toString().toStdString();
370+
metric.hazardName = object.value("hazardName").toString().toStdString();
371+
metric.kind = hazardExposureKindFromJson(object.value("kind"));
372+
metric.severity = hazardExposureSeverityFromJson(object.value("severity"));
373+
metric.affectedZoneId = object.value("affectedZoneId").toString().toStdString();
374+
metric.floorId = object.value("floorId").toString().toStdString();
375+
metric.position = pointFromJson(object.value("position"));
376+
metric.exposedAgentSeconds = object.value("exposedAgentSeconds").toDouble();
377+
metric.peakExposedAgentCount = static_cast<std::size_t>(object.value("peakExposedAgentCount").toInteger());
378+
metric.firstExposureSeconds = optionalDoubleFromJson(object.value("firstExposureSeconds"));
379+
metric.peakAtSeconds = optionalDoubleFromJson(object.value("peakAtSeconds"));
380+
metric.exposureScore = object.value("exposureScore").toDouble();
381+
return metric;
382+
}
383+
384+
QJsonObject hazardExposureSummaryToJson(const safecrowd::domain::HazardExposureSummary& summary) {
385+
QJsonObject object;
386+
object["totalExposureScore"] = summary.totalExposureScore;
387+
QJsonArray hazards;
388+
for (const auto& metric : summary.hazards) {
389+
hazards.append(hazardExposureMetricToJson(metric));
390+
}
391+
object["hazards"] = hazards;
392+
return object;
393+
}
394+
395+
safecrowd::domain::HazardExposureSummary hazardExposureSummaryFromJson(const QJsonObject& object) {
396+
safecrowd::domain::HazardExposureSummary summary;
397+
summary.totalExposureScore = object.value("totalExposureScore").toDouble();
398+
for (const auto& value : object.value("hazards").toArray()) {
399+
summary.hazards.push_back(hazardExposureMetricFromJson(value.toObject()));
400+
}
401+
return summary;
402+
}
403+
303404
QJsonObject resultArtifactsToJson(const safecrowd::domain::ScenarioResultArtifacts& artifacts) {
304405
QJsonObject object;
305406
QJsonArray progress;
@@ -335,6 +436,7 @@ QJsonObject resultArtifactsToJson(const safecrowd::domain::ScenarioResultArtifac
335436
object["timingSummary"] = timing;
336437

337438
object["densitySummary"] = densitySummaryToJson(artifacts.densitySummary);
439+
object["hazardExposureSummary"] = hazardExposureSummaryToJson(artifacts.hazardExposureSummary);
338440

339441
QJsonArray exitUsage;
340442
for (const auto& exit : artifacts.exitUsage) {
@@ -389,6 +491,10 @@ safecrowd::domain::ScenarioResultArtifacts resultArtifactsFromJson(const QJsonOb
389491
if (object.value("densitySummary").isObject()) {
390492
artifacts.densitySummary = densitySummaryFromJson(object.value("densitySummary").toObject());
391493
}
494+
if (object.value("hazardExposureSummary").isObject()) {
495+
artifacts.hazardExposureSummary =
496+
hazardExposureSummaryFromJson(object.value("hazardExposureSummary").toObject());
497+
}
392498
for (const auto& value : object.value("exitUsage").toArray()) {
393499
artifacts.exitUsage.push_back(exitUsageMetricFromJson(value.toObject()));
394500
}

src/application/ScenarioBatchResultWidget.cpp

Lines changed: 114 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,48 @@ QString formatPressureScore(double score) {
9090
return QString::number(score, 'f', 1);
9191
}
9292

93+
QString formatExposureScore(double score) {
94+
return QString::number(score, 'f', 1);
95+
}
96+
97+
QString formatExposureSeconds(double seconds) {
98+
return QString("%1 agent-sec").arg(seconds, 0, 'f', 1);
99+
}
100+
101+
double hazardExposureSecondsForKind(
102+
const safecrowd::domain::HazardExposureSummary& summary,
103+
safecrowd::domain::EnvironmentHazardKind kind) {
104+
double total = 0.0;
105+
for (const auto& metric : summary.hazards) {
106+
if (metric.kind == kind) {
107+
total += metric.exposedAgentSeconds;
108+
}
109+
}
110+
return total;
111+
}
112+
113+
double totalHazardExposureSeconds(const safecrowd::domain::HazardExposureSummary& summary) {
114+
double total = 0.0;
115+
for (const auto& metric : summary.hazards) {
116+
total += metric.exposedAgentSeconds;
117+
}
118+
return total;
119+
}
120+
121+
const safecrowd::domain::HazardExposureMetric* peakHazardExposureMetric(
122+
const safecrowd::domain::HazardExposureSummary& summary) {
123+
const safecrowd::domain::HazardExposureMetric* best = nullptr;
124+
for (const auto& metric : summary.hazards) {
125+
if (best == nullptr
126+
|| metric.peakExposedAgentCount > best->peakExposedAgentCount
127+
|| (metric.peakExposedAgentCount == best->peakExposedAgentCount
128+
&& metric.exposedAgentSeconds > best->exposedAgentSeconds)) {
129+
best = &metric;
130+
}
131+
}
132+
return best;
133+
}
134+
93135
QTableWidget* createComparisonTable(const QStringList& headers, QWidget* parent) {
94136
auto* table = new QTableWidget(0, headers.size(), parent);
95137
table->setHorizontalHeaderLabels(headers);
@@ -833,6 +875,8 @@ ScenarioResultNavigationView resultNavigationViewFromSaved(SavedResultNavigation
833875
switch (view) {
834876
case SavedResultNavigationView::Hotspot:
835877
return ScenarioResultNavigationView::Hotspot;
878+
case SavedResultNavigationView::HazardExposure:
879+
return ScenarioResultNavigationView::HazardExposure;
836880
case SavedResultNavigationView::Zone:
837881
return ScenarioResultNavigationView::Zone;
838882
case SavedResultNavigationView::Groups:
@@ -849,6 +893,8 @@ SavedResultNavigationView savedResultNavigationView(ScenarioResultNavigationView
849893
switch (view) {
850894
case ScenarioResultNavigationView::Hotspot:
851895
return SavedResultNavigationView::Hotspot;
896+
case ScenarioResultNavigationView::HazardExposure:
897+
return SavedResultNavigationView::HazardExposure;
852898
case ScenarioResultNavigationView::Zone:
853899
return SavedResultNavigationView::Zone;
854900
case ScenarioResultNavigationView::Groups:
@@ -1012,6 +1058,7 @@ QWidget* ScenarioBatchResultWidget::createCanvasPanel() {
10121058
auto* tabs = new QTabWidget(graphPanel);
10131059
remainingChart_ = new ComparisonGraphWidget(ComparisonGraphMode::Remaining, tabs);
10141060
exitsChart_ = new ComparisonGraphWidget(ComparisonGraphMode::Exits, tabs);
1061+
exposureTable_ = createComparisonTable({"Scenario", "Total", "Fire", "Smoke", "Peak", "Peak at"}, tabs);
10151062
pressureTable_ = createComparisonTable({"Scenario", "Peak score", "Exposed / Critical", "Hotspots", "Events", "Peak at"}, tabs);
10161063
static_cast<ComparisonGraphWidget*>(remainingChart_)->setResults(results_, selectedCompareIndices_, currentResultIndex_);
10171064
static_cast<ComparisonGraphWidget*>(exitsChart_)->setResults(results_, selectedCompareIndices_, currentResultIndex_);
@@ -1020,6 +1067,7 @@ QWidget* ScenarioBatchResultWidget::createCanvasPanel() {
10201067
});
10211068
tabs->addTab(remainingChart_, "Remaining");
10221069
tabs->addTab(exitsChart_, "Exits");
1070+
tabs->addTab(exposureTable_, "Exposure");
10231071
tabs->addTab(pressureTable_, "Pressure");
10241072
graphLayout->addWidget(tabs, 1);
10251073
layout->addWidget(graphPanel, 1);
@@ -1087,7 +1135,7 @@ QWidget* ScenarioBatchResultWidget::createSummaryPanel() {
10871135
layout->setContentsMargins(0, 0, 10, 0);
10881136
layout->setSpacing(12);
10891137

1090-
auto* intro = createLabel("Choose which completed scenarios appear together in the comparison graphs and pressure summary table.", content, ui::FontRole::Caption);
1138+
auto* intro = createLabel("Choose which completed scenarios appear together in the comparison graphs and risk summary tables.", content, ui::FontRole::Caption);
10911139
intro->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
10921140
intro->setStyleSheet(ui::mutedTextStyleSheet());
10931141
layout->addWidget(intro);
@@ -1517,6 +1565,7 @@ void ScenarioBatchResultWidget::refreshComparisonSelection() {
15171565
static_cast<ComparisonGraphWidget*>(exitsChart_)->setResults(results_, selectedCompareIndices_, currentResultIndex_);
15181566
}
15191567
refreshComparisonCountLabel();
1568+
refreshExposureComparisonTable();
15201569
refreshPressureComparisonTable();
15211570
}
15221571

@@ -1529,6 +1578,53 @@ void ScenarioBatchResultWidget::refreshComparisonCountLabel() {
15291578
.arg(static_cast<int>(results_.size())));
15301579
}
15311580

1581+
void ScenarioBatchResultWidget::refreshExposureComparisonTable() {
1582+
if (exposureTable_ == nullptr) {
1583+
return;
1584+
}
1585+
1586+
const std::vector<int> visibleIndices = selectedCompareIndices_;
1587+
exposureTable_->setRowCount(static_cast<int>(visibleIndices.size()));
1588+
for (int row = 0; row < static_cast<int>(visibleIndices.size()); ++row) {
1589+
const auto index = visibleIndices[static_cast<std::size_t>(row)];
1590+
if (index < 0 || index >= static_cast<int>(results_.size())) {
1591+
continue;
1592+
}
1593+
1594+
const auto& result = results_[static_cast<std::size_t>(index)];
1595+
const auto& summary = result.artifacts.hazardExposureSummary;
1596+
const bool emphasized = index == currentResultIndex_;
1597+
const auto* peakHazard = peakHazardExposureMetric(summary);
1598+
exposureTable_->setItem(row, 0, tableItem(QString::fromStdString(result.scenario.name), emphasized));
1599+
exposureTable_->setItem(row, 1, tableItem(formatExposureSeconds(totalHazardExposureSeconds(summary)), emphasized));
1600+
exposureTable_->setItem(
1601+
row,
1602+
2,
1603+
tableItem(formatExposureSeconds(hazardExposureSecondsForKind(
1604+
summary,
1605+
safecrowd::domain::EnvironmentHazardKind::Fire)), emphasized));
1606+
exposureTable_->setItem(
1607+
row,
1608+
3,
1609+
tableItem(formatExposureSeconds(hazardExposureSecondsForKind(
1610+
summary,
1611+
safecrowd::domain::EnvironmentHazardKind::Smoke)), emphasized));
1612+
exposureTable_->setItem(
1613+
row,
1614+
4,
1615+
tableItem(
1616+
peakHazard == nullptr
1617+
? QString("0 people")
1618+
: QString("%1 people").arg(static_cast<int>(peakHazard->peakExposedAgentCount)),
1619+
emphasized));
1620+
exposureTable_->setItem(
1621+
row,
1622+
5,
1623+
tableItem(peakHazard == nullptr ? QString("Pending") : formatSeconds(peakHazard->peakAtSeconds), emphasized));
1624+
}
1625+
exposureTable_->resizeRowsToContents();
1626+
}
1627+
15321628
void ScenarioBatchResultWidget::refreshPressureComparisonTable() {
15331629
if (pressureTable_ == nullptr) {
15341630
return;
@@ -1610,6 +1706,7 @@ void ScenarioBatchResultWidget::setComparisonSelection(std::vector<int> indices)
16101706
static_cast<ComparisonGraphWidget*>(exitsChart_)->setResults(results_, selectedCompareIndices_, currentResultIndex_);
16111707
}
16121708
refreshComparisonCountLabel();
1709+
refreshExposureComparisonTable();
16131710
refreshPressureComparisonTable();
16141711
}
16151712

@@ -1892,6 +1989,7 @@ void ScenarioBatchResultWidget::refreshSelectedResult() {
18921989
if (exitsChart_ != nullptr) {
18931990
static_cast<ComparisonGraphWidget*>(exitsChart_)->setResults(results_, selectedCompareIndices_, currentResultIndex_);
18941991
}
1992+
refreshExposureComparisonTable();
18951993
refreshPressureComparisonTable();
18961994
refreshResultNavigationPanel();
18971995
if (detailLabel_ != nullptr) {
@@ -1901,7 +1999,9 @@ void ScenarioBatchResultWidget::refreshSelectedResult() {
19011999
? QString("Baseline")
19022000
: formatDeltaSeconds(selectedFinalSeconds - finalSeconds(results_[static_cast<std::size_t>(baselineIndex)])))
19032001
: QString("No baseline");
1904-
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")
2002+
const auto& exposureSummary = result.artifacts.hazardExposureSummary;
2003+
const auto* peakHazard = peakHazardExposureMetric(exposureSummary);
2004+
detailLabel_->setText(QString("%1 (%2)\nFinal: %3\nDelta vs baseline: %4\nEvacuated: %5 / %6 (%7)\nRisk: %8\nHotspots: %9\nBottlenecks: %10\nHazard exposure: %11 (fire %12 / smoke %13)\nHazard peak: %14 at %15; score %16\nPressure hotspots: %17\nCritical pressure: %18 agents / %19 events\nPeak pressure: %20 at %21\nT90 / T95: %22 / %23")
19052005
.arg(QString::fromStdString(result.scenario.name))
19062006
.arg(scenarioRoleLabel(result.scenario.role))
19072007
.arg(formatSeconds(selectedFinalSeconds))
@@ -1912,6 +2012,18 @@ void ScenarioBatchResultWidget::refreshSelectedResult() {
19122012
.arg(safecrowd::domain::scenarioRiskLevelLabel(result.risk.completionRisk))
19132013
.arg(static_cast<int>(result.risk.hotspots.size()))
19142014
.arg(static_cast<int>(result.risk.bottlenecks.size()))
2015+
.arg(formatExposureSeconds(totalHazardExposureSeconds(exposureSummary)))
2016+
.arg(formatExposureSeconds(hazardExposureSecondsForKind(
2017+
exposureSummary,
2018+
safecrowd::domain::EnvironmentHazardKind::Fire)))
2019+
.arg(formatExposureSeconds(hazardExposureSecondsForKind(
2020+
exposureSummary,
2021+
safecrowd::domain::EnvironmentHazardKind::Smoke)))
2022+
.arg(peakHazard == nullptr
2023+
? QString("0 people")
2024+
: QString("%1 people").arg(static_cast<int>(peakHazard->peakExposedAgentCount)))
2025+
.arg(peakHazard == nullptr ? QString("Pending") : formatSeconds(peakHazard->peakAtSeconds))
2026+
.arg(formatExposureScore(exposureSummary.totalExposureScore))
19152027
.arg(static_cast<int>(result.artifacts.pressureSummary.peakHotspots.size()))
19162028
.arg(static_cast<int>(result.artifacts.pressureSummary.peakCriticalAgentCount))
19172029
.arg(static_cast<int>(result.artifacts.pressureSummary.criticalEvents.size()))

src/application/ScenarioBatchResultWidget.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ class ScenarioBatchResultWidget : public QWidget {
6767
void pauseReplay();
6868
void refreshComparisonSelection();
6969
void refreshComparisonCountLabel();
70+
void refreshExposureComparisonTable();
7071
void refreshPressureComparisonTable();
7172
void refreshResultNavigationPanel();
7273
void refreshSelectedResult();
@@ -107,6 +108,7 @@ class ScenarioBatchResultWidget : public QWidget {
107108
QLabel* replayTimeLabel_{nullptr};
108109
QLabel* detailLabel_{nullptr};
109110
QLabel* comparisonCountLabel_{nullptr};
111+
QTableWidget* exposureTable_{nullptr};
110112
QTableWidget* pressureTable_{nullptr};
111113
std::vector<QCheckBox*> compareCheckBoxes_{};
112114
QWidget* remainingChart_{nullptr};

0 commit comments

Comments
 (0)