Skip to content
Merged
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
12 changes: 12 additions & 0 deletions src/application/ProjectPersistence.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1040,6 +1040,12 @@ QJsonObject resultArtifactsToJson(const safecrowd::domain::ScenarioResultArtifac
timing["t90Seconds"] = optionalDoubleToJson(artifacts.timingSummary.t90Seconds);
timing["t95Seconds"] = optionalDoubleToJson(artifacts.timingSummary.t95Seconds);
timing["finalEvacuationTimeSeconds"] = optionalDoubleToJson(artifacts.timingSummary.finalEvacuationTimeSeconds);
if (artifacts.timingSummary.t90Frame.has_value()) {
timing["t90Frame"] = simulationFrameToJson(*artifacts.timingSummary.t90Frame);
}
if (artifacts.timingSummary.t95Frame.has_value()) {
timing["t95Frame"] = simulationFrameToJson(*artifacts.timingSummary.t95Frame);
}
object["timingSummary"] = timing;
return object;
}
Expand All @@ -1064,6 +1070,12 @@ safecrowd::domain::ScenarioResultArtifacts resultArtifactsFromJson(const QJsonOb
artifacts.timingSummary.t90Seconds = optionalDoubleFromJson(timing.value("t90Seconds"));
artifacts.timingSummary.t95Seconds = optionalDoubleFromJson(timing.value("t95Seconds"));
artifacts.timingSummary.finalEvacuationTimeSeconds = optionalDoubleFromJson(timing.value("finalEvacuationTimeSeconds"));
if (timing.value("t90Frame").isObject()) {
artifacts.timingSummary.t90Frame = simulationFrameFromJson(timing.value("t90Frame").toObject());
}
if (timing.value("t95Frame").isObject()) {
artifacts.timingSummary.t95Frame = simulationFrameFromJson(timing.value("t95Frame").toObject());
}
return artifacts;
}

Expand Down
124 changes: 123 additions & 1 deletion src/application/ScenarioResultWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <functional>
#include <utility>

#include <QAbstractItemView>
Expand All @@ -15,9 +16,11 @@
#include <QIcon>
#include <QLabel>
#include <QLinearGradient>
#include <QMouseEvent>
#include <QPainter>
#include <QPainterPath>
#include <QPixmap>
#include <QPointer>
#include <QPushButton>
#include <QScrollArea>
#include <QSizePolicy>
Expand Down Expand Up @@ -97,13 +100,18 @@ class EvacuationProgressWidget final : public QWidget {
setMinimumHeight(150);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
setToolTip("Remaining occupant curve. T90/T95 indicate when 90%/95% of occupants have evacuated.");
setMouseTracking(true);
}

void setCurrentTimeSeconds(std::optional<double> seconds) {
currentTimeSeconds_ = seconds;
update();
}

void setTimingMarkerActivatedHandler(std::function<void(double)> handler) {
timingMarkerActivatedHandler_ = std::move(handler);
}

protected:
void paintEvent(QPaintEvent* event) override {
(void)event;
Expand Down Expand Up @@ -175,10 +183,99 @@ class EvacuationProgressWidget final : public QWidget {
QString("%1 / %2 remaining by %3 sec")
.arg(static_cast<int>(remainingCount))
.arg(static_cast<int>(last.totalCount))
.arg(last.timeSeconds, 0, 'f', 1));
.arg(last.timeSeconds, 0, 'f', 1));
}

void mouseMoveEvent(QMouseEvent* event) override {
if (event == nullptr) {
return;
}
const bool hover = timingMarkerHitSeconds(event->position()).has_value();
setCursor(hover ? Qt::PointingHandCursor : Qt::ArrowCursor);
QWidget::mouseMoveEvent(event);
}

void leaveEvent(QEvent* event) override {
unsetCursor();
QWidget::leaveEvent(event);
}

void mousePressEvent(QMouseEvent* event) override {
if (event == nullptr || event->button() != Qt::LeftButton) {
QWidget::mousePressEvent(event);
return;
}
const auto seconds = timingMarkerHitSeconds(event->position());
if (!seconds.has_value()) {
QWidget::mousePressEvent(event);
return;
}
event->accept();
if (timingMarkerActivatedHandler_) {
timingMarkerActivatedHandler_(*seconds);
}
}

