Skip to content

Commit 622dace

Browse files
authored
[Application] Seek replay to exact T90/T95 (#181)
## Summary - t90, t95 시점을 그래프에서 클릭해 리플레이를 해당 시점으로 이동(상태는 pause) - T90/T95 키프레임을 evacuation 전환 시점에서 캡처해 시간/상태 오차 제거 Closes #182
1 parent a38d38a commit 622dace

6 files changed

Lines changed: 260 additions & 19 deletions

src/application/ProjectPersistence.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1040,6 +1040,12 @@ QJsonObject resultArtifactsToJson(const safecrowd::domain::ScenarioResultArtifac
10401040
timing["t90Seconds"] = optionalDoubleToJson(artifacts.timingSummary.t90Seconds);
10411041
timing["t95Seconds"] = optionalDoubleToJson(artifacts.timingSummary.t95Seconds);
10421042
timing["finalEvacuationTimeSeconds"] = optionalDoubleToJson(artifacts.timingSummary.finalEvacuationTimeSeconds);
1043+
if (artifacts.timingSummary.t90Frame.has_value()) {
1044+
timing["t90Frame"] = simulationFrameToJson(*artifacts.timingSummary.t90Frame);
1045+
}
1046+
if (artifacts.timingSummary.t95Frame.has_value()) {
1047+
timing["t95Frame"] = simulationFrameToJson(*artifacts.timingSummary.t95Frame);
1048+
}
10431049
object["timingSummary"] = timing;
10441050
return object;
10451051
}
@@ -1064,6 +1070,12 @@ safecrowd::domain::ScenarioResultArtifacts resultArtifactsFromJson(const QJsonOb
10641070
artifacts.timingSummary.t90Seconds = optionalDoubleFromJson(timing.value("t90Seconds"));
10651071
artifacts.timingSummary.t95Seconds = optionalDoubleFromJson(timing.value("t95Seconds"));
10661072
artifacts.timingSummary.finalEvacuationTimeSeconds = optionalDoubleFromJson(timing.value("finalEvacuationTimeSeconds"));
1073+
if (timing.value("t90Frame").isObject()) {
1074+
artifacts.timingSummary.t90Frame = simulationFrameFromJson(timing.value("t90Frame").toObject());
1075+
}
1076+
if (timing.value("t95Frame").isObject()) {
1077+
artifacts.timingSummary.t95Frame = simulationFrameFromJson(timing.value("t95Frame").toObject());
1078+
}
10671079
return artifacts;
10681080
}
10691081

src/application/ScenarioResultWidget.cpp

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include <algorithm>
44
#include <cmath>
55
#include <cstddef>
6+
#include <functional>
67
#include <utility>
78

89
#include <QAbstractItemView>
@@ -15,9 +16,11 @@
1516
#include <QIcon>
1617
#include <QLabel>
1718
#include <QLinearGradient>
19+
#include <QMouseEvent>
1820
#include <QPainter>
1921
#include <QPainterPath>
2022
#include <QPixmap>
23+
#include <QPointer>
2124
#include <QPushButton>
2225
#include <QScrollArea>
2326
#include <QSizePolicy>
@@ -97,13 +100,18 @@ class EvacuationProgressWidget final : public QWidget {
97100
setMinimumHeight(150);
98101
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
99102
setToolTip("Remaining occupant curve. T90/T95 indicate when 90%/95% of occupants have evacuated.");
103+
setMouseTracking(true);
100104
}
101105

102106
void setCurrentTimeSeconds(std::optional<double> seconds) {
103107
currentTimeSeconds_ = seconds;
104108
update();
105109
}
106110

111+
void setTimingMarkerActivatedHandler(std::function<void(double)> handler) {
112+
timingMarkerActivatedHandler_ = std::move(handler);
113+
}
114+
107115
protected:
108116
void paintEvent(QPaintEvent* event) override {
109117
(void)event;
@@ -175,10 +183,99 @@ class EvacuationProgressWidget final : public QWidget {
175183
QString("%1 / %2 remaining by %3 sec")
176184
.arg(static_cast<int>(remainingCount))
177185
.arg(static_cast<int>(last.totalCount))
178-
.arg(last.timeSeconds, 0, 'f', 1));
186+
.arg(last.timeSeconds, 0, 'f', 1));
187+
}
188+
189+
void mouseMoveEvent(QMouseEvent* event) override {
190+
if (event == nullptr) {
191+
return;
192+
}
193+
const bool hover = timingMarkerHitSeconds(event->position()).has_value();
194+
setCursor(hover ? Qt::PointingHandCursor : Qt::ArrowCursor);
195+
QWidget::mouseMoveEvent(event);
196+
}
197+
198+
void leaveEvent(QEvent* event) override {
199+
unsetCursor();
200+
QWidget::leaveEvent(event);
201+
}
202+
203+
void mousePressEvent(QMouseEvent* event) override {
204+
if (event == nullptr || event->button() != Qt::LeftButton) {
205+
QWidget::mousePressEvent(event);
206+
return;
207+
}
208+
const auto seconds = timingMarkerHitSeconds(event->position());
209+
if (!seconds.has_value()) {
210+
QWidget::mousePressEvent(event);
211+
return;
212+
}
213+
event->accept();
214+
if (timingMarkerActivatedHandler_) {
215+
timingMarkerActivatedHandler_(*seconds);
216+
}
179217
}
180218

