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
10 changes: 10 additions & 0 deletions src/application/ProjectPersistence.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -905,6 +905,10 @@ QJsonObject routeGuidanceToJson(const safecrowd::domain::RouteGuidanceDraft& gui
object["periods"] = periods;
object["guidedExitZoneId"] = QString::fromStdString(guidance.guidedExitZoneId);
object["installConnectionId"] = QString::fromStdString(guidance.installConnectionId);
object["installFloorId"] = QString::fromStdString(guidance.installFloorId);
object["installZoneId"] = QString::fromStdString(guidance.installZoneId);
object["installPositionX"] = guidance.installPosition.x;
object["installPositionY"] = guidance.installPosition.y;
object["baseComplianceRate"] = guidance.baseComplianceRate;
object["guidanceStrength"] = guidance.guidanceStrength;
object["maxDetourMeters"] = guidance.maxDetourMeters;
Expand Down Expand Up @@ -933,6 +937,12 @@ safecrowd::domain::RouteGuidanceDraft routeGuidanceFromJson(const QJsonObject& o
}
guidance.guidedExitZoneId = object.value("guidedExitZoneId").toString().toStdString();
guidance.installConnectionId = object.value("installConnectionId").toString().toStdString();
guidance.installFloorId = object.value("installFloorId").toString().toStdString();
guidance.installZoneId = object.value("installZoneId").toString().toStdString();
guidance.installPosition = {
.x = object.value("installPositionX").toDouble(0.0),
.y = object.value("installPositionY").toDouble(0.0),
};
guidance.baseComplianceRate = object.value("baseComplianceRate").toDouble(0.5);
guidance.guidanceStrength = object.value("guidanceStrength").toDouble(0.55);
guidance.maxDetourMeters = object.value("maxDetourMeters").toDouble(20.0);
Expand Down
48 changes: 36 additions & 12 deletions src/application/ScenarioAuthoringWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,29 @@ QString hazardPositionSummary(const safecrowd::domain::EnvironmentHazardDraft& h
return QString("(%1, %2)").arg(hazard.position.x, 0, 'f', 1).arg(hazard.position.y, 0, 'f', 1);
}

QString routeGuidancePositionSummary(const safecrowd::domain::RouteGuidanceDraft& guidance) {
if (guidance.installFloorId.empty() && guidance.installZoneId.empty()) {
return guidance.installConnectionId.empty()
? QStringLiteral("Derived from target")
: QStringLiteral("Derived from install anchor");
}
return QString("(%1, %2)")
.arg(guidance.installPosition.x, 0, 'f', 1)
.arg(guidance.installPosition.y, 0, 'f', 1);
}

QString routeGuidanceInstallLabel(
const safecrowd::domain::FacilityLayout2D& layout,
const safecrowd::domain::RouteGuidanceDraft& guidance) {
if (!guidance.installConnectionId.empty()) {
return connectionLabelForId(layout, guidance.installConnectionId);
}
if (!guidance.installZoneId.empty()) {
return zoneName(layout, guidance.installZoneId);
}
return QStringLiteral("Unassigned");
}

bool hasSmokeHazard(const safecrowd::domain::EnvironmentState& environment) {
return std::any_of(environment.hazards.begin(), environment.hazards.end(), [](const auto& hazard) {
return hazard.kind == safecrowd::domain::EnvironmentHazardKind::Smoke;
Expand Down Expand Up @@ -958,12 +981,11 @@ std::vector<NavigationTreeNode> buildEventsTree(
nodes.reserve(routeGuidances.size());
for (const auto& guidance : routeGuidances) {
const auto guidanceId = QString::fromStdString(guidance.id);
const auto doorLabel = guidance.installConnectionId.empty()
? QString{}
: connectionLabelForId(layout, guidance.installConnectionId);
const auto installLabel = routeGuidanceInstallLabel(layout, guidance);
const auto exitLabel = guidance.guidedExitZoneId.empty()
? QStringLiteral("Nearest exit")
: zoneName(layout, guidance.guidedExitZoneId);
const auto locationLabel = routeGuidancePositionSummary(guidance);
QString periodSummary = QStringLiteral("Always");
if (!guidance.periods.empty()) {
periodSummary = blockScheduleSummary(safecrowd::domain::ConnectionBlockDraft{
Expand All @@ -982,26 +1004,28 @@ std::vector<NavigationTreeNode> buildEventsTree(
}

std::vector<NavigationTreeNode> children;
children.reserve(doorLabel.isEmpty() ? 2u : 3u);
children.reserve(4u);
children.push_back({
.label = QString("Exit - %1").arg(exitLabel),
.id = QString("%1/exit").arg(guidanceId),
});
if (!doorLabel.isEmpty()) {
children.push_back({
.label = QString("Door - %1").arg(doorLabel),
.id = QString("%1/door").arg(guidanceId),
});
}
children.push_back({
.label = QString("Install - %1").arg(installLabel),
.id = QString("%1/install").arg(guidanceId),
});
children.push_back({
.label = QString("Location - %1").arg(locationLabel),
.id = QString("%1/location").arg(guidanceId),
});
children.push_back({
.label = QString("Period - %1").arg(periodSummary),
.id = QString("%1/period").arg(guidanceId),
});

nodes.push_back({
.label = QString("Guidance - %1").arg(doorLabel.isEmpty() ? exitLabel : doorLabel),
.label = QString("Guidance - %1").arg(installLabel),
.id = guidanceId,
.detail = QString("Period: %1").arg(periodSummary),
.detail = QString("Location: %1 / Period: %2").arg(locationLabel, periodSummary),
.children = std::move(children),
.expanded = true,
});
Expand Down
160 changes: 124 additions & 36 deletions src/application/ScenarioCanvasWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ constexpr double kDefaultInitialSpeed = 1.3;
constexpr double kOccupantMarkerRadius = 5.0;
constexpr double kOccupantWorldRadius = 0.25;
constexpr double kOccupantMinSpacing = kOccupantWorldRadius * 2.0;
constexpr double kGuidancePlacementBarrierClearance = 0.35;
constexpr int kMaxSourceOccupantCount = 5000;
constexpr int kDefaultSourceAgentsPerSpawn = 1;
constexpr double kDefaultSourceStartSeconds = 0.0;
Expand Down Expand Up @@ -171,45 +172,31 @@ safecrowd::domain::Point2D connectionMarkerCenter(const safecrowd::domain::Conne
};
}

std::string pickNearestExitZoneIdForConnection(
const safecrowd::domain::FacilityLayout2D& layout,
const safecrowd::domain::Connection2D& connection) {
const auto pickAdjacentExit = [&]() -> std::string {
if (connection.fromZoneId.empty() && connection.toZoneId.empty()) {
return {};
}
const auto exitIt = std::find_if(layout.zones.begin(), layout.zones.end(), [&](const auto& zone) {
if (zone.kind != safecrowd::domain::ZoneKind::Exit) {
return false;
}
return zone.id == connection.fromZoneId || zone.id == connection.toZoneId;
});
return exitIt == layout.zones.end() ? std::string{} : exitIt->id;
};

if (auto adjacent = pickAdjacentExit(); !adjacent.empty()) {
return adjacent;
}

const auto doorCenter = connectionMarkerCenter(connection);
bool hasExplicitGuidanceInstallPosition(const safecrowd::domain::RouteGuidanceDraft& guidance) {
return !guidance.installFloorId.empty() || !guidance.installZoneId.empty();
}

std::string pickNearestExitZoneIdForPoint(
const safecrowd::domain::FacilityLayout2D& layout,
const safecrowd::domain::Point2D& point,
const std::string& preferredFloorId) {
const auto pickNearest = [&](bool sameFloorOnly) -> std::string {
double bestDistanceSq = std::numeric_limits<double>::infinity();
const safecrowd::domain::Zone2D* bestZone = nullptr;
for (const auto& zone : layout.zones) {
if (zone.kind != safecrowd::domain::ZoneKind::Exit) {
continue;
}
if (sameFloorOnly && !connection.floorId.empty() && !zone.floorId.empty() && zone.floorId != connection.floorId) {
if (sameFloorOnly && !preferredFloorId.empty() && !zone.floorId.empty() && zone.floorId != preferredFloorId) {
continue;
}

const auto exitCenter = polygonCenter(zone.area);
const auto dx = exitCenter.x - doorCenter.x;
const auto dy = exitCenter.y - doorCenter.y;
const auto d2 = (dx * dx) + (dy * dy);
if (d2 < bestDistanceSq) {
bestDistanceSq = d2;
const auto dx = exitCenter.x - point.x;
const auto dy = exitCenter.y - point.y;
const auto distanceSq = (dx * dx) + (dy * dy);
if (distanceSq < bestDistanceSq) {
bestDistanceSq = distanceSq;
bestZone = &zone;
}
}
Expand All @@ -222,6 +209,29 @@ std::string pickNearestExitZoneIdForConnection(
return pickNearest(false);
}

std::string pickNearestExitZoneIdForConnection(
const safecrowd::domain::FacilityLayout2D& layout,
const safecrowd::domain::Connection2D& connection) {
const auto pickAdjacentExit = [&]() -> std::string {
if (connection.fromZoneId.empty() && connection.toZoneId.empty()) {
return {};
}
const auto exitIt = std::find_if(layout.zones.begin(), layout.zones.end(), [&](const auto& zone) {
if (zone.kind != safecrowd::domain::ZoneKind::Exit) {
return false;
}
return zone.id == connection.fromZoneId || zone.id == connection.toZoneId;
});
return exitIt == layout.zones.end() ? std::string{} : exitIt->id;
};

if (auto adjacent = pickAdjacentExit(); !adjacent.empty()) {
return adjacent;
}

return pickNearestExitZoneIdForPoint(layout, connectionMarkerCenter(connection), connection.floorId);
}

QString formatConnectionBlockTooltip(const safecrowd::domain::ConnectionBlockDraft& block) {
if (block.connectionId.empty()) {
return {};
Expand Down Expand Up @@ -361,6 +371,11 @@ QString formatRouteGuidanceTooltip(
text.append(QString("\n %1s~%2s").arg(start, 0, 'f', 1).arg(end, 0, 'f', 1));
}
}
if (hasExplicitGuidanceInstallPosition(guidance)) {
text.append(QString("\n Location: (%1, %2)")
.arg(guidance.installPosition.x, 0, 'f', 1)
.arg(guidance.installPosition.y, 0, 'f', 1));
}
text.append(QString("\n Base compliance: %1").arg(std::clamp(guidance.baseComplianceRate, 0.0, 1.0), 0, 'f', 2));
text.append(QString("\n Strength: %1").arg(std::clamp(guidance.guidanceStrength, 0.0, 1.0), 0, 'f', 2));
text.append(QString("\n Max detour:%1m").arg(std::max(0.0, guidance.maxDetourMeters), 0, 'f', 1));
Expand All @@ -374,7 +389,12 @@ std::optional<QPointF> routeGuidanceMarkerCenter(
const LayoutCanvasTransform& transform,
const QString& currentFloorId) {
std::optional<QPointF> center;
if (!guidance.installConnectionId.empty()) {
if (hasExplicitGuidanceInstallPosition(guidance)) {
if (!matchesFloor(guidance.installFloorId, currentFloorId)) {
return std::nullopt;
}
center = transform.map(guidance.installPosition);
} else if (!guidance.installConnectionId.empty()) {
const auto it = std::find_if(layout.connections.begin(), layout.connections.end(), [&](const auto& connection) {
return connection.id == guidance.installConnectionId;
});
Expand Down Expand Up @@ -2871,16 +2891,19 @@ void ScenarioCanvasWidget::addEnvironmentHazardForZone(
void ScenarioCanvasWidget::addRouteGuidance(const QPointF& position) {
const auto point = unmapPoint(position);
const auto zoneId = zoneAt(point);
const safecrowd::domain::Zone2D* clickedZone = nullptr;
if (!zoneId.isEmpty()) {
const auto zoneIdStd = zoneId.toStdString();
const auto it = std::find_if(layout_.zones.begin(), layout_.zones.end(), [&](const auto& zone) {
return zone.id == zoneIdStd;
});
if (it != layout_.zones.end() && it->kind == safecrowd::domain::ZoneKind::Exit) {
addRouteGuidanceForExitZone(*it);
return;
if (it != layout_.zones.end()) {
clickedZone = &(*it);
if (it->kind == safecrowd::domain::ZoneKind::Exit) {
addRouteGuidanceForExitZone(*it);
return;
}
}
// If the user clicked inside a non-exit zone, still allow installing guidance by selecting a nearby door.
}

constexpr double kPickRadiusPixels = 18.0;
Expand Down Expand Up @@ -2909,12 +2932,69 @@ void ScenarioCanvasWidget::addRouteGuidance(const QPointF& position) {
}
}

if (connection == nullptr) {
QMessageBox::information(this, "Route guidance", "Click an exit zone or a door to install guidance.");
if (connection != nullptr) {
addRouteGuidanceForConnection(*connection);
return;
}

if (clickedZone != nullptr) {
addRouteGuidanceForZonePosition(*clickedZone, point);
return;
}

addRouteGuidanceForConnection(*connection);
QMessageBox::information(this, "Route guidance", "Click a walkable room area, exit zone, or a door to install guidance.");
}

void ScenarioCanvasWidget::addRouteGuidanceForZonePosition(
const safecrowd::domain::Zone2D& zone,
safecrowd::domain::Point2D position) {
if (!matchesFloor(zone.floorId, currentFloorId_)) {
return;
}
if (zone.kind == safecrowd::domain::ZoneKind::Exit) {
addRouteGuidanceForExitZone(zone);
return;
}
if (!pointInPolygon(zone.area, position)) {
QMessageBox::information(this, "Route guidance", "Click inside a walkable room area to place guidance.");
return;
}

const auto floorId = zone.floorId.empty() ? currentFloorId_.toStdString() : zone.floorId;
if (!safecrowd::domain::pointInsideWalkableZoneWithClearance(
layout_,
position,
floorId,
kGuidancePlacementBarrierClearance)) {
QMessageBox::information(
this,
"Route guidance",
"Guidance must be placed inside walkable room space and not too close to walls.");
return;
}

const auto exitZoneId = pickNearestExitZoneIdForPoint(layout_, position, floorId);
if (exitZoneId.empty()) {
QMessageBox::information(this, "Route guidance", "Could not find a reachable exit target for this guidance.");
return;
}

safecrowd::domain::RouteGuidanceDraft draft;
draft.id = nextRouteGuidanceId().toStdString();
draft.startSeconds = 0.0;
draft.endSeconds = 0.0;
draft.periods.clear();
draft.guidedExitZoneId = exitZoneId;
draft.installConnectionId.clear();
draft.installFloorId = floorId;
draft.installZoneId = zone.id;
draft.installPosition = position;
draft.baseComplianceRate = 0.5;
draft.guidanceStrength = 0.55;
draft.maxDetourMeters = 20.0;
routeGuidances_.push_back(std::move(draft));
emitRouteGuidancesChanged();
update();
}

void ScenarioCanvasWidget::addRouteGuidanceForExitZone(const safecrowd::domain::Zone2D& zone) {
Expand All @@ -2924,7 +3004,9 @@ void ScenarioCanvasWidget::addRouteGuidanceForExitZone(const safecrowd::domain::
}

for (const auto& existing : routeGuidances_) {
if (existing.installConnectionId.empty() && existing.guidedExitZoneId == zone.id) {
if (existing.installConnectionId.empty()
&& existing.guidedExitZoneId == zone.id
&& (existing.installZoneId.empty() || existing.installZoneId == zone.id)) {
QMessageBox::information(this, "Route guidance", "Guidance is already installed on this exit.");
return;
}
Expand All @@ -2937,6 +3019,9 @@ void ScenarioCanvasWidget::addRouteGuidanceForExitZone(const safecrowd::domain::
draft.periods.clear();
draft.guidedExitZoneId = zone.id;
draft.installConnectionId.clear();
draft.installFloorId = zone.floorId.empty() ? currentFloorId_.toStdString() : zone.floorId;
draft.installZoneId = zone.id;
draft.installPosition = representativePointInPolygon(zone.area).value_or(polygonCenter(zone.area));
draft.baseComplianceRate = 0.5;
draft.guidanceStrength = 0.55;
draft.maxDetourMeters = 20.0;
Expand Down Expand Up @@ -2968,6 +3053,9 @@ void ScenarioCanvasWidget::addRouteGuidanceForConnection(const safecrowd::domain
draft.periods.clear();
draft.guidedExitZoneId = exitZoneId;
draft.installConnectionId = connection.id;
draft.installFloorId = connection.floorId.empty() ? currentFloorId_.toStdString() : connection.floorId;
draft.installZoneId.clear();
draft.installPosition = connectionMarkerCenter(connection);
draft.baseComplianceRate = 0.5;
draft.guidanceStrength = 0.55;
draft.maxDetourMeters = 20.0;
Expand Down
3 changes: 3 additions & 0 deletions src/application/ScenarioCanvasWidget.h
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ class ScenarioCanvasWidget : public QWidget {
safecrowd::domain::Point2D position,
safecrowd::domain::EnvironmentHazardKind kind);
void addRouteGuidance(const QPointF& position);
void addRouteGuidanceForZonePosition(
const safecrowd::domain::Zone2D& zone,
safecrowd::domain::Point2D position);
void addRouteGuidanceForExitZone(const safecrowd::domain::Zone2D& zone);
void addRouteGuidanceForConnection(const safecrowd::domain::Connection2D& connection);
void openRouteGuidanceEditor(const QString& guidanceId, const QPoint& screenPosition);
Expand Down
Loading
Loading