private:
QRectF plotRect() const {
return QRectF(rect()).adjusted(34, 18, -14, -28);
}

double maxTimeSeconds() const {
if (artifacts_.evacuationProgress.empty()) {
return 1.0;
}
const auto maxTimeIt = std::max_element(
artifacts_.evacuationProgress.begin(),
artifacts_.evacuationProgress.end(),
[](const auto& lhs, const auto& rhs) {
return lhs.timeSeconds < rhs.timeSeconds;
});
return std::max(1.0, maxTimeIt->timeSeconds);
}

QRectF markerHitRegion(const QRectF& plot, double maxTime, double seconds) const {
const auto x = plot.left() + (std::clamp(seconds / maxTime, 0.0, 1.0) * plot.width());
const QRectF lineHit(x - 6.0, plot.top(), 12.0, plot.height());
const QRectF labelHit(x - 4.0, plot.top() - 2.0, 70.0, 22.0);
return lineHit.united(labelHit);
}

std::optional<double> timingMarkerHitSeconds(const QPointF& position) const {
const QRectF plot = plotRect();
if (!plot.contains(position)) {
return std::nullopt;
}

const auto maxTime = maxTimeSeconds();
struct Candidate {
double seconds;
double distance;
};
std::optional<Candidate> best;
auto consider = [&](const std::optional<double>& markerSeconds) {
if (!markerSeconds.has_value()) {
return;
}
const auto region = markerHitRegion(plot, maxTime, *markerSeconds);
if (!region.contains(position)) {
return;
}
const auto x = plot.left() + (std::clamp(*markerSeconds / maxTime, 0.0, 1.0) * plot.width());
const auto distance = std::abs(position.x() - x);
if (!best.has_value() || distance < best->distance) {
best = Candidate{.seconds = *markerSeconds, .distance = distance};
}
};

consider(artifacts_.timingSummary.t90Seconds);
consider(artifacts_.timingSummary.t95Seconds);
if (!best.has_value()) {
return std::nullopt;
}
return best->seconds;
}

