Skip to content

Commit 28b4617

Browse files
[Domain] add recommended scenario drafts
Fix zero-use layout exit handling before merging PR #250.
1 parent b930b46 commit 28b4617

13 files changed

Lines changed: 1550 additions & 33 deletions

CMakeLists.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ add_library(safecrowd_domain STATIC
8282
src/domain/GeometryQueries.h
8383
src/domain/GeometryQueries.cpp
8484
src/domain/PopulationSpec.h
85+
src/domain/AlternativeRecommendationService.h
86+
src/domain/AlternativeRecommendationService.cpp
8587
src/domain/ScenarioAuthoring.h
8688
src/domain/ScenarioAuthoring.cpp
8789
src/domain/ScenarioBatchRunner.h
@@ -158,6 +160,7 @@ if (BUILD_TESTING)
158160
tests/EngineIntegrationTests.cpp
159161
tests/ResourceStoreTests.cpp
160162
tests/DeterministicRngTests.cpp
163+
tests/AlternativeRecommendationServiceTests.cpp
161164
tests/ScenarioSimulationRunnerTests.cpp
162165
tests/ScenarioAuthoringTests.cpp
163166
tests/ScenarioBatchRunnerTests.cpp
@@ -174,6 +177,19 @@ if (BUILD_TESTING)
174177
safecrowd_domain
175178
)
176179

180+
if (SAFECROWD_BUILD_APP)
181+
target_sources(safecrowd_tests
182+
PRIVATE
183+
tests/ProjectPersistenceTests.cpp
184+
src/application/ProjectPersistence.cpp
185+
)
186+
187+
target_link_libraries(safecrowd_tests
188+
PRIVATE
189+
Qt6::Core
190+
)
191+
endif()
192+
177193
configure_project_target(safecrowd_tests)
178194

179195
add_test(NAME safecrowd_tests COMMAND safecrowd_tests)

docs/UI.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,9 @@ stage된 baseline 시나리오를 실제로 실행하고 진행 상태를 보는
347347
- 하단 graph panel에 Remaining, Exits, Compare 탭 제공
348348
- Exits 탭에 출구별 이용 인원, 비율, 마지막 통과 시각 표시
349349
- Compare 탭은 v1 placeholder이며 baseline/alternative 비교 계산은 후속 Analysis Workspace 범위
350+
- Batch Result 우측 패널에 완료 결과 아티팩트 기반 Recommendations v1 제공
351+
- 차단 해제, 출구 분산 유도, 병목/압박 hotspot 완화 후보를 표시
352+
- `Create Recommended Scenario``Recommended` 시나리오 초안만 만들고 자동 재실행하지 않음
350353
- 상세 탭에 Zones, Groups, Criteria 표시
351354
- Zones 탭에 구역별 초기 인원, 대피 인원, 마지막 완료시각 표시
352355
- Groups 탭에 배치 그룹별 초기 인원, 대피 인원, 마지막 완료시각 표시
@@ -554,7 +557,7 @@ stage된 baseline 시나리오를 실제로 실행하고 진행 상태를 보는
554557
- [ ] Variation Summary 제공
555558
- [ ] Heatmap Selector 제공
556559
- [ ] Comparison View 제공
557-
- [ ] Recommendation Drawer 제공
560+
- [x] Recommendation Drawer v1 제공
558561
- [ ] Export Dialog 제공
559562

560563
## 7. 문서 유지 규칙

docs/alternative-recommendation-evidence.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212

1313
---
14-
1514

1615

1716

@@ -189,4 +188,3 @@
189188
- “문헌상 특정 조건에서 개선 사례가 보고됨”
190189
- “현재 시나리오에서도 효과가 있는지는 재시뮬레이션으로 검증 필요”
191190
- “장애물/분리대는 잘못 배치하면 오히려 악화될 수 있음”
192-