181219
private:
220+
QRectF plotRect() const {
221+
return QRectF(rect()).adjusted(34, 18, -14, -28);
222+
}
223+
224+
double maxTimeSeconds() const {
225+
if (artifacts_.evacuationProgress.empty()) {
226+
return 1.0;
227+
}
228+
const auto maxTimeIt = std::max_element(
229+
artifacts_.evacuationProgress.begin(),
230+
artifacts_.evacuationProgress.end(),
231+
[](const auto& lhs, const auto& rhs) {
232+
return lhs.timeSeconds < rhs.timeSeconds;
233+
});
234+
return std::max(1.0, maxTimeIt->timeSeconds);
235+
}
236+
237+
QRectF markerHitRegion(const QRectF& plot, double maxTime, double seconds) const {
238+
const auto x = plot.left() + (std::clamp(seconds / maxTime, 0.0, 1.0) * plot.width());
239+
const QRectF lineHit(x - 6.0, plot.top(), 12.0, plot.height());
240+
const QRectF labelHit(x - 4.0, plot.top() - 2.0, 70.0, 22.0);
241+
return lineHit.united(labelHit);
242+
}
243+
244+
std::optional<double> timingMarkerHitSeconds(const QPointF& position) const {
245+
const QRectF plot = plotRect();
246+
if (!plot.contains(position)) {
247+
return std::nullopt;
248+
}
249+
250+
const auto maxTime = maxTimeSeconds();
251+
struct Candidate {
252+
double seconds;
253+
double distance;
254+
};
255+
std::optional<Candidate> best;
256+
auto consider = [&](const std::optional<double>& markerSeconds) {
257+
if (!markerSeconds.has_value()) {
258+
return;
259+
}
260+
const auto region = markerHitRegion(plot, maxTime, *markerSeconds);
261+
if (!region.contains(position)) {
262+
return;
263+
}
264+
const auto x = plot.left() + (std::clamp(*markerSeconds / maxTime, 0.0, 1.0) * plot.width());
265+
const auto distance = std::abs(position.x() - x);
266+
if (!best.has_value() || distance < best->distance) {
267+
best = Candidate{.seconds = *markerSeconds, .distance = distance};
268+
}
269+
};
270+
271+
consider(artifacts_.timingSummary.t90Seconds);
272+
consider(artifacts_.timingSummary.t95Seconds);
273+
if (!best.has_value()) {
274+
return std::nullopt;
275+
}
276+
return best->seconds;
277+
}
278+
182279
void drawTimingMarker(
183280
QPainter& painter,
184281
const QRectF& plot,
@@ -215,6 +312,7 @@ class EvacuationProgressWidget final : public QWidget {
215312

216313
safecrowd::domain::ScenarioResultArtifacts artifacts_{};
217314
std::optional<double> currentTimeSeconds_{};
315+
std::function<void(double)> timingMarkerActivatedHandler_{};
218316
};
219317

220318
class DensityLegendWidget final : public QWidget {
@@ -970,6 +1068,30 @@ QWidget* createResultCanvasPanel(
9701068
if (replayControlsOut != nullptr) {
9711069
*replayControlsOut = replayControls;
9721070
}
1071+
if (progressWidget != nullptr) {
1072+
const QPointer<ResultReplayControls> replayControlsGuard(replayControls);
1073+
const auto t90Seconds = artifacts.timingSummary.t90Seconds;
1074+
const auto t95Seconds = artifacts.timingSummary.t95Seconds;
1075+
const auto t90Frame = artifacts.timingSummary.t90Frame;
1076+
const auto t95Frame = artifacts.timingSummary.t95Frame;
1077+
progressWidget->setTimingMarkerActivatedHandler([replayControlsGuard, t90Seconds, t95Seconds, t90Frame, t95Frame](double seconds) {
1078+
if (replayControlsGuard != nullptr) {
1079+
if (t90Seconds.has_value()
1080+
&& t90Frame.has_value()
1081+
&& std::abs(seconds - *t90Seconds) <= 1e-6) {
1082+
replayControlsGuard->showFrame(*t90Frame);
1083+
return;
1084+
}
1085+
if (t95Seconds.has_value()
1086+
&& t95Frame.has_value()
1087+
&& std::abs(seconds - *t95Seconds) <= 1e-6) {
1088+
replayControlsGuard->showFrame(*t95Frame);
1089+
return;
1090+
}
1091+
replayControlsGuard->showClosestFrameAtSeconds(seconds);
1092+
}
1093+
});
1094+
}
9731095
layout->addWidget(replayControls);
9741096
layout->addWidget(graphPanel, 1);
9751097
return panel;

src/domain/ScenarioResultArtifacts.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ struct EvacuationTimingSummary {
2424
std::optional<double> finalEvacuationTimeSeconds{};
2525
double targetTimeSeconds{0.0};
2626
std::optional<double> marginSeconds{};
27+
std::optional<SimulationFrame> t90Frame{};
28+
std::optional<SimulationFrame> t95Frame{};
2729
};
2830

2931
struct DensityCellMetric {

src/domain/ScenarioSimulationMotionSystem.cpp

Lines changed: 102 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -68,30 +68,113 @@ class ScenarioSimulationMotionSystem final : public engine::EngineSystem {
6868
advanceRoutesForWaypointProgress(query, 0.0, entities, layoutCache);
6969
replanBlockedExitRoutes(query, entities, layoutCache, clock.elapsedSeconds, layoutRevision);
7070
replanBlockedRouteSegments(query, entities, layoutCache, clock.elapsedSeconds, layoutRevision);
71-
72-
for (const auto entity : entities) {
73-
auto& position = query.get<Position>(entity);
74-
const auto& agent = query.get<Agent>(entity);
75-
auto& velocity = query.get<Velocity>(entity);
76-
auto& route = query.get<EvacuationRoute>(entity);
77-
auto& status = query.get<EvacuationStatus>(entity);
71+
72+
if (!resources.contains<ScenarioTimingKeyframesResource>()) {
73+
resources.set(ScenarioTimingKeyframesResource{});
74+
}
75+
auto& timingKeyframes = resources.get<ScenarioTimingKeyframesResource>();
76+
const auto totalAgentCount = entities.size();
77+
const auto t90TargetCount = static_cast<std::size_t>(std::ceil(static_cast<double>(totalAgentCount) * 0.90));
78+
const auto t95TargetCount = static_cast<std::size_t>(std::ceil(static_cast<double>(totalAgentCount) * 0.95));
79+
80+
std::size_t evacuatedAtStartCount = 0;
81+
std::size_t newlyEvacuatedCount = 0;
82+
for (const auto entity : entities) {
83+
auto& position = query.get<Position>(entity);
84+
auto& velocity = query.get<Velocity>(entity);
85+
auto& route = query.get<EvacuationRoute>(entity);
86+
auto& status = query.get<EvacuationStatus>(entity);
7887
if (status.evacuated) {
88+
++evacuatedAtStartCount;
89+
continue;
90+
}
91+
if (route.destinationZoneId.empty()) {
7992
continue;
8093
}
8194

8295
const auto& floorLayout = cachedLayoutForFloor(layoutCache, route.currentFloorId);
8396
const auto* destinationZone = findZone(floorLayout, route.destinationZoneId);
8497
if (destinationZone != nullptr && pointInRing(destinationZone->area.outline, position.value)) {
8598
status.evacuated = true;
86-
status.completionTimeSeconds = clock.elapsedSeconds;
87-
velocity.value = {};
88-
continue;
89-
}
90-
91-
if (route.nextWaypointIndex >= route.waypoints.size()) {
92-
velocity.value = {};
93-
continue;
94-
}
99+
status.completionTimeSeconds = clock.elapsedSeconds;
100+
velocity.value = {};
101+
++newlyEvacuatedCount;
102+
}
103+
}
104+
105+
const auto evacuatedAfterCount = evacuatedAtStartCount + newlyEvacuatedCount;
106+
const bool shouldCaptureT90 = t90TargetCount > 0
107+
&& !timingKeyframes.t90Frame.has_value()
108+
&& evacuatedAtStartCount < t90TargetCount
109+
&& evacuatedAfterCount >= t90TargetCount;
110+
const bool shouldCaptureT95 = t95TargetCount > 0
111+
&& !timingKeyframes.t95Frame.has_value()
112+
&& evacuatedAtStartCount < t95TargetCount
113+
&& evacuatedAfterCount >= t95TargetCount;
114+
if (shouldCaptureT90 || shouldCaptureT95) {
115+
SimulationFrame keyframe;
116+
keyframe.elapsedSeconds = clock.elapsedSeconds;
117+
keyframe.totalAgentCount = totalAgentCount;
118+
keyframe.evacuatedAgentCount = evacuatedAfterCount;
119+
keyframe.complete = totalAgentCount > 0 && evacuatedAfterCount >= totalAgentCount;
120+
121+
const auto view = query.view<Position, Agent, Velocity, EvacuationStatus>();
122+
keyframe.agents.reserve(view.size());
123+
for (const auto entity : view) {
124+
const auto& status = query.get<EvacuationStatus>(entity);
125+
if (status.evacuated) {
126+
continue;
127+
}
128+
const auto& position = query.get<Position>(entity);
129+
const auto& velocity = query.get<Velocity>(entity);
130+
const auto& agent = query.get<Agent>(entity);
131+
const auto* route = query.contains<EvacuationRoute>(entity) ? &query.get<EvacuationRoute>(entity) : nullptr;
132+
keyframe.agents.push_back({
133+
.id = entity.index,
134+
.position = position.value,
135+
.velocity = velocity.value,
136+
.radius = agent.radius,
137+
.floorId = route != nullptr
138+
? (!route->displayFloorId.empty()
139+
? route->displayFloorId
140+
: route->currentFloorId)
141+
: std::string{},
142+
.stalled = route != nullptr
143+
&& scenarioAgentStalled(simulation_internal::lengthOf(velocity.value), route->stalledSeconds),
144+
});
145+
}
146+
147+
if (shouldCaptureT90) {
148+
timingKeyframes.t90Frame = keyframe;
149+
}
150+
if (shouldCaptureT95) {
151+
timingKeyframes.t95Frame = keyframe;
152+
}
153+
if (resources.contains<ScenarioResultArtifactsResource>()) {
154+
auto& result = resources.get<ScenarioResultArtifactsResource>();
155+
if (shouldCaptureT90 && !result.artifacts.timingSummary.t90Frame.has_value()) {
156+
result.artifacts.timingSummary.t90Frame = timingKeyframes.t90Frame;
157+
}
158+
if (shouldCaptureT95 && !result.artifacts.timingSummary.t95Frame.has_value()) {
159+
result.artifacts.timingSummary.t95Frame = timingKeyframes.t95Frame;
160+
}
161+
}
162+
}
163+
164+
for (const auto entity : entities) {
165+
auto& position = query.get<Position>(entity);
166+
const auto& agent = query.get<Agent>(entity);
167+
auto& velocity = query.get<Velocity>(entity);
168+
auto& route = query.get<EvacuationRoute>(entity);
169+
auto& status = query.get<EvacuationStatus>(entity);
170+
if (status.evacuated) {
171+
continue;
172+
}
173+
174+
if (route.nextWaypointIndex >= route.waypoints.size()) {
175+
velocity.value = {};
176+
continue;
177+
}
95178

96179
const auto target = routeWaypointTarget(route, position.value);
97180
const auto distance = distanceBetween(position.value, target);
@@ -105,11 +188,12 @@ class ScenarioSimulationMotionSystem final : public engine::EngineSystem {
105188
velocity.value = {};
106189
continue;
107190
}
108-
191+
192+
const auto& floorLayout = cachedLayoutForFloor(layoutCache, route.currentFloorId);
109193
const auto routeDirection = (target - position.value) * (1.0 / distance);
110194
const auto maxSpeed = effectiveMaxSpeed(layoutCache, agent, route, position.value);
111195
const auto desiredVelocity = routeDirection * maxSpeed;
112-
double speedScale = 1.0;
196+
double speedScale = 1.0;
113197
const auto neighborRadius = std::max(
114198
static_cast<double>(agent.radius) + kDefaultAgentRadius + kPersonalSpaceBuffer,
115199
kHeadOnLookAheadDistance);

src/domain/ScenarioSimulationSystems.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,21 @@ void ScenarioResultArtifactsSystem::update(engine::EngineWorld& world, const eng
541541
percentileCompletionTime(completionTimes, totalAgentCount, 0.90);
542542
result.artifacts.timingSummary.t95Seconds =
543543
percentileCompletionTime(completionTimes, totalAgentCount, 0.95);
544+
if (resources.contains<ScenarioTimingKeyframesResource>()) {
545+
const auto& keyframes = resources.get<ScenarioTimingKeyframesResource>();
546+
if (!result.artifacts.timingSummary.t90Frame.has_value()
547+
&& result.artifacts.timingSummary.t90Seconds.has_value()
548+
&& keyframes.t90Frame.has_value()
549+
&& std::abs(keyframes.t90Frame->elapsedSeconds - *result.artifacts.timingSummary.t90Seconds) <= 1e-9) {
550+
result.artifacts.timingSummary.t90Frame = keyframes.t90Frame;
551+
}
552+
if (!result.artifacts.timingSummary.t95Frame.has_value()
553+
&& result.artifacts.timingSummary.t95Seconds.has_value()
554+
&& keyframes.t95Frame.has_value()
555+
&& std::abs(keyframes.t95Frame->elapsedSeconds - *result.artifacts.timingSummary.t95Seconds) <= 1e-9) {
556+
result.artifacts.timingSummary.t95Frame = keyframes.t95Frame;
557+
}
558+
}
544559
if (totalAgentCount > 0 && completionTimes.size() == totalAgentCount) {
545560
result.artifacts.timingSummary.finalEvacuationTimeSeconds =
546561
*std::max_element(completionTimes.begin(), completionTimes.end());

0 commit comments

Comments
 (0)