void drawTimingMarker(
QPainter& painter,
const QRectF& plot,
Expand Down Expand Up @@ -215,6 +312,7 @@ class EvacuationProgressWidget final : public QWidget {

safecrowd::domain::ScenarioResultArtifacts artifacts_{};
std::optional<double> currentTimeSeconds_{};
std::function<void(double)> timingMarkerActivatedHandler_{};
};

class DensityLegendWidget final : public QWidget {
Expand Down Expand Up @@ -970,6 +1068,30 @@ QWidget* createResultCanvasPanel(
if (replayControlsOut != nullptr) {
*replayControlsOut = replayControls;
}
if (progressWidget != nullptr) {
const QPointer<ResultReplayControls> replayControlsGuard(replayControls);
const auto t90Seconds = artifacts.timingSummary.t90Seconds;
const auto t95Seconds = artifacts.timingSummary.t95Seconds;
const auto t90Frame = artifacts.timingSummary.t90Frame;
const auto t95Frame = artifacts.timingSummary.t95Frame;
progressWidget->setTimingMarkerActivatedHandler([replayControlsGuard, t90Seconds, t95Seconds, t90Frame, t95Frame](double seconds) {
if (replayControlsGuard != nullptr) {
if (t90Seconds.has_value()
&& t90Frame.has_value()
&& std::abs(seconds - *t90Seconds) <= 1e-6) {
replayControlsGuard->showFrame(*t90Frame);
return;
}
if (t95Seconds.has_value()
&& t95Frame.has_value()
&& std::abs(seconds - *t95Seconds) <= 1e-6) {
replayControlsGuard->showFrame(*t95Frame);
return;
}
replayControlsGuard->showClosestFrameAtSeconds(seconds);
}
});
}
layout->addWidget(replayControls);
layout->addWidget(graphPanel, 1);
return panel;
Expand Down
2 changes: 2 additions & 0 deletions src/domain/ScenarioResultArtifacts.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ struct EvacuationTimingSummary {
std::optional<double> finalEvacuationTimeSeconds{};
double targetTimeSeconds{0.0};
std::optional<double> marginSeconds{};
std::optional<SimulationFrame> t90Frame{};
std::optional<SimulationFrame> t95Frame{};
};

struct DensityCellMetric {
Expand Down
120 changes: 102 additions & 18 deletions src/domain/ScenarioSimulationMotionSystem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,30 +68,113 @@ class ScenarioSimulationMotionSystem final : public engine::EngineSystem {
advanceRoutesForWaypointProgress(query, 0.0, entities, layoutCache);
replanBlockedExitRoutes(query, entities, layoutCache, clock.elapsedSeconds, layoutRevision);
replanBlockedRouteSegments(query, entities, layoutCache, clock.elapsedSeconds, layoutRevision);

for (const auto entity : entities) {
auto& position = query.get<Position>(entity);
const auto& agent = query.get<Agent>(entity);
auto& velocity = query.get<Velocity>(entity);
auto& route = query.get<EvacuationRoute>(entity);
auto& status = query.get<EvacuationStatus>(entity);

if (!resources.contains<ScenarioTimingKeyframesResource>()) {
resources.set(ScenarioTimingKeyframesResource{});
}
auto& timingKeyframes = resources.get<ScenarioTimingKeyframesResource>();
const auto totalAgentCount = entities.size();
const auto t90TargetCount = static_cast<std::size_t>(std::ceil(static_cast<double>(totalAgentCount) * 0.90));
const auto t95TargetCount = static_cast<std::size_t>(std::ceil(static_cast<double>(totalAgentCount) * 0.95));

std::size_t evacuatedAtStartCount = 0;
std::size_t newlyEvacuatedCount = 0;
for (const auto entity : entities) {
auto& position = query.get<Position>(entity);
auto& velocity = query.get<Velocity>(entity);
auto& route = query.get<EvacuationRoute>(entity);
auto& status = query.get<EvacuationStatus>(entity);
if (status.evacuated) {
++evacuatedAtStartCount;
continue;
}
if (route.destinationZoneId.empty()) {
continue;
}

const auto& floorLayout = cachedLayoutForFloor(layoutCache, route.currentFloorId);
const auto* destinationZone = findZone(floorLayout, route.destinationZoneId);
if (destinationZone != nullptr && pointInRing(destinationZone->area.outline, position.value)) {
status.evacuated = true;
status.completionTimeSeconds = clock.elapsedSeconds;
velocity.value = {};
continue;
}

if (route.nextWaypointIndex >= route.waypoints.size()) {
velocity.value = {};
continue;
}
status.completionTimeSeconds = clock.elapsedSeconds;
velocity.value = {};
++newlyEvacuatedCount;
}
}

const auto evacuatedAfterCount = evacuatedAtStartCount + newlyEvacuatedCount;
const bool shouldCaptureT90 = t90TargetCount > 0
&& !timingKeyframes.t90Frame.has_value()
&& evacuatedAtStartCount < t90TargetCount
&& evacuatedAfterCount >= t90TargetCount;
const bool shouldCaptureT95 = t95TargetCount > 0
&& !timingKeyframes.t95Frame.has_value()
&& evacuatedAtStartCount < t95TargetCount
&& evacuatedAfterCount >= t95TargetCount;
if (shouldCaptureT90 || shouldCaptureT95) {
SimulationFrame keyframe;
keyframe.elapsedSeconds = clock.elapsedSeconds;
keyframe.totalAgentCount = totalAgentCount;
keyframe.evacuatedAgentCount = evacuatedAfterCount;
keyframe.complete = totalAgentCount > 0 && evacuatedAfterCount >= totalAgentCount;

const auto view = query.view<Position, Agent, Velocity, EvacuationStatus>();
keyframe.agents.reserve(view.size());
for (const auto entity : view) {
const auto& status = query.get<EvacuationStatus>(entity);
if (status.evacuated) {
continue;
}
const auto& position = query.get<Position>(entity);
const auto& velocity = query.get<Velocity>(entity);
const auto& agent = query.get<Agent>(entity);
const auto* route = query.contains<EvacuationRoute>(entity) ? &query.get<EvacuationRoute>(entity) : nullptr;
keyframe.agents.push_back({
.id = entity.index,
.position = position.value,
.velocity = velocity.value,
.radius = agent.radius,
.floorId = route != nullptr
? (!route->displayFloorId.empty()
? route->displayFloorId
: route->currentFloorId)
: std::string{},
.stalled = route != nullptr
&& scenarioAgentStalled(simulation_internal::lengthOf(velocity.value), route->stalledSeconds),
});
}

if (shouldCaptureT90) {
timingKeyframes.t90Frame = keyframe;
}
if (shouldCaptureT95) {
timingKeyframes.t95Frame = keyframe;
}
if (resources.contains<ScenarioResultArtifactsResource>()) {
auto& result = resources.get<ScenarioResultArtifactsResource>();
if (shouldCaptureT90 && !result.artifacts.timingSummary.t90Frame.has_value()) {
result.artifacts.timingSummary.t90Frame = timingKeyframes.t90Frame;
}
if (shouldCaptureT95 && !result.artifacts.timingSummary.t95Frame.has_value()) {
result.artifacts.timingSummary.t95Frame = timingKeyframes.t95Frame;
}
}
}

for (const auto entity : entities) {
auto& position = query.get<Position>(entity);
const auto& agent = query.get<Agent>(entity);
auto& velocity = query.get<Velocity>(entity);
auto& route = query.get<EvacuationRoute>(entity);
auto& status = query.get<EvacuationStatus>(entity);
if (status.evacuated) {
continue;
}

if (route.nextWaypointIndex >= route.waypoints.size()) {
velocity.value = {};
continue;
}

const auto target = routeWaypointTarget(route, position.value);
const auto distance = distanceBetween(position.value, target);
Expand All @@ -105,11 +188,12 @@ class ScenarioSimulationMotionSystem final : public engine::EngineSystem {
velocity.value = {};
continue;
}


const auto& floorLayout = cachedLayoutForFloor(layoutCache, route.currentFloorId);
const auto routeDirection = (target - position.value) * (1.0 / distance);
const auto maxSpeed = effectiveMaxSpeed(layoutCache, agent, route, position.value);
const auto desiredVelocity = routeDirection * maxSpeed;
double speedScale = 1.0;
double speedScale = 1.0;
const auto neighborRadius = std::max(
static_cast<double>(agent.radius) + kDefaultAgentRadius + kPersonalSpaceBuffer,
kHeadOnLookAheadDistance);
Expand Down
15 changes: 15 additions & 0 deletions src/domain/ScenarioSimulationSystems.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,21 @@ void ScenarioResultArtifactsSystem::update(engine::EngineWorld& world, const eng
percentileCompletionTime(completionTimes, totalAgentCount, 0.90);
result.artifacts.timingSummary.t95Seconds =
percentileCompletionTime(completionTimes, totalAgentCount, 0.95);
if (resources.contains<ScenarioTimingKeyframesResource>()) {
const auto& keyframes = resources.get<ScenarioTimingKeyframesResource>();
if (!result.artifacts.timingSummary.t90Frame.has_value()
&& result.artifacts.timingSummary.t90Seconds.has_value()
&& keyframes.t90Frame.has_value()
&& std::abs(keyframes.t90Frame->elapsedSeconds - *result.artifacts.timingSummary.t90Seconds) <= 1e-9) {
result.artifacts.timingSummary.t90Frame = keyframes.t90Frame;
}
if (!result.artifacts.timingSummary.t95Frame.has_value()
&& result.artifacts.timingSummary.t95Seconds.has_value()
&& keyframes.t95Frame.has_value()
&& std::abs(keyframes.t95Frame->elapsedSeconds - *result.artifacts.timingSummary.t95Seconds) <= 1e-9) {
result.artifacts.timingSummary.t95Frame = keyframes.t95Frame;
}
}
if (totalAgentCount > 0 && completionTimes.size() == totalAgentCount) {
result.artifacts.timingSummary.finalEvacuationTimeSeconds =
*std::max_element(completionTimes.begin(), completionTimes.end());
Expand Down
Loading
Loading