docs/alternative-recommendation-plan.md

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,20 @@
1212
- 판단기준
1313
측정 구역(출구 앞 2M)에 밀도가 2명/㎡ 이상있을때,
1414
10초 이상 대기 상태가 지속되고,
15-
16-
15+
1716
- 복도에서 병목
1817
- 판단기준
1918
복도 실제 통과 시간이 정상 예상 통과 시간의 2배 이상이고,
2019
평균속도가 0.5이하 상태가 10초 이상 지속되면 복도 병목으로 판단한다.
21-
22-
20+
2321
- 제한시간 초과/ 미대피
2422
- 판단기준
2523
제한시간동안 모든인원이 대피하지 못했을때.
26-
27-
24+
25+
2826
- 양방향 흐름 충돌
2927
- 판단기준
3028
120도 이상 반대 방향 흐름, 양쪽 인원 각각 30% 이상, 속도 0.7m/s 이하가 10초 이상
31-
3229

3330

3431
### 대안추천 예상값
@@ -69,5 +66,3 @@
6966
|제한시간 초과 / 미대피 증가|과부하 출구 인원을 다른 출구로 분산|가장 혼잡한 E1 구역 지연시간 180초 감소|
7067
|제한시간 초과 / 미대피 증가|여유 구역 E2/E3로 부하 이전|E2/E3 시간 증가는 30초 내외로 제한|
7168
|제한시간 초과 / 미대피 증가|단계적 출발 / 지연 대기 전략|병목 전방 동시 압력 감소, 미대피 발생 억제|
72-
73-

src/application/ScenarioAuthoringWidget.cpp

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,23 @@ QLabel* createRoleBadge(const QString& text, bool alternative, QWidget* parent)
576576
return badge;
577577
}
578578

