Skip to content

Commit d2bc4f8

Browse files
[Application] Visualize hazard closure replay states
1 parent 0e4b6c9 commit d2bc4f8

1 file changed

Lines changed: 135 additions & 24 deletions

File tree

src/application/SimulationCanvasWidget.cpp

Lines changed: 135 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ constexpr int kFloorSelectorMargin = 14;
4040
const QColor kMovingAgentColor("#1f5fae");
4141
const QColor kStalledAgentColor("#7c3aed");
4242

43+
enum class TimelineVisualState {
44+
Future,
45+
Active,
46+
Expired,
47+
};
48+
4349
std::string defaultFloorId(const safecrowd::domain::FacilityLayout2D& layout) {
4450
if (!layout.floors.empty() && !layout.floors.front().id.empty()) {
4551
return layout.floors.front().id;
@@ -90,6 +96,39 @@ QString formatEnvironmentHazardTooltip(const safecrowd::domain::EnvironmentHazar
9096
return text;
9197
}
9298

99+
QString visualStateLabel(TimelineVisualState state) {
100+
switch (state) {
101+
case TimelineVisualState::Future:
102+
return QStringLiteral("Future");
103+
case TimelineVisualState::Expired:
104+
return QStringLiteral("Expired");
105+
case TimelineVisualState::Active:
106+
default:
107+
return QStringLiteral("Active");
108+
}
109+
}
110+
111+
std::optional<TimelineVisualState> environmentHazardVisualState(
112+
const safecrowd::domain::EnvironmentHazardDraft& hazard,
113+
double elapsedSeconds) {
114+
if (safecrowd::domain::environmentHazardActiveAt(hazard, elapsedSeconds)) {
115+
return TimelineVisualState::Active;
116+
}
117+
const auto start = std::max(0.0, hazard.startSeconds);
118+
if (elapsedSeconds + 1e-9 < start) {
119+
return TimelineVisualState::Future;
120+
}
121+
return TimelineVisualState::Expired;
122+
}
123+
124+
QString formatEnvironmentHazardTooltip(
125+
const safecrowd::domain::EnvironmentHazardDraft& hazard,
126+
TimelineVisualState state) {
127+
auto text = formatEnvironmentHazardTooltip(hazard);
128+
text.append(QString("\nState: %1").arg(visualStateLabel(state)));
129+
return text;
130+
}
131+
93132
safecrowd::domain::Point2D connectionCenter(const safecrowd::domain::Connection2D& connection) {
94133
return {
95134
.x = (connection.centerSpan.start.x + connection.centerSpan.end.x) * 0.5,
@@ -159,9 +198,38 @@ QString formatScheduleTooltip(const safecrowd::domain::ConnectionBlockDraft& blo
159198

160199
for (const auto& interval : block.intervals) {
161200
const auto start = std::max(0.0, interval.startSeconds);
162-
const auto end = std::max(start, interval.endSeconds);
163-
text.append(QString("\n- %1s ~ %2s").arg(start, 0, 'f', 1).arg(end, 0, 'f', 1));
201+
if (interval.endSeconds <= interval.startSeconds) {
202+
text.append(QString("\n- %1s ~ open").arg(start, 0, 'f', 1));
203+
} else {
204+
text.append(QString("\n- %1s ~ %2s").arg(start, 0, 'f', 1).arg(std::max(start, interval.endSeconds), 0, 'f', 1));
205+
}
206+
}
207+
return text;
208+
}
209+
210+
std::optional<TimelineVisualState> connectionBlockVisualState(
211+
const safecrowd::domain::ConnectionBlockDraft& block,
212+
double elapsedSeconds) {
213+
if (block.connectionId.empty()) {
214+
return std::nullopt;
164215
}
216+
if (safecrowd::domain::connectionBlockActiveAt(block, elapsedSeconds)) {
217+
return TimelineVisualState::Active;
218+
}
219+
if (block.intervals.empty()) {
220+
return TimelineVisualState::Active;
221+
}
222+
const auto hasFutureInterval = std::any_of(block.intervals.begin(), block.intervals.end(), [&](const auto& interval) {
223+
return elapsedSeconds + 1e-9 < std::max(0.0, interval.startSeconds);
224+
});
225+
return hasFutureInterval ? TimelineVisualState::Future : TimelineVisualState::Expired;
226+
}
227+
228+
QString formatScheduleTooltip(
229+
const safecrowd::domain::ConnectionBlockDraft& block,
230+
TimelineVisualState state) {
231+
auto text = formatScheduleTooltip(block);
232+
text.append(QString("\nState: %1").arg(visualStateLabel(state)));
165233
return text;
166234
}
167235

@@ -182,7 +250,7 @@ QString formatRouteGuidanceTooltip(const safecrowd::domain::RouteGuidanceDraft&
182250
return text;
183251
}
184252

185-
std::optional<std::size_t> hoveredBlockedConnectionIndex(
253+
std::optional<std::size_t> hoveredConnectionBlockIndex(
186254
const safecrowd::domain::FacilityLayout2D& layout,
187255
const std::vector<safecrowd::domain::ConnectionBlockDraft>& blocks,
188256
const LayoutCanvasTransform& transform,
@@ -196,7 +264,7 @@ std::optional<std::size_t> hoveredBlockedConnectionIndex(
196264

197265
for (std::size_t index = 0; index < blocks.size(); ++index) {
198266
const auto& block = blocks[index];
199-
if (!safecrowd::domain::connectionBlockActiveAt(block, elapsedSeconds)) {
267+
if (!connectionBlockVisualState(block, elapsedSeconds).has_value()) {
200268
continue;
201269
}
202270
const auto it = std::find_if(layout.connections.begin(), layout.connections.end(), [&](const auto& connection) {
@@ -222,7 +290,7 @@ std::optional<std::size_t> hoveredBlockedConnectionIndex(
222290
return closestIndex;
223291
}
224292

225-
std::optional<std::size_t> hoveredActiveEnvironmentHazardIndex(
293+
std::optional<std::size_t> hoveredEnvironmentHazardIndex(
226294
const safecrowd::domain::FacilityLayout2D& layout,
227295
const std::vector<safecrowd::domain::EnvironmentHazardDraft>& hazards,
228296
const LayoutCanvasTransform& transform,
@@ -235,7 +303,7 @@ std::optional<std::size_t> hoveredActiveEnvironmentHazardIndex(
235303
double closestDistanceSq = kHoverRadiusPixels * kHoverRadiusPixels;
236304
for (std::size_t index = 0; index < hazards.size(); ++index) {
237305
const auto& hazard = hazards[index];
238-
if (!safecrowd::domain::environmentHazardActiveAt(hazard, elapsedSeconds)) {
306+
if (!environmentHazardVisualState(hazard, elapsedSeconds).has_value()) {
239307
continue;
240308
}
241309
if (!matchesFloor(safecrowd::domain::environmentHazardFloorId(layout, hazard), currentFloorId)) {
@@ -342,7 +410,7 @@ std::optional<QPointF> routeGuidanceMarkerCenter(
342410
std::vector<QPointF> blockedCenters;
343411
blockedCenters.reserve(blocks.size());
344412
for (const auto& block : blocks) {
345-
if (!safecrowd::domain::connectionBlockActiveAt(block, elapsedSeconds)) {
413+
if (!connectionBlockVisualState(block, elapsedSeconds).has_value()) {
346414
continue;
347415
}
348416
const auto connectionIt = std::find_if(layout.connections.begin(), layout.connections.end(), [&](const auto& connection) {
@@ -618,14 +686,14 @@ void SimulationCanvasWidget::mouseMoveEvent(QMouseEvent* event) {
618686
currentFloorId_,
619687
elapsedSeconds,
620688
event->position());
621-
const auto hoveredIndex = hoveredBlockedConnectionIndex(
689+
const auto hoveredIndex = hoveredConnectionBlockIndex(
622690
layout_,
623691
connectionBlocks_,
624692
transform,
625693
currentFloorId_,
626694
elapsedSeconds,
627695
event->position());
628-
const auto hoveredHazard = hoveredActiveEnvironmentHazardIndex(
696+
const auto hoveredHazard = hoveredEnvironmentHazardIndex(
629697
layout_,
630698
environmentHazards_,
631699
transform,
@@ -651,7 +719,8 @@ void SimulationCanvasWidget::mouseMoveEvent(QMouseEvent* event) {
651719

652720
if (hoveredIndex.has_value()) {
653721
const auto& block = connectionBlocks_[*hoveredIndex];
654-
const auto tooltip = formatScheduleTooltip(block);
722+
const auto state = connectionBlockVisualState(block, elapsedSeconds);
723+
const auto tooltip = state.has_value() ? formatScheduleTooltip(block, *state) : formatScheduleTooltip(block);
655724
if (tooltip.isEmpty()) {
656725
QWidget::mouseMoveEvent(event);
657726
return;
@@ -670,7 +739,10 @@ void SimulationCanvasWidget::mouseMoveEvent(QMouseEvent* event) {
670739

671740
if (hoveredHazard.has_value()) {
672741
const auto& hazard = environmentHazards_[*hoveredHazard];
673-
const auto tooltip = formatEnvironmentHazardTooltip(hazard);
742+
const auto state = environmentHazardVisualState(hazard, elapsedSeconds);
743+
const auto tooltip = state.has_value()
744+
? formatEnvironmentHazardTooltip(hazard, *state)
745+
: formatEnvironmentHazardTooltip(hazard);
674746
const auto hoveredId = hazard.id.empty()
675747
? QString("%1:%2:%3")
676748
.arg(hazardKindLabel(hazard.kind))
@@ -861,11 +933,9 @@ void SimulationCanvasWidget::drawConnectionBlockOverlay(QPainter& painter, const
861933
const auto elapsedSeconds = std::max(0.0, frame_.elapsedSeconds);
862934

863935
painter.save();
864-
painter.setBrush(Qt::NoBrush);
865-
painter.setPen(QPen(QColor("#c0392b"), 2.8, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
866-
867936
for (const auto& block : connectionBlocks_) {
868-
if (!safecrowd::domain::connectionBlockActiveAt(block, elapsedSeconds)) {
937+
const auto state = connectionBlockVisualState(block, elapsedSeconds);
938+
if (!state.has_value()) {
869939
continue;
870940
}
871941

@@ -880,9 +950,32 @@ void SimulationCanvasWidget::drawConnectionBlockOverlay(QPainter& painter, const
880950
}
881951

882952
const auto center = transform.map(connectionCenter(*it));
953+
QColor color("#c0392b");
954+
Qt::PenStyle penStyle = Qt::SolidLine;
955+
double penWidth = 2.8;
956+
if (*state == TimelineVisualState::Future) {
957+
color = QColor("#64748b");
958+
penStyle = Qt::DashLine;
959+
penWidth = 2.2;
960+
} else if (*state == TimelineVisualState::Expired) {
961+
color = QColor(100, 116, 139, 120);
962+
penStyle = Qt::DotLine;
963+
penWidth = 2.0;
964+
}
965+
883966
const double r = 10.0;
967+
painter.setBrush(Qt::NoBrush);
968+
painter.setPen(QPen(color, penWidth, penStyle, Qt::RoundCap, Qt::RoundJoin));
884969
painter.drawEllipse(center, r, r);
885-
painter.drawLine(QPointF(center.x() - 6.5, center.y() + 6.5), QPointF(center.x() + 6.5, center.y() - 6.5));
970+
if (*state == TimelineVisualState::Future) {
971+
painter.drawLine(center, QPointF(center.x(), center.y() - 5.8));
972+
painter.drawLine(center, QPointF(center.x() + 5.2, center.y()));
973+
} else if (*state == TimelineVisualState::Expired) {
974+
painter.drawLine(QPointF(center.x() - 5.6, center.y() + 0.4), QPointF(center.x() - 1.8, center.y() + 4.2));
975+
painter.drawLine(QPointF(center.x() - 1.8, center.y() + 4.2), QPointF(center.x() + 6.0, center.y() - 5.0));
976+
} else {
977+
painter.drawLine(QPointF(center.x() - 6.5, center.y() + 6.5), QPointF(center.x() + 6.5, center.y() - 6.5));
978+
}
886979
}
887980

888981
painter.restore();
@@ -900,7 +993,8 @@ void SimulationCanvasWidget::drawEnvironmentHazardOverlay(QPainter& painter, con
900993
painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
901994

902995
for (const auto& hazard : environmentHazards_) {
903-
if (!safecrowd::domain::environmentHazardActiveAt(hazard, elapsedSeconds)) {
996+
const auto state = environmentHazardVisualState(hazard, elapsedSeconds);
997+
if (!state.has_value()) {
904998
continue;
905999
}
9061000
if (!matchesFloor(safecrowd::domain::environmentHazardFloorId(layout_, hazard), currentFloorId_)) {
@@ -918,9 +1012,27 @@ void SimulationCanvasWidget::drawEnvironmentHazardOverlay(QPainter& painter, con
9181012
std::hypot(radiusAnchor.x() - center.x(), radiusAnchor.y() - center.y()));
9191013

9201014
const auto isFire = hazard.kind == safecrowd::domain::EnvironmentHazardKind::Fire;
921-
const QColor core = isFire ? QColor(220, 38, 38, 110) : QColor(71, 85, 105, 92);
922-
const QColor mid = isFire ? QColor(249, 115, 22, 46) : QColor(148, 163, 184, 40);
923-
const QColor edge = isFire ? QColor(249, 115, 22, 0) : QColor(148, 163, 184, 0);
1015+
QColor core = isFire ? QColor(220, 38, 38, 110) : QColor(71, 85, 105, 92);
1016+
QColor mid = isFire ? QColor(249, 115, 22, 46) : QColor(148, 163, 184, 40);
1017+
QColor edge = isFire ? QColor(249, 115, 22, 0) : QColor(148, 163, 184, 0);
1018+
QColor outline = isFire ? QColor(185, 28, 28, 180) : QColor(71, 85, 105, 165);
1019+
Qt::PenStyle outlineStyle = Qt::DashLine;
1020+
QColor markerFill = isFire ? QColor("#c2410c") : QColor("#64748b");
1021+
if (*state == TimelineVisualState::Future) {
1022+
core = isFire ? QColor(220, 38, 38, 42) : QColor(71, 85, 105, 34);
1023+
mid = isFire ? QColor(249, 115, 22, 16) : QColor(148, 163, 184, 14);
1024+
edge = QColor(0, 0, 0, 0);
1025+
outline = QColor(100, 116, 139, 135);
1026+
outlineStyle = Qt::DashLine;
1027+
markerFill = QColor(100, 116, 139, 170);
1028+
} else if (*state == TimelineVisualState::Expired) {
1029+
core = QColor(100, 116, 139, 28);
1030+
mid = QColor(100, 116, 139, 10);
1031+
edge = QColor(100, 116, 139, 0);
1032+
outline = QColor(100, 116, 139, 90);
1033+
outlineStyle = Qt::DotLine;
1034+
markerFill = QColor(100, 116, 139, 115);
1035+
}
9241036
QRadialGradient gradient(center, radius);
9251037
gradient.setColorAt(0.0, core);
9261038
gradient.setColorAt(0.48, mid);
@@ -931,16 +1043,15 @@ void SimulationCanvasWidget::drawEnvironmentHazardOverlay(QPainter& painter, con
9311043

9321044
painter.setBrush(Qt::NoBrush);
9331045
painter.setPen(QPen(
934-
isFire ? QColor(185, 28, 28, 180) : QColor(71, 85, 105, 165),
1046+
outline,
9351047
1.8,
936-
Qt::DashLine,
1048+
outlineStyle,
9371049
Qt::RoundCap,
9381050
Qt::RoundJoin));
9391051
painter.drawEllipse(center, radius, radius);
9401052

941-
const QColor fill = isFire ? QColor("#c2410c") : QColor("#64748b");
9421053
painter.setPen(Qt::NoPen);
943-
painter.setBrush(fill);
1054+
painter.setBrush(markerFill);
9441055
painter.drawEllipse(center, 11.0, 11.0);
9451056

9461057
painter.setPen(QPen(Qt::white, 2.0, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));

0 commit comments

Comments
 (0)