579+
QString scenarioRoleLabel(safecrowd::domain::ScenarioRole role) {
580+
switch (role) {
581+
case safecrowd::domain::ScenarioRole::Baseline:
582+
return "Baseline";
583+
case safecrowd::domain::ScenarioRole::Recommended:
584+
return "Recommended";
585+
case safecrowd::domain::ScenarioRole::Alternative:
586+
default:
587+
return "Alternative";
588+
}
589+
}
590+
591+
bool scenarioRoleHasBaselineDiff(safecrowd::domain::ScenarioRole role) {
592+
return role == safecrowd::domain::ScenarioRole::Alternative
593+
|| role == safecrowd::domain::ScenarioRole::Recommended;
594+
}
595+
579596
void addMetaRow(QVBoxLayout* layout, const QString& label, const QString& value, QWidget* parent) {
580597
auto* row = new QWidget(parent);
581598
auto* rowLayout = new QHBoxLayout(row);
@@ -1223,7 +1240,7 @@ void ScenarioAuthoringWidget::createScenarioWithName(const QString& name, int so
12231240
scenario.crowdPlacements = source.crowdPlacements;
12241241
scenario.startText = source.startText;
12251242
scenario.destinationText = source.destinationText;
1226-
scenario.baseScenarioId = source.draft.role == safecrowd::domain::ScenarioRole::Alternative
1243+
scenario.baseScenarioId = scenarioRoleHasBaselineDiff(source.draft.role)
12271244
? source.baseScenarioId
12281245
: QString::fromStdString(source.draft.scenarioId);
12291246
scenario.stagedForRun = false;
@@ -1356,10 +1373,10 @@ void ScenarioAuthoringWidget::refreshInspector() {
13561373
if (!hasScenario) {
13571374
addStatusMessage(panelLayout, "No scenario selected", scenarioOverviewPanel_);
13581375
} else {
1359-
const bool alternative = scenario->draft.role == safecrowd::domain::ScenarioRole::Alternative;
1376+
const bool variation = scenarioRoleHasBaselineDiff(scenario->draft.role);
13601377
panelLayout->addWidget(createRoleBadge(
1361-
alternative ? "Alternative" : "Baseline",
1362-
alternative,
1378+
scenarioRoleLabel(scenario->draft.role),
1379+
variation,
13631380
scenarioOverviewPanel_));
13641381

13651382
auto* nameLabel = createLabel(
@@ -1377,7 +1394,7 @@ void ScenarioAuthoringWidget::refreshInspector() {
13771394
addMetaRow(panelLayout, "Blocked", QString::number(static_cast<int>(scenario->draft.control.connectionBlocks.size())), scenarioOverviewPanel_);
13781395
addMetaRow(panelLayout, "Start", scenario->startText, scenarioOverviewPanel_);
13791396
addMetaRow(panelLayout, "Destination", scenario->destinationText, scenarioOverviewPanel_);
1380-
if (alternative && !scenario->baseScenarioId.isEmpty()) {
1397+
if (variation && !scenario->baseScenarioId.isEmpty()) {
13811398
addMetaRow(panelLayout, "Based on", scenario->baseScenarioId, scenarioOverviewPanel_);
13821399
}
13831400
}
@@ -1397,7 +1414,7 @@ void ScenarioAuthoringWidget::refreshInspector() {
13971414
} else if (scenario->draft.role == safecrowd::domain::ScenarioRole::Baseline) {
13981415
addStatusMessage(panelLayout, "Baseline scenario", scenarioDiffPanel_);
13991416
} else if (scenario->baseScenarioId.isEmpty()) {
1400-
addStatusMessage(panelLayout, "Alternative scenario / no baseline link", scenarioDiffPanel_);
1417+
addStatusMessage(panelLayout, "Variation scenario / no baseline link", scenarioDiffPanel_);
14011418
} else {
14021419
const auto baseId = scenario->baseScenarioId.toStdString();
14031420
const auto baselineIt = std::find_if(scenarios_.begin(), scenarios_.end(), [&](const auto& candidate) {
@@ -1452,8 +1469,8 @@ void ScenarioAuthoringWidget::refreshInspector() {
14521469
if (!stagedScenario.stagedForRun || !scenarioHasOccupants(stagedScenario)) {
14531470
continue;
14541471
}
1455-
const auto role = stagedScenario.draft.role == safecrowd::domain::ScenarioRole::Baseline ? "Baseline" : "Alternative";
1456-
lines << QString("- %1 (%2)").arg(QString::fromStdString(stagedScenario.draft.name), role);
1472+
lines << QString("- %1 (%2)")
1473+
.arg(QString::fromStdString(stagedScenario.draft.name), scenarioRoleLabel(stagedScenario.draft.role));
14571474
}
14581475
}
14591476
stagedScenariosLabel_->setText(lines.join('\n'));
@@ -1698,8 +1715,8 @@ void ScenarioAuthoringWidget::refreshScenarioSwitcher() {
16981715
scenarioSwitcher_->blockSignals(true);
16991716
scenarioSwitcher_->clear();
17001717
for (const auto& scenario : scenarios_) {
1701-
const auto role = scenario.draft.role == safecrowd::domain::ScenarioRole::Baseline ? "Baseline" : "Alternative";
1702-
scenarioSwitcher_->addItem(QString("%1 (%2)").arg(QString::fromStdString(scenario.draft.name), role));
1718+
scenarioSwitcher_->addItem(QString("%1 (%2)")
1719+
.arg(QString::fromStdString(scenario.draft.name), scenarioRoleLabel(scenario.draft.role)));
17031720
}
17041721
scenarioSwitcher_->setCurrentIndex(currentScenarioIndex_);
17051722
scenarioSwitcher_->blockSignals(false);
@@ -1822,8 +1839,7 @@ void ScenarioAuthoringWidget::recomputeDependentVariationDiffKeys(const QString&
18221839
}
18231840

18241841
void ScenarioAuthoringWidget::recomputeVariationDiffKeysIfAlternative(ScenarioState& scenario) const {
1825-
if (scenario.draft.role != safecrowd::domain::ScenarioRole::Alternative
1826-
|| scenario.baseScenarioId.isEmpty()) {
1842+
if (!scenarioRoleHasBaselineDiff(scenario.draft.role) || scenario.baseScenarioId.isEmpty()) {
18271843
scenario.draft.variationDiffKeys.clear();
18281844
return;
18291845
}
@@ -2009,8 +2025,8 @@ QWidget* ScenarioAuthoringWidget::createScenarioPanel() {
20092025
if (!scenario.stagedForRun || !scenarioHasOccupants(scenario)) {
20102026
continue;
20112027
}
2012-
const auto role = scenario.draft.role == safecrowd::domain::ScenarioRole::Baseline ? "Baseline" : "Alternative";
2013-
lines << QString("- %1 (%2)").arg(QString::fromStdString(scenario.draft.name), role);
2028+
lines << QString("- %1 (%2)")
2029+
.arg(QString::fromStdString(scenario.draft.name), scenarioRoleLabel(scenario.draft.role));
20142030
}
20152031
}
20162032
stagedScenariosLabel_->setText(lines.join('\n'));

0 commit comments

Comments
 (0)