From 9567a711f4f3556ec4b1fe4fe0489a5f8bdc62e2 Mon Sep 17 00:00:00 2001
From: Noah Schick <noah.schick@in-tech.com>
Date: Fri, 20 Dec 2024 10:50:48 +0100
Subject: [PATCH 01/26] Place conan deps inside project and change .gitignore

---
 .gitignore                                | 4 +++-
 utils/ci/scripts/15_prepare_thirdParty.sh | 2 +-
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/.gitignore b/.gitignore
index fc6f72e2..0ef100c2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,4 +6,6 @@ __pycache__
 generator.log
 *.orig
 *.bkp
-.devcontainer
\ No newline at end of file
+.devcontainer
+build
+deps
\ No newline at end of file
diff --git a/utils/ci/scripts/15_prepare_thirdParty.sh b/utils/ci/scripts/15_prepare_thirdParty.sh
index 05617eb1..1273e1c3 100755
--- a/utils/ci/scripts/15_prepare_thirdParty.sh
+++ b/utils/ci/scripts/15_prepare_thirdParty.sh
@@ -148,4 +148,4 @@ done
 
 # Command to install all the packages into the required folder.
 # --build=missing argument is necessary as at this point protobuf is checked and if not available it builds from conancenter
-"$PYTHON_COMMAND" -m conans.conan install $file --build=missing --deployer=direct_deploy -of="$REPO_ROOT/../deps" -pr:a "$conanprofile" || exit 1
\ No newline at end of file
+"$PYTHON_COMMAND" -m conans.conan install $file --build=missing --deployer=direct_deploy -of="$REPO_ROOT/deps" -pr:a "$conanprofile" || exit 1
\ No newline at end of file
-- 
GitLab


From f8f9662a8e2f774e2f0a8fe1c24315790b5ae03b Mon Sep 17 00:00:00 2001
From: Noah Schick <noah.schick@in-tech.com>
Date: Fri, 20 Dec 2024 10:51:39 +0100
Subject: [PATCH 02/26] Change mantle_api branch for traffic area

---
 engine/bazel/deps.bzl                         | 46 +++++++++++++++++++
 .../conan/recipe/mantleapi/all/conandata.yml  |  4 ++
 .../openscenario_engine/all/conanfile.py      |  2 +-
 3 files changed, 51 insertions(+), 1 deletion(-)
 create mode 100644 engine/bazel/deps.bzl

diff --git a/engine/bazel/deps.bzl b/engine/bazel/deps.bzl
new file mode 100644
index 00000000..cc30aa01
--- /dev/null
+++ b/engine/bazel/deps.bzl
@@ -0,0 +1,46 @@
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe")
+
+ECLIPSE_GITLAB = "https://gitlab.eclipse.org/eclipse"
+
+MANTLE_API_TAG = "v6.0.0"
+UNITS_TAG = "2.3.3"
+YASE_TAG = "v0.0.1"
+
+MANTLE_API_TAG = "15bf75e4e3238afa5f1aa85c4054f37804375453"
+
+def osc_engine_deps():
+    """Load dependencies"""
+    maybe(
+        http_archive,
+        name = "mantle_api",
+        url = ECLIPSE_GITLAB + "/openpass/mantle-api/-/archive/{tag}/mantle-api-{tag}.tar.gz".format(tag = MANTLE_API_TAG),
+        sha256 = "9f898c45b0a0541a553e6d1a7721215d76516daf9ad70658aab40d669bd2364d",
+        strip_prefix = "mantle-api-{tag}".format(tag = MANTLE_API_TAG),
+        type = "tar.gz",
+    )
+
+    maybe(
+        http_archive,
+        name = "units_nhh",
+        url = "https://github.com/nholthaus/units/archive/refs/tags/v{tag}.tar.gz".format(tag = UNITS_TAG),
+        sha256 = "b1f3c1dd11afa2710a179563845ce79f13ebf0c8c090d6aa68465b18bd8bd5fc",
+        strip_prefix = "./units-{tag}".format(tag = UNITS_TAG),
+        build_file = "//bazel:units.BUILD",
+    )
+
+    maybe(
+        http_archive,
+        name = "yase",
+        url = ECLIPSE_GITLAB + "/openpass/yase/-/archive/{tag}/yase-{tag}.tar.gz".format(tag = YASE_TAG),
+        sha256 = "243d710172dff4c4e7ab95e5bd68a6d8335cf8a3fdd1efa87d118250e3b85ee3",
+        strip_prefix = "yase-{tag}".format(tag = YASE_TAG),
+    )
+
+    http_archive(
+        name = "open_scenario_parser",
+        build_file = "//bazel:openscenario_api.BUILD",
+        url = "https://github.com/RA-Consulting-GmbH/openscenario.api.test/releases/download/v1.4.0/OpenSCENARIO_API_LinuxSharedRelease_2024.11.18.tgz",
+        sha256 = "7a4cdb82ccaaeed5fccf12efd94cf4f9c9d3ac0fc7d7feedd4c0babe2404f853",
+        strip_prefix = "OpenSCENARIO_API_LinuxSharedRelease",
+    )
diff --git a/utils/ci/conan/recipe/mantleapi/all/conandata.yml b/utils/ci/conan/recipe/mantleapi/all/conandata.yml
index 30e9ad2a..56d0b1a5 100644
--- a/utils/ci/conan/recipe/mantleapi/all/conandata.yml
+++ b/utils/ci/conan/recipe/mantleapi/all/conandata.yml
@@ -41,6 +41,10 @@ sources:
     url: https://gitlab.eclipse.org/eclipse/openpass/mantle-api.git
     sha256: "a5530013edcd2028ce48d1dcd7e90a512c158f27"
 
+  "6.0.1":
+    url: https://gitlab.eclipse.org/eclipse/openpass/mantle-api.git
+    sha256: "4c04afb373d26b9e4ddcae3fd920f6c5543a19f2"
+
   "8.0.0":
     url: https://gitlab.eclipse.org/eclipse/openpass/mantle-api.git
     sha256: "18eadd8ad4f3ddd78c955ce3d4d637a8ff912091"
diff --git a/utils/ci/conan/recipe/openscenario_engine/all/conanfile.py b/utils/ci/conan/recipe/openscenario_engine/all/conanfile.py
index 43f2201c..b79fb3fa 100644
--- a/utils/ci/conan/recipe/openscenario_engine/all/conanfile.py
+++ b/utils/ci/conan/recipe/openscenario_engine/all/conanfile.py
@@ -25,7 +25,7 @@ class OpenScenarioEngineConan(ConanFile):
     settings = "os", "compiler", "build_type", "arch"
     options = {"shared": [True, False],
                "fPIC": [True, False],
-               "MantleAPI_version":["ANY"],
+               "MantleAPI_version":["6.0.1"],
                "openscenario_api_version":["ANY"],
                "units_version":["ANY"],
                "Yase_version":["ANY"]
-- 
GitLab


From 336edc8588571139cb0249544ff004e517568b49 Mon Sep 17 00:00:00 2001
From: Noah Schick <noah.schick@in-tech.com>
Date: Fri, 20 Dec 2024 10:55:51 +0100
Subject: [PATCH 03/26] Fix "entitiy" typo

---
 engine/tests/Utils/ControllerCreatorTest.cpp | 8 ++++----
 generator/model/v1_3.json                    | 2 +-
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/engine/tests/Utils/ControllerCreatorTest.cpp b/engine/tests/Utils/ControllerCreatorTest.cpp
index 2b5bc9ec..93f11830 100644
--- a/engine/tests/Utils/ControllerCreatorTest.cpp
+++ b/engine/tests/Utils/ControllerCreatorTest.cpp
@@ -42,11 +42,11 @@ public:
         mockController{}
   {
     ON_CALL(mockController, GetName()).WillByDefault(ReturnRef(TEST_CONTROLLER_NAME));
-    ON_CALL(mockController, GetUniqueId()).WillByDefault(testing::Invoke([] {
+    ON_CALL(mockController, GetUniqueId()).WillByDefault(testing::Invoke([]
+                                                                         {
       // id will be incremented at every call, starting with 1000
       static mantle_api::UniqueId id{1000};
-      return id++;
-    }));
+      return id++; }));
   }
 
   void SET_VEHICLE_MOCK_NAME_AND_ID(const std::string& name, mantle_api::UniqueId id)
@@ -168,7 +168,7 @@ TEST_F(ControllerCreator, GivenScenarioObjectWithController_WhenCreateIsCalled_T
   controller_creator.CreateControllers(scenario_objects);
 }
 
-TEST_F(ControllerCreator, GivenScenarioObjectWithController_WhenCreateIsCalled_ThenControllerServiceContainsEntitiyControllers)
+TEST_F(ControllerCreator, GivenScenarioObjectWithController_WhenCreateIsCalled_ThenControllerServiceContainsEntityControllers)
 {
   constexpr mantle_api::UniqueId ENTITY_ID = 1234;
   SETUP_ENTITY("test_entity", ENTITY_ID, true, true);
diff --git a/generator/model/v1_3.json b/generator/model/v1_3.json
index dc20d2c9..e74e23b4 100755
--- a/generator/model/v1_3.json
+++ b/generator/model/v1_3.json
@@ -10545,7 +10545,7 @@
   "ScenarioObjectTemplate": {
     "isModelGroupChoice": false,
     "properties": {
-      "entitiyObject": {
+      "entityObject": {
         "isSimpleContentProperty": false,
         "isList": false,
         "isOptional": false,
-- 
GitLab


From b054b27025d99828d7d1cadc9d5bd93fc82e6dbd Mon Sep 17 00:00:00 2001
From: Noah Schick <noah.schick@in-tech.com>
Date: Fri, 20 Dec 2024 10:56:18 +0100
Subject: [PATCH 04/26] Rework TrafficAreaAction Spawning

Add Spawning Utils
---
 engine/cmake/generated_files.cmake            |  23 +--
 .../GenericAction/TrafficAreaAction.h         |  61 --------
 .../GenericAction/TrafficAreaAction_base.h    |  47 ------
 .../GenericAction/TrafficAreaAction_impl.cpp  |  27 ----
 .../GenericAction/TrafficAreaAction_impl.h    |  27 ----
 .../GenericAction/TrafficSourceAction.h       |   6 +-
 .../GenericAction/TrafficSourceAction_base.h  |   2 +-
 .../ConvertScenarioTrafficArea.cpp            |  51 ++++++
 .../OscToMantle/ConvertScenarioTrafficArea.h  |  15 +-
 .../ConvertScenarioTrafficDistribution.cpp    |  46 ++++++
 .../ConvertScenarioTrafficDistribution.h      |  18 ++-
 engine/src/Storyboard/GenericAction/Action.h  | 119 ++++++++++++++
 .../ActivateControllerAction.cpp              |  76 +++++----
 .../GenericAction/ActivateControllerAction.h  |  62 ++++++--
 .../ActivateControllerAction_base.h           |  57 -------
 .../ActivateControllerAction_impl.cpp         |  47 ------
 .../ActivateControllerAction_impl.h           |  29 ----
 .../GenericAction/LightStateAction.cpp        |  65 ++++++++
 .../GenericAction/LightStateAction.h          |  81 ++++++----
 .../GenericAction/LightStateAction_base.h     |  48 ------
 .../GenericAction/LightStateAction_impl.cpp   |  38 -----
 .../GenericAction/LightStateAction_impl.h     |  33 ----
 .../GenericAction/TrafficAreaAction.cpp       | 147 ++++++++++++++++++
 .../GenericAction/TrafficAreaAction.h         |  76 +++++++++
 engine/src/Utils/Spawning/Common.h            |  45 ++++++
 engine/src/Utils/Spawning/Interval.cpp        |  27 ++++
 engine/src/Utils/Spawning/Interval.h          | 112 +++++++++++++
 engine/src/Utils/Spawning/Iterable.h          | 131 ++++++++++++++++
 engine/src/Utils/Spawning/Length.cpp          |  52 +++++++
 engine/src/Utils/Spawning/Length.h            |  96 ++++++++++++
 engine/src/Utils/Spawning/SpawnZone.cpp       | 104 +++++++++++++
 engine/src/Utils/Spawning/SpawnZone.h         |  97 ++++++++++++
 engine/src/Utils/Spawning/Transform.h         |  49 ++++++
 .../ActivateControllerActionTest.cpp          |   2 +-
 34 files changed, 1396 insertions(+), 520 deletions(-)
 delete mode 100644 engine/gen/Storyboard/GenericAction/TrafficAreaAction.h
 delete mode 100644 engine/gen/Storyboard/GenericAction/TrafficAreaAction_base.h
 delete mode 100644 engine/gen/Storyboard/GenericAction/TrafficAreaAction_impl.cpp
 delete mode 100644 engine/gen/Storyboard/GenericAction/TrafficAreaAction_impl.h
 create mode 100644 engine/src/Conversion/OscToMantle/ConvertScenarioTrafficArea.cpp
 create mode 100644 engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.cpp
 create mode 100644 engine/src/Storyboard/GenericAction/Action.h
 delete mode 100644 engine/src/Storyboard/GenericAction/ActivateControllerAction_base.h
 delete mode 100644 engine/src/Storyboard/GenericAction/ActivateControllerAction_impl.cpp
 delete mode 100644 engine/src/Storyboard/GenericAction/ActivateControllerAction_impl.h
 create mode 100644 engine/src/Storyboard/GenericAction/LightStateAction.cpp
 delete mode 100644 engine/src/Storyboard/GenericAction/LightStateAction_base.h
 delete mode 100644 engine/src/Storyboard/GenericAction/LightStateAction_impl.cpp
 delete mode 100644 engine/src/Storyboard/GenericAction/LightStateAction_impl.h
 create mode 100644 engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp
 create mode 100644 engine/src/Storyboard/GenericAction/TrafficAreaAction.h
 create mode 100644 engine/src/Utils/Spawning/Common.h
 create mode 100644 engine/src/Utils/Spawning/Interval.cpp
 create mode 100644 engine/src/Utils/Spawning/Interval.h
 create mode 100644 engine/src/Utils/Spawning/Iterable.h
 create mode 100644 engine/src/Utils/Spawning/Length.cpp
 create mode 100644 engine/src/Utils/Spawning/Length.h
 create mode 100644 engine/src/Utils/Spawning/SpawnZone.cpp
 create mode 100644 engine/src/Utils/Spawning/SpawnZone.h
 create mode 100644 engine/src/Utils/Spawning/Transform.h

diff --git a/engine/cmake/generated_files.cmake b/engine/cmake/generated_files.cmake
index a1d1a403..7a4e54a1 100644
--- a/engine/cmake/generated_files.cmake
+++ b/engine/cmake/generated_files.cmake
@@ -133,7 +133,6 @@ list(APPEND ${PROJECT_NAME}_SOURCES
     gen/Storyboard/GenericAction/RandomRouteAction_impl.cpp
     gen/Storyboard/GenericAction/SetMonitorAction_impl.cpp
     gen/Storyboard/GenericAction/SpeedProfileAction_impl.cpp
-    gen/Storyboard/GenericAction/TrafficAreaAction_impl.cpp
     gen/Storyboard/GenericAction/TrafficSignalControllerAction_impl.cpp
     gen/Storyboard/GenericAction/TrafficSourceAction_impl.cpp
     gen/Storyboard/GenericAction/TrafficStopAction_impl.cpp
@@ -163,7 +162,9 @@ list(APPEND ${PROJECT_NAME}_SOURCES
     src/Conversion/OscToMantle/ConvertScenarioSpeedActionTarget.cpp
     src/Conversion/OscToMantle/ConvertScenarioTimeReference.cpp
     src/Conversion/OscToMantle/ConvertScenarioTimeToCollisionConditionTarget.cpp
+    src/Conversion/OscToMantle/ConvertScenarioTrafficArea.cpp
     src/Conversion/OscToMantle/ConvertScenarioTrafficDefinition.cpp
+    src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.cpp
     src/Conversion/OscToMantle/ConvertScenarioTrafficSignalController.cpp
     src/Conversion/OscToMantle/ConvertScenarioTrajectoryRef.cpp
     src/Conversion/OscToMantle/ConvertScenarioTransitionDynamics.cpp
@@ -211,13 +212,13 @@ list(APPEND ${PROJECT_NAME}_SOURCES
     src/Storyboard/ByValueCondition/UserDefinedValueCondition_impl.cpp
     src/Storyboard/GenericAction/AcquirePositionAction_impl.cpp
     src/Storyboard/GenericAction/ActivateControllerAction.cpp
-    src/Storyboard/GenericAction/ActivateControllerAction_impl.cpp
     src/Storyboard/GenericAction/AssignControllerAction_impl.cpp
     src/Storyboard/GenericAction/AssignRouteAction_impl.cpp
     src/Storyboard/GenericAction/CustomCommandAction_impl.cpp
     src/Storyboard/GenericAction/EnvironmentAction_impl.cpp
-    src/Storyboard/GenericAction/LightStateAction_impl.cpp
+    src/Storyboard/GenericAction/LightStateAction.cpp
     src/Storyboard/GenericAction/TeleportAction_impl.cpp
+    src/Storyboard/GenericAction/TrafficAreaAction.cpp
     src/Storyboard/GenericAction/TrafficSignalStateAction_impl.cpp
     src/Storyboard/GenericAction/TrafficSinkAction_base.h
     src/Storyboard/GenericAction/TrafficSinkAction_impl.cpp
@@ -231,6 +232,9 @@ list(APPEND ${PROJECT_NAME}_SOURCES
     src/Storyboard/MotionControlAction/LateralDistanceAction_impl.cpp
     src/Storyboard/MotionControlAction/LongitudinalDistanceAction_impl.cpp
     src/Storyboard/MotionControlAction/SpeedAction_impl.cpp
+    src/Utils/Spawning/Interval.cpp
+    src/Utils/Spawning/Length.cpp
+    src/Utils/Spawning/SpawnZone.cpp
     src/Utils/ConditionEdgeEvaluator.cpp
     src/Utils/ControllerCreator.cpp
     src/Utils/ControllerService.cpp
@@ -458,9 +462,6 @@ list(APPEND ${PROJECT_NAME}_HEADERS
     gen/Storyboard/GenericAction/TeleportAction.h
     gen/Storyboard/GenericAction/TeleportAction_base.h
     gen/Storyboard/GenericAction/TeleportAction_impl.h
-    gen/Storyboard/GenericAction/TrafficAreaAction.h
-    gen/Storyboard/GenericAction/TrafficAreaAction_base.h
-    gen/Storyboard/GenericAction/TrafficAreaAction_impl.h
     gen/Storyboard/GenericAction/TrafficSignalControllerAction.h
     gen/Storyboard/GenericAction/TrafficSignalControllerAction_base.h
     gen/Storyboard/GenericAction/TrafficSignalControllerAction_impl.h
@@ -573,12 +574,9 @@ list(APPEND ${PROJECT_NAME}_HEADERS
     src/Storyboard/ByEntityCondition/TraveledDistanceCondition_impl.h
     src/Storyboard/ByValueCondition/UserDefinedValueCondition_impl.h
     src/Storyboard/GenericAction/ActivateControllerAction.h
-    src/Storyboard/GenericAction/ActivateControllerAction_base.h
-    src/Storyboard/GenericAction/ActivateControllerAction_impl.h
     src/Storyboard/GenericAction/CustomCommandAction_impl.h
     src/Storyboard/GenericAction/LightStateAction.h
-    src/Storyboard/GenericAction/LightStateAction_base.h
-    src/Storyboard/GenericAction/LightStateAction_impl.h
+    src/Storyboard/GenericAction/TrafficAreaAction.h
     src/Storyboard/GenericAction/TrafficSignalAction.h
     src/Storyboard/GenericAction/TrafficSwarmAction.h
     src/Storyboard/GenericAction/TrafficSwarmAction_base.h
@@ -592,6 +590,11 @@ list(APPEND ${PROJECT_NAME}_HEADERS
     src/Storyboard/MotionControlAction/LongitudinalDistanceAction_base.h
     src/Storyboard/MotionControlAction/MotionControlAction.h
     src/Storyboard/MotionControlAction/SpeedAction_impl.h
+    src/Utils/Spawning/Common.h
+    src/Utils/Spawning/Interval.h
+    src/Utils/Spawning/Length.h
+    src/Utils/Spawning/SpawnZone.h
+    src/Utils/Spawning/Transform.h
     src/Utils/ConditionEdgeEvaluator.h
     src/Utils/Constants.h
     src/Utils/ControllerCreator.h
diff --git a/engine/gen/Storyboard/GenericAction/TrafficAreaAction.h b/engine/gen/Storyboard/GenericAction/TrafficAreaAction.h
deleted file mode 100644
index 0a92698d..00000000
--- a/engine/gen/Storyboard/GenericAction/TrafficAreaAction.h
+++ /dev/null
@@ -1,61 +0,0 @@
-/********************************************************************************
- * Copyright (c) 2021-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0.
- *
- * SPDX-License-Identifier: EPL-2.0
- ********************************************************************************/
-
-#pragma once
-
-#include <MantleAPI/Execution/i_environment.h>
-#include <agnostic_behavior_tree/action_node.h>
-#include <openScenarioLib/generated/v1_3/api/ApiClassInterfacesV1_3.h>
-
-#include <cassert>
-#include <utility>
-
-#include "Storyboard/GenericAction/TrafficAreaAction_impl.h"
-
-namespace OpenScenarioEngine::v1_3::Node
-{
-class TrafficAreaAction : public yase::ActionNode
-{
-public:
-  TrafficAreaAction(std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::ITrafficAreaAction> trafficAreaAction)
-      : yase::ActionNode{"TrafficAreaAction"},
-        trafficAreaAction_{trafficAreaAction}
-  {
-  }
-
-  void onInit() override{};
-
-private:
-  yase::NodeStatus tick() override
-  {
-    assert(impl_);
-    const auto is_finished = impl_->Step();
-    return is_finished ? yase::NodeStatus::kSuccess : yase::NodeStatus::kRunning;
-  };
-
-  void lookupAndRegisterData(yase::Blackboard& blackboard) final
-  {
-    std::shared_ptr<mantle_api::IEnvironment> environment = blackboard.get<std::shared_ptr<mantle_api::IEnvironment>>("Environment");
-
-    impl_ = std::make_unique<OpenScenarioEngine::v1_3::TrafficAreaAction>(
-        OpenScenarioEngine::v1_3::TrafficAreaAction::Values{
-            trafficAreaAction_->GetContinuous(),
-            trafficAreaAction_->GetNumberOfEntities(),
-            ConvertScenarioTrafficDistribution(trafficAreaAction_->GetTrafficDistribution()),
-            ConvertScenarioTrafficArea(trafficAreaAction_->GetTrafficArea())},
-        OpenScenarioEngine::v1_3::TrafficAreaAction::Interfaces{
-            environment});
-  }
-
-  std::unique_ptr<OpenScenarioEngine::v1_3::TrafficAreaAction> impl_{nullptr};
-  std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::ITrafficAreaAction> trafficAreaAction_;
-};
-
-}  // namespace OpenScenarioEngine::v1_3::Node
diff --git a/engine/gen/Storyboard/GenericAction/TrafficAreaAction_base.h b/engine/gen/Storyboard/GenericAction/TrafficAreaAction_base.h
deleted file mode 100644
index 4768c8e8..00000000
--- a/engine/gen/Storyboard/GenericAction/TrafficAreaAction_base.h
+++ /dev/null
@@ -1,47 +0,0 @@
-/********************************************************************************
- * Copyright (c) 2021-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0.
- *
- * SPDX-License-Identifier: EPL-2.0
- ********************************************************************************/
-
-#pragma once
-
-#include <MantleAPI/Execution/i_environment.h>
-
-#include "Conversion/OscToMantle/ConvertScenarioTrafficArea.h"
-#include "Conversion/OscToMantle/ConvertScenarioTrafficDistribution.h"
-
-namespace OpenScenarioEngine::v1_3
-{
-
-class TrafficAreaActionBase
-{
-public:
-  struct Values
-  {
-    bool continuous;
-    unsigned int numberOfEntities;
-    TrafficDistribution trafficDistribution;
-    TrafficArea trafficArea;
-  };
-  struct Interfaces
-  {
-    std::shared_ptr<mantle_api::IEnvironment> environment;
-  };
-
-  TrafficAreaActionBase(Values values, Interfaces interfaces)
-      : values{std::move(values)},
-        mantle{std::move(interfaces)} {};
-  virtual ~TrafficAreaActionBase() = default;
-  virtual bool Step() = 0;
-
-protected:
-  Values values;
-  Interfaces mantle;
-};
-
-}  // namespace OpenScenarioEngine::v1_3
\ No newline at end of file
diff --git a/engine/gen/Storyboard/GenericAction/TrafficAreaAction_impl.cpp b/engine/gen/Storyboard/GenericAction/TrafficAreaAction_impl.cpp
deleted file mode 100644
index 2c7b6a75..00000000
--- a/engine/gen/Storyboard/GenericAction/TrafficAreaAction_impl.cpp
+++ /dev/null
@@ -1,27 +0,0 @@
-/********************************************************************************
- * Copyright (c) 2021-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0.
- *
- * SPDX-License-Identifier: EPL-2.0
- ********************************************************************************/
-
-#include "Storyboard/GenericAction/TrafficAreaAction_impl.h"
-
-#include "Utils/Logger.h"
-
-namespace OpenScenarioEngine::v1_3
-{
-
-bool TrafficAreaAction::Step()
-{
-  // Note:
-  // - Access to values parse to mantle/ose datatypes: this->values.xxx
-  // - Access to mantle interfaces: this->mantle.xxx
-  Logger::Error("Method TrafficAreaAction::Step() not implemented yet (returning \"true\" by default)");
-  return true;
-}
-
-}  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/gen/Storyboard/GenericAction/TrafficAreaAction_impl.h b/engine/gen/Storyboard/GenericAction/TrafficAreaAction_impl.h
deleted file mode 100644
index 60371a16..00000000
--- a/engine/gen/Storyboard/GenericAction/TrafficAreaAction_impl.h
+++ /dev/null
@@ -1,27 +0,0 @@
-/********************************************************************************
- * Copyright (c) 2021-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0.
- *
- * SPDX-License-Identifier: EPL-2.0
- ********************************************************************************/
-
-#pragma once
-
-#include "Storyboard/GenericAction/TrafficAreaAction_base.h"
-
-namespace OpenScenarioEngine::v1_3
-{
-
-class TrafficAreaAction : public TrafficAreaActionBase
-{
-public:
-  TrafficAreaAction(Values values, Interfaces interfaces)
-      : TrafficAreaActionBase{std::move(values), std::move(interfaces)} {};
-
-  bool Step() override;
-};
-
-}  // namespace OpenScenarioEngine::v1_3
\ No newline at end of file
diff --git a/engine/gen/Storyboard/GenericAction/TrafficSourceAction.h b/engine/gen/Storyboard/GenericAction/TrafficSourceAction.h
index 2a582a7a..dfbcfdf4 100644
--- a/engine/gen/Storyboard/GenericAction/TrafficSourceAction.h
+++ b/engine/gen/Storyboard/GenericAction/TrafficSourceAction.h
@@ -55,9 +55,9 @@ private:
             [=]()
             { return ConvertScenarioPosition(environment, trafficSourceAction_->GetPosition()); },
             ConvertScenarioTrafficDefinition(trafficSourceAction_->GetTrafficDefinition()),
-            ConvertScenarioTrafficDistribution(trafficSourceAction_->GetTrafficDistribution())},
-        OpenScenarioEngine::v1_3::TrafficSourceAction::Interfaces{
-            environment});
+            ConvertScenarioTrafficDistribution(trafficSourceAction_->GetTrafficDistribution())  //
+        },
+        OpenScenarioEngine::v1_3::TrafficSourceAction::Interfaces{environment});
   }
 
   std::unique_ptr<OpenScenarioEngine::v1_3::TrafficSourceAction> impl_{nullptr};
diff --git a/engine/gen/Storyboard/GenericAction/TrafficSourceAction_base.h b/engine/gen/Storyboard/GenericAction/TrafficSourceAction_base.h
index afffc0b2..45bce206 100644
--- a/engine/gen/Storyboard/GenericAction/TrafficSourceAction_base.h
+++ b/engine/gen/Storyboard/GenericAction/TrafficSourceAction_base.h
@@ -32,7 +32,7 @@ public:
     double velocity;
     std::function<std::optional<mantle_api::Pose>()> GetPosition;
     TrafficDefinition trafficDefinition;
-    TrafficDistribution trafficDistribution;
+    TrafficDistributions trafficDistributions;
   };
   struct Interfaces
   {
diff --git a/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficArea.cpp b/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficArea.cpp
new file mode 100644
index 00000000..9828f445
--- /dev/null
+++ b/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficArea.cpp
@@ -0,0 +1,51 @@
+
+/********************************************************************************
+ * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+#include "ConvertScenarioTrafficArea.h"
+
+#include "Utils/Spawning/Transform.h"
+
+namespace OpenScenarioEngine::v1_3
+{
+struct ToLaneRestriction
+{
+  mantle_api::RoadCursor::LaneRestriction operator()(const NET_ASAM_OPENSCENARIO::v1_3::ILane& lane) const
+  {
+    return {lane.GetId()};
+  }
+};
+
+struct ToRoadCursor
+{
+  mantle_api::RoadCursor operator()(const NET_ASAM_OPENSCENARIO::v1_3::IRoadCursor& cursor) const
+  {
+    return {units::length::meter_t{cursor.GetS()}, cursor.GetRoadId(), TransformSharedPointers(cursor.GetLane(), ToLaneRestriction{})};
+  }
+};
+
+struct ToRoadRange
+{
+  mantle_api::RoadRange operator()(const NET_ASAM_OPENSCENARIO::v1_3::IRoadRange& range) const
+  {
+    return {(range.GetRoadCursorSize() > 1 && range.GetLength() == .0) ? std::nullopt : std::optional<units::length::meter_t>{range.GetLength()},
+            TransformSharedPointers(range.GetRoadCursor(), ToRoadCursor{})};
+  }
+};
+
+TrafficArea ConvertScenarioTrafficArea(const std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::ITrafficArea>& trafficArea)
+{
+  if (trafficArea->GetPolygon() != nullptr)
+  {
+    throw std::runtime_error("ConvertScenarioTrafficArea: Conversion of polygon not yet supported");
+  }
+  return TransformSharedPointers(trafficArea->GetRoadRange(), ToRoadRange{});
+}
+}  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficArea.h b/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficArea.h
index 6e615fd8..cbc3b3be 100644
--- a/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficArea.h
+++ b/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficArea.h
@@ -11,20 +11,15 @@
 
 #pragma once
 
+#include <MantleAPI/Traffic/road_range.h>
 #include <openScenarioLib/generated/v1_3/api/ApiClassInterfacesV1_3.h>
 
 #include <memory>
-#include <string>
+#include <vector>
 
 namespace OpenScenarioEngine::v1_3
 {
-struct TrafficArea
-{
-};
-
-inline TrafficArea ConvertScenarioTrafficArea(const std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::ITrafficArea>& /*trafficArea*/)
-{
-  return {};
-}
+using TrafficArea = std::vector<mantle_api::RoadRange>;
 
-}  // namespace OpenScenarioEngine::v1_3
\ No newline at end of file
+TrafficArea ConvertScenarioTrafficArea(const std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::ITrafficArea>&);
+}  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.cpp b/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.cpp
new file mode 100644
index 00000000..04461351
--- /dev/null
+++ b/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.cpp
@@ -0,0 +1,46 @@
+/********************************************************************************
+ * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+#include "ConvertScenarioTrafficDistribution.h"
+
+#include "Utils/Spawning/Transform.h"
+
+namespace OpenScenarioEngine::v1_3
+{
+struct ToEntities
+{
+  std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IEntityObject> operator()(const NET_ASAM_OPENSCENARIO::v1_3::IEntityDistributionEntry& entry) const
+  {
+    return entry.GetScenarioObjectTemplate()->GetEntityObject();
+  }
+
+  std::vector<std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IEntityObject>> operator()(const NET_ASAM_OPENSCENARIO::v1_3::ITrafficDistributionEntry& entry) const
+  {
+    return {TransformSharedPointers(entry.GetEntityDistribution()->GetEntityDistributionEntry(), *this)};
+  }
+};
+
+struct ToTrafficDistribution
+{
+  TrafficDistribution operator()(const NET_ASAM_OPENSCENARIO::v1_3::ITrafficDistributionEntry& entry) const
+  {
+    return {TransformSharedPointers(entry.GetEntityDistribution()->GetEntityDistributionEntry(), [](const NET_ASAM_OPENSCENARIO::v1_3::IEntityDistributionEntry& entry)
+                                    { return entry.GetScenarioObjectTemplate()->GetEntityObject(); }),
+            entry.GetWeight()};
+  }
+};
+
+TrafficDistributions ConvertScenarioTrafficDistribution(const std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::ITrafficDistribution>& distribution)
+{
+  decltype(auto) entries{distribution->GetTrafficDistributionEntry()};
+  return {TransformSharedPointers(entries, ToTrafficDistribution{}),
+          TransformSharedPointers(entries, ToAccumulatedWeight{})};
+}
+}  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.h b/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.h
index e3c7abcf..ae7421b7 100644
--- a/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.h
+++ b/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.h
@@ -1,4 +1,3 @@
-
 /********************************************************************************
  * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
  *
@@ -12,19 +11,26 @@
 #pragma once
 
 #include <openScenarioLib/generated/v1_3/api/ApiClassInterfacesV1_3.h>
+#include <units.h>
 
 #include <memory>
-#include <string>
+#include <vector>
+
+#include "Utils/Spawning/Length.h"
 
 namespace OpenScenarioEngine::v1_3
 {
 struct TrafficDistribution
 {
+  std::vector<std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IEntityObject>> entities;
+  units::dimensionless::scalar_t weight;
 };
 
-inline TrafficDistribution ConvertScenarioTrafficDistribution(const std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::ITrafficDistribution>& /*trafficDistribution*/)
+struct TrafficDistributions
 {
-  return {};
-}
+  std::vector<TrafficDistribution> distributions;
+  std::vector<units::dimensionless::scalar_t> weights;
+};
 
-}  // namespace OpenScenarioEngine::v1_3
\ No newline at end of file
+TrafficDistributions ConvertScenarioTrafficDistribution(const std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::ITrafficDistribution>&);
+}  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Storyboard/GenericAction/Action.h b/engine/src/Storyboard/GenericAction/Action.h
new file mode 100644
index 00000000..d97f656d
--- /dev/null
+++ b/engine/src/Storyboard/GenericAction/Action.h
@@ -0,0 +1,119 @@
+/********************************************************************************
+ * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+#pragma once
+
+#include <agnostic_behavior_tree/action_node.h>
+
+#include <string>
+#include <string_view>
+
+namespace OpenScenarioEngine::v1_3
+{
+template <typename>
+struct Interfaces
+{
+  std::shared_ptr<mantle_api::IEnvironment> environment;
+};
+
+template <typename>
+struct Values
+{
+};
+
+template <typename Instance>  // CRTP
+class Action
+{
+public:
+  Action(Values<Instance> values, Interfaces<Instance> interfaces)
+      : values{std::move(values)},
+        mantle{std::move(interfaces)} {};
+
+protected:
+  Values<Instance> values;
+  Interfaces<Instance> mantle;
+};
+
+namespace Node
+{
+namespace detail
+{
+template <typename>
+struct Identifier
+{
+  static constexpr std::string_view value = "UNASSIGNED_IDENTIFIER";
+};
+}  // namespace detail
+
+template <typename Type>
+constexpr std::string_view identifier = detail::Identifier<Type>::value;
+
+#define SET_NODE_IDENTIFIER(ACTION)                    \
+  namespace detail                                     \
+  {                                                    \
+  template <>                                          \
+  struct Identifier<ACTION>                            \
+  {                                                    \
+    static constexpr std::string_view value = #ACTION; \
+  };                                                   \
+  }
+
+namespace detail
+{
+template <typename>
+struct Implementation
+{
+  using type = void;
+};
+}  // namespace detail
+
+template <typename Type>
+using Implementation = typename detail::Implementation<Type>::type;
+
+#define SPECIALIZE_NODE_TYPE_TRAIT(TRAIT, ACTION, TYPE) \
+  namespace detail                                      \
+  {                                                     \
+  template <>                                           \
+  struct TRAIT<ACTION>                                  \
+  {                                                     \
+    using type = TYPE;                                  \
+  };                                                    \
+  }
+
+#define SET_NODE_IMPLEMENTATION(ACTION, TYPE) SPECIALIZE_NODE_TYPE_TRAIT(Implementation, ACTION, TYPE)
+
+namespace detail
+{
+template <typename>
+struct Interface
+{
+  using type = void;
+};
+}  // namespace detail
+
+template <typename Type>
+using Interface = typename detail::Interface<Type>::type;
+
+#define SET_NODE_INTERFACE(ACTION, TYPE) SPECIALIZE_NODE_TYPE_TRAIT(Interface, ACTION, TYPE)
+
+template <typename Instance>
+class ActionNode : public yase::ActionNode
+{
+public:
+  ActionNode(std::shared_ptr<Interface<Instance>> action)
+      : yase::ActionNode{std::string{identifier<Instance>}},
+        action_{action} {}
+
+protected:
+  std::unique_ptr<Implementation<Instance>> impl_{nullptr};
+  std::shared_ptr<Interface<Instance>> action_;
+};
+}  // namespace Node
+}  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Storyboard/GenericAction/ActivateControllerAction.cpp b/engine/src/Storyboard/GenericAction/ActivateControllerAction.cpp
index 646c8cfd..aa2ccf2a 100644
--- a/engine/src/Storyboard/GenericAction/ActivateControllerAction.cpp
+++ b/engine/src/Storyboard/GenericAction/ActivateControllerAction.cpp
@@ -15,12 +15,43 @@
 #include <cassert>
 #include <utility>
 
+#include "Storyboard/GenericAction/ActivateControllerAction.h"
 #include "Utils/EntityBroker.h"
+#include "Utils/EntityUtils.h"
 #include "Utils/IControllerService.h"
+#include "Utils/Logger.h"
 
-namespace OpenScenarioEngine::v1_3::Node
+namespace OpenScenarioEngine::v1_3
 {
+bool ActivateControllerAction::Step()
+{
+  const auto& entity_repository = mantle.environment->GetEntityRepository();
+
+  for (const auto& entity_name : values.entities)
+  {
+    // entities might have been despawned, leading to a dangling reference to the controllers
+    // so we check if the entity is still part of the simulation, before we access the controllers
+    if (auto entity_ref = entity_repository.Get(entity_name))
+    {
+      const auto entity_id = entity_ref->get().GetUniqueId();
+      if (const auto& entity_controllers = ose_services.controllerService->GetControllers(entity_id))
+      {
+        auto controller_id = ose_services.controllerService->GetControllerId(
+            values.controllerRef, *entity_controllers);
+        ose_services.controllerService->ChangeState(
+            entity_id, controller_id, values.lateral_state, values.longitudinal_state);
+      }
+      else
+      {
+        Logger::Error("ActivateControllerAction: No user defined controller available for " + entity_name);
+      }
+    }
+  }
 
+  return true;
+}
+namespace Node
+{
 namespace detail
 {
 template <typename TargetState>
@@ -34,13 +65,6 @@ auto convert_to(bool is_set, bool value)
 }
 }  // namespace detail
 
-ActivateControllerAction::ActivateControllerAction(
-    std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IActivateControllerAction> activateControllerAction)
-    : yase::ActionNode{"ActivateControllerAction"},
-      activateControllerAction_{activateControllerAction}
-{
-}
-
 void ActivateControllerAction::onInit() {}
 
 yase::NodeStatus ActivateControllerAction::tick()
@@ -57,30 +81,22 @@ void ActivateControllerAction::lookupAndRegisterData(yase::Blackboard& blackboar
   auto controllerService = blackboard.get<ControllerServicePtr>("ControllerService");
 
   impl_ = std::make_unique<OpenScenarioEngine::v1_3::ActivateControllerAction>(
-      OpenScenarioEngine::v1_3::ActivateControllerAction::Values{
+      Values<OpenScenarioEngine::v1_3::ActivateControllerAction>{
           entityBroker->GetEntities(),
-
-          activateControllerAction_->IsSetAnimation() ? std::make_optional<bool>(activateControllerAction_->GetAnimation()) : std::nullopt,
-
-          ConvertScenarioControllerRef(activateControllerAction_->GetControllerRef()),
-
+          action_->IsSetAnimation() ? std::make_optional<bool>(action_->GetAnimation()) : std::nullopt,
+          ConvertScenarioControllerRef(action_->GetControllerRef()),
           detail::convert_to<mantle_api::IController::LateralState>(
-              activateControllerAction_->IsSetLateral(),
-              activateControllerAction_->GetLateral()),
-
-          activateControllerAction_->IsSetLighting()
-              ? std::make_optional<bool>(activateControllerAction_->GetLighting())
+              action_->IsSetLateral(),
+              action_->GetLateral()),
+          action_->IsSetLighting()
+              ? std::make_optional<bool>(action_->GetLighting())
               : std::nullopt,
-
           detail::convert_to<mantle_api::IController::LongitudinalState>(
-              activateControllerAction_->IsSetLongitudinal(),
-              activateControllerAction_->GetLongitudinal())
-
-      },  // Values
-      OpenScenarioEngine::v1_3::ActivateControllerAction::Interfaces{
-          environment},
-      OpenScenarioEngine::v1_3::ActivateControllerAction::OseServices{
-          controllerService});
+              action_->IsSetLongitudinal(),
+              action_->GetLongitudinal())  //
+      },
+      Interfaces<OpenScenarioEngine::v1_3::ActivateControllerAction>{environment},
+      OseServices{controllerService});
 }
-
-}  // namespace OpenScenarioEngine::v1_3::Node
+}  // namespace Node
+}  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Storyboard/GenericAction/ActivateControllerAction.h b/engine/src/Storyboard/GenericAction/ActivateControllerAction.h
index 601a3d3b..a154b360 100644
--- a/engine/src/Storyboard/GenericAction/ActivateControllerAction.h
+++ b/engine/src/Storyboard/GenericAction/ActivateControllerAction.h
@@ -10,25 +10,65 @@
 
 #pragma once
 
-#include <agnostic_behavior_tree/action_node.h>
-#include <openScenarioLib/generated/v1_3/api/ApiClassInterfacesV1_3.h>
+#include <MantleAPI/Execution/i_environment.h>
+#include <MantleAPI/Traffic/i_controller.h>
 
-#include "Storyboard/GenericAction/ActivateControllerAction_impl.h"
+#include "Action.h"
+#include "Conversion/OscToMantle/ConvertScenarioController.h"
+#include "Utils/EntityBroker.h"
+#include "Utils/IControllerService.h"
 
-namespace OpenScenarioEngine::v1_3::Node
+namespace OpenScenarioEngine::v1_3
 {
-class ActivateControllerAction : public yase::ActionNode
+struct OseServices
+{
+  ControllerServicePtr controllerService;
+};
+
+class ActivateControllerAction;
+
+template <>
+struct Values<ActivateControllerAction>
+{
+  Entities entities;
+  std::optional<bool> animation;
+  std::optional<std::string> controllerRef;
+  mantle_api::IController::LateralState lateral_state;
+  std::optional<bool> lighting;
+  mantle_api::IController::LongitudinalState longitudinal_state;
+};
+
+class ActivateControllerAction : Action<ActivateControllerAction>
 {
 public:
-  ActivateControllerAction(std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IActivateControllerAction> activateControllerAction);
+  ActivateControllerAction(Values<ActivateControllerAction> values, Interfaces<ActivateControllerAction> interfaces, OseServices ose_services)
+      : Action<ActivateControllerAction>{std::move(values), std::move(interfaces)},
+        ose_services{std::move(ose_services)} {}
+
+  bool Step();
+
+protected:
+  OseServices ose_services;
+};
+
+namespace Node
+{
+class ActivateControllerAction;
+SET_NODE_IDENTIFIER(ActivateControllerAction)
+SET_NODE_IMPLEMENTATION(ActivateControllerAction, OpenScenarioEngine::v1_3::ActivateControllerAction)
+SET_NODE_INTERFACE(ActivateControllerAction, NET_ASAM_OPENSCENARIO::v1_3::IActivateControllerAction)
+
+class ActivateControllerAction : public ActionNode<ActivateControllerAction>
+{
+public:
+  using ActionNode<ActivateControllerAction>::ActionNode;
+
   void onInit() override;
 
 private:
   yase::NodeStatus tick() override;
-  void lookupAndRegisterData(yase::Blackboard& blackboard) final;
 
-  std::unique_ptr<OpenScenarioEngine::v1_3::ActivateControllerAction> impl_{nullptr};
-  std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IActivateControllerAction> activateControllerAction_;
+  void lookupAndRegisterData(yase::Blackboard& blackboard) final;
 };
-
-}  // namespace OpenScenarioEngine::v1_3::Node
\ No newline at end of file
+}  // namespace Node
+}  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Storyboard/GenericAction/ActivateControllerAction_base.h b/engine/src/Storyboard/GenericAction/ActivateControllerAction_base.h
deleted file mode 100644
index bbdd2091..00000000
--- a/engine/src/Storyboard/GenericAction/ActivateControllerAction_base.h
+++ /dev/null
@@ -1,57 +0,0 @@
-/********************************************************************************
- * Copyright (c) 2021-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0.
- *
- * SPDX-License-Identifier: EPL-2.0
- ********************************************************************************/
-
-#pragma once
-
-#include <MantleAPI/Execution/i_environment.h>
-#include <MantleAPI/Traffic/i_controller.h>
-
-#include "Conversion/OscToMantle/ConvertScenarioController.h"
-#include "Utils/EntityBroker.h"
-#include "Utils/IControllerService.h"
-
-namespace OpenScenarioEngine::v1_3
-{
-class ActivateControllerActionBase
-{
-public:
-  struct Values
-  {
-    Entities entities;
-    std::optional<bool> animation;
-    std::optional<std::string> controllerRef;
-    mantle_api::IController::LateralState lateral_state;
-    std::optional<bool> lighting;
-    mantle_api::IController::LongitudinalState longitudinal_state;
-  };
-  struct Interfaces
-  {
-    std::shared_ptr<mantle_api::IEnvironment> environment;
-  };
-
-  struct OseServices
-  {
-    ControllerServicePtr controllerService;
-  };
-
-  ActivateControllerActionBase(Values values, Interfaces interfaces, OseServices ose_services)
-      : values{std::move(values)},
-        mantle{std::move(interfaces)},
-        ose_services{std::move(ose_services)} {};
-  virtual ~ActivateControllerActionBase() = default;
-  virtual bool Step() = 0;
-
-protected:
-  Values values;
-  Interfaces mantle;
-  OseServices ose_services;
-};
-
-}  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Storyboard/GenericAction/ActivateControllerAction_impl.cpp b/engine/src/Storyboard/GenericAction/ActivateControllerAction_impl.cpp
deleted file mode 100644
index 92d6399a..00000000
--- a/engine/src/Storyboard/GenericAction/ActivateControllerAction_impl.cpp
+++ /dev/null
@@ -1,47 +0,0 @@
-/********************************************************************************
- * Copyright (c) 2021-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0.
- *
- * SPDX-License-Identifier: EPL-2.0
- ********************************************************************************/
-
-#include "Storyboard/GenericAction/ActivateControllerAction_impl.h"
-
-#include "Utils/EntityUtils.h"
-#include "Utils/Logger.h"
-
-namespace OpenScenarioEngine::v1_3
-{
-
-bool ActivateControllerAction::Step()
-{
-  const auto& entity_repository = mantle.environment->GetEntityRepository();
-
-  for (const auto& entity_name : values.entities)
-  {
-    // entities might have been despawned, leading to a dangling reference to the controllers
-    // so we check if the entity is still part of the simulation, before we access the controllers
-    if (auto entity_ref = entity_repository.Get(entity_name))
-    {
-      const auto entity_id = entity_ref->get().GetUniqueId();
-      if (const auto& entity_controllers = ose_services.controllerService->GetControllers(entity_id))
-      {
-        auto controller_id = ose_services.controllerService->GetControllerId(
-            values.controllerRef, *entity_controllers);
-        ose_services.controllerService->ChangeState(
-            entity_id, controller_id, values.lateral_state, values.longitudinal_state);
-      }
-      else
-      {
-        Logger::Error("ActivateControllerAction: No user defined controller available for " + entity_name);
-      }
-    }
-  }
-
-  return true;
-}
-
-}  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Storyboard/GenericAction/ActivateControllerAction_impl.h b/engine/src/Storyboard/GenericAction/ActivateControllerAction_impl.h
deleted file mode 100644
index 701d514f..00000000
--- a/engine/src/Storyboard/GenericAction/ActivateControllerAction_impl.h
+++ /dev/null
@@ -1,29 +0,0 @@
-/********************************************************************************
- * Copyright (c) 2021-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0.
- *
- * SPDX-License-Identifier: EPL-2.0
- ********************************************************************************/
-
-#pragma once
-
-#include "Storyboard/GenericAction/ActivateControllerAction_base.h"
-
-namespace OpenScenarioEngine::v1_3
-{
-class ActivateControllerAction : public ActivateControllerActionBase
-{
-public:
-  ActivateControllerAction(Values values, Interfaces interfaces, OseServices ose_services)
-      : ActivateControllerActionBase{
-            std::move(values),
-            std::move(interfaces),
-            std::move(ose_services)} {};
-
-  bool Step() override;
-};
-
-}  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Storyboard/GenericAction/LightStateAction.cpp b/engine/src/Storyboard/GenericAction/LightStateAction.cpp
new file mode 100644
index 00000000..7f225d0a
--- /dev/null
+++ b/engine/src/Storyboard/GenericAction/LightStateAction.cpp
@@ -0,0 +1,65 @@
+/********************************************************************************
+ * Copyright (c) 2021-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+#include "Storyboard/GenericAction/LightStateAction.h"
+
+#include <MantleAPI/Execution/i_environment.h>
+
+#include "Conversion/OscToMantle/ConvertScenarioLightState.h"
+#include "Conversion/OscToMantle/ConvertScenarioLightType.h"
+#include "Utils/EntityUtils.h"
+#include "Utils/Logger.h"
+
+namespace OpenScenarioEngine::v1_3
+{
+
+bool LightStateAction::Step()
+{
+  SetControlStrategy();
+  return true;
+}
+
+void LightStateAction::SetControlStrategy()
+{
+  control_strategy_->light_type = values.lightType;
+  control_strategy_->light_state = values.lightState;
+  for (const auto& actor : values.entities)
+  {
+    auto& entity = EntityUtils::GetEntityByName(mantle.environment, actor);
+    mantle.environment->UpdateControlStrategies(
+        entity.GetUniqueId(),
+        {control_strategy_});
+  }
+}
+namespace Node
+{
+yase::NodeStatus LightStateAction::tick()
+{
+  assert(impl_);
+  const auto is_finished = impl_->Step();
+  return is_finished ? yase::NodeStatus::kSuccess : yase::NodeStatus::kRunning;
+};
+
+void LightStateAction::lookupAndRegisterData(yase::Blackboard& blackboard)
+{
+  std::shared_ptr<mantle_api::IEnvironment> environment = blackboard.get<std::shared_ptr<mantle_api::IEnvironment>>("Environment");
+  const EntityBroker::Ptr entityBroker = blackboard.get<EntityBroker::Ptr>("EntityBroker");
+
+  impl_ = std::make_unique<OpenScenarioEngine::v1_3::LightStateAction>(
+      Values<OpenScenarioEngine::v1_3::LightStateAction>{
+          entityBroker->GetEntities(),
+          action_->GetTransitionTime(),
+          ConvertScenarioLightType(action_->GetLightType()),
+          ConvertScenarioLightState(action_->GetLightState())  //
+      },
+      Interfaces<OpenScenarioEngine::v1_3::LightStateAction>{environment});
+}
+}  // namespace Node
+}  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Storyboard/GenericAction/LightStateAction.h b/engine/src/Storyboard/GenericAction/LightStateAction.h
index 3311528a..982fb916 100644
--- a/engine/src/Storyboard/GenericAction/LightStateAction.h
+++ b/engine/src/Storyboard/GenericAction/LightStateAction.h
@@ -17,47 +17,60 @@
 #include <cassert>
 #include <utility>
 
-#include "Storyboard/GenericAction/LightStateAction_impl.h"
+#include "Action.h"
 #include "Utils/EntityBroker.h"
 
-namespace OpenScenarioEngine::v1_3::Node
+namespace OpenScenarioEngine::v1_3
 {
-class LightStateAction : public yase::ActionNode
+class LightStateAction;
+
+template <>
+struct Values<LightStateAction>
+{
+  Entities entities;
+  double transitionTime;
+  mantle_api::LightType lightType;
+  mantle_api::LightState lightState;
+};
+
+template <>
+struct Interfaces<LightStateAction>
+{
+  std::shared_ptr<mantle_api::IEnvironment> environment;
+};
+
+class LightStateAction : Action<LightStateAction>
 {
 public:
-  LightStateAction(std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::ILightStateAction> lightStateAction)
-      : yase::ActionNode{"LightStateAction"},
-        lightStateAction_{lightStateAction}
-  {
-  }
+  using Action<LightStateAction>::Action;
 
-  void onInit() override{};
+  bool Step();
 
 private:
-  yase::NodeStatus tick() override
-  {
-    assert(impl_);
-    const auto is_finished = impl_->Step();
-    return is_finished ? yase::NodeStatus::kSuccess : yase::NodeStatus::kRunning;
-  };
-
-  void lookupAndRegisterData(yase::Blackboard& blackboard) final
-  {
-    std::shared_ptr<mantle_api::IEnvironment> environment = blackboard.get<std::shared_ptr<mantle_api::IEnvironment>>("Environment");
-    const EntityBroker::Ptr entityBroker = blackboard.get<EntityBroker::Ptr>("EntityBroker");
-
-    impl_ = std::make_unique<OpenScenarioEngine::v1_3::LightStateAction>(
-        OpenScenarioEngine::v1_3::LightStateAction::Values{
-            entityBroker->GetEntities(),
-            lightStateAction_->GetTransitionTime(),
-            ConvertScenarioLightType(lightStateAction_->GetLightType()),
-            ConvertScenarioLightState(lightStateAction_->GetLightState())},
-        OpenScenarioEngine::v1_3::LightStateAction::Interfaces{
-            environment});
-  }
-
-  std::unique_ptr<OpenScenarioEngine::v1_3::LightStateAction> impl_{nullptr};
-  std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::ILightStateAction> lightStateAction_;
+  void SetControlStrategy();
+
+  std::shared_ptr<mantle_api::VehicleLightStatesControlStrategy> control_strategy_{
+      std::make_shared<mantle_api::VehicleLightStatesControlStrategy>()};
 };
 
-}  // namespace OpenScenarioEngine::v1_3::Node
+namespace Node
+{
+class LightStateAction;
+SET_NODE_IDENTIFIER(LightStateAction)
+SET_NODE_IMPLEMENTATION(LightStateAction, OpenScenarioEngine::v1_3::LightStateAction)
+SET_NODE_INTERFACE(LightStateAction, NET_ASAM_OPENSCENARIO::v1_3::ILightStateAction)
+
+class LightStateAction : public ActionNode<LightStateAction>
+{
+public:
+  using ActionNode<LightStateAction>::ActionNode;
+
+  void onInit() override{};
+
+private:
+  yase::NodeStatus tick() override;
+
+  void lookupAndRegisterData(yase::Blackboard& blackboard) final;
+};
+}  // namespace Node
+}  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Storyboard/GenericAction/LightStateAction_base.h b/engine/src/Storyboard/GenericAction/LightStateAction_base.h
deleted file mode 100644
index efcd12a3..00000000
--- a/engine/src/Storyboard/GenericAction/LightStateAction_base.h
+++ /dev/null
@@ -1,48 +0,0 @@
-/********************************************************************************
- * Copyright (c) 2021-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0.
- *
- * SPDX-License-Identifier: EPL-2.0
- ********************************************************************************/
-
-#pragma once
-
-#include <MantleAPI/Execution/i_environment.h>
-
-#include "Conversion/OscToMantle/ConvertScenarioLightState.h"
-#include "Conversion/OscToMantle/ConvertScenarioLightType.h"
-#include "Utils/EntityBroker.h"
-
-namespace OpenScenarioEngine::v1_3
-{
-
-class LightStateActionBase
-{
-public:
-  struct Values
-  {
-    Entities entities;
-    double transitionTime;
-    mantle_api::LightType lightType;
-    mantle_api::LightState lightState;
-  };
-  struct Interfaces
-  {
-    std::shared_ptr<mantle_api::IEnvironment> environment;
-  };
-
-  LightStateActionBase(Values values, Interfaces interfaces)
-      : values{std::move(values)},
-        mantle{std::move(interfaces)} {};
-  virtual ~LightStateActionBase() = default;
-  virtual bool Step() = 0;
-
-protected:
-  Values values;
-  Interfaces mantle;
-};
-
-}  // namespace OpenScenarioEngine::v1_3
\ No newline at end of file
diff --git a/engine/src/Storyboard/GenericAction/LightStateAction_impl.cpp b/engine/src/Storyboard/GenericAction/LightStateAction_impl.cpp
deleted file mode 100644
index e993a670..00000000
--- a/engine/src/Storyboard/GenericAction/LightStateAction_impl.cpp
+++ /dev/null
@@ -1,38 +0,0 @@
-/********************************************************************************
- * Copyright (c) 2021-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0.
- *
- * SPDX-License-Identifier: EPL-2.0
- ********************************************************************************/
-
-#include "Storyboard/GenericAction/LightStateAction_impl.h"
-
-#include "Utils/EntityUtils.h"
-#include "Utils/Logger.h"
-
-namespace OpenScenarioEngine::v1_3
-{
-
-bool LightStateAction::Step()
-{
-  SetControlStrategy();
-  return true;
-}
-
-void LightStateAction::SetControlStrategy()
-{
-  control_strategy_->light_type = values.lightType;
-  control_strategy_->light_state = values.lightState;
-  for (const auto& actor : values.entities)
-  {
-    auto& entity = EntityUtils::GetEntityByName(mantle.environment, actor);
-    mantle.environment->UpdateControlStrategies(
-        entity.GetUniqueId(),
-        {control_strategy_});
-  }
-}
-
-}  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Storyboard/GenericAction/LightStateAction_impl.h b/engine/src/Storyboard/GenericAction/LightStateAction_impl.h
deleted file mode 100644
index 2ea1c6fb..00000000
--- a/engine/src/Storyboard/GenericAction/LightStateAction_impl.h
+++ /dev/null
@@ -1,33 +0,0 @@
-/********************************************************************************
- * Copyright (c) 2021-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0.
- *
- * SPDX-License-Identifier: EPL-2.0
- ********************************************************************************/
-
-#pragma once
-
-#include "Storyboard/GenericAction/LightStateAction_base.h"
-
-namespace OpenScenarioEngine::v1_3
-{
-
-class LightStateAction : public LightStateActionBase
-{
-public:
-  LightStateAction(Values values, Interfaces interfaces)
-      : LightStateActionBase{std::move(values), std::move(interfaces)} {};
-
-  bool Step() override;
-
-private:
-  void SetControlStrategy();
-
-  std::shared_ptr<mantle_api::VehicleLightStatesControlStrategy> control_strategy_{
-      std::make_shared<mantle_api::VehicleLightStatesControlStrategy>()};
-};
-
-}  // namespace OpenScenarioEngine::v1_3
\ No newline at end of file
diff --git a/engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp b/engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp
new file mode 100644
index 00000000..1733fd88
--- /dev/null
+++ b/engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp
@@ -0,0 +1,147 @@
+/********************************************************************************
+ * Copyright (c) 2021-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+#include "Storyboard/GenericAction/TrafficAreaAction.h"
+
+#include "Conversion/OscToMantle/ConvertScenarioTrafficArea.h"
+#include "Utils/EntityCreator.h"
+#include "Utils/Logger.h"
+#include "Utils/Spawning/Length.h"
+#include "Utils/Spawning/Transform.h"
+
+namespace OpenScenarioEngine::v1_3
+{
+void TrafficAreaAction::UpdateTrafficAreas()
+{
+  trafficAreas = Transform(values.trafficArea, [this](const mantle_api::RoadRange& range)
+                           { return mantle.environment->GetTrafficAreaService().CreateTrafficArea(range); });
+  lengths = Transform(trafficAreas, ToAccumulatedLength{});
+}
+
+bool TrafficAreaAction::Step()
+{
+  // Spawn entities in a randomly selected area among this action's defined traffic areas.
+  // Selection is weighted based on the length of each area's remaining spawnable area (SpawnZone).
+
+  // Traffic distributions determine which vehicles can spawn and their likelihood of spawning. The distributions are sorted by
+  // entity length for efficient filtering in case the largest interval grows smaller than some spawnable entities. Similary, a
+  // SpawnZone tracks its largest interval (without sorting, as it would have to be reordered each time an entity is spawned).
+
+  // The general process of the spawn loop must go as follows:
+  // - Filter out the entities of all traffic distributions longer than the largest interval of all areas.
+  //   (can be skipped if the last spawned entity was not on the largest interval)
+  // - Sample the entity to be spawned based on the weights of all entity distributions
+  // - Filter out the intervals of all areas shorter than the entity to be spawned
+  // - Sample the spawn position from the total length of all remaining intervals
+  // - Shrink the sampled interval interval by the front and rear offset from the entity's center to its
+  //   front and rear bounds, scaling the sampled value accordingly.
+  // - Spawn the entity and update the computed area intervals and lengths.
+
+  if (values.numberOfEntities == 0)  // No entities left to spawn
+  {
+    return true;
+  }
+  if (values.trafficDistributions.distributions.empty())  // No spawnable entities
+  {
+    return false;
+  }
+  if (trafficAreas.empty())
+  {
+    UpdateTrafficAreas();
+    if (trafficAreas.empty())  // No areas to spawn entities in
+    {
+      return false;
+    }
+  }
+  EntityCreator spawner{mantle.environment};
+
+  if (values.trafficDistributions.distributions.size() == 1)
+  {
+    auto& distributions{values.trafficDistributions.distributions.front()};
+    if (distributions.entities.size() == 1)  // Skip sampling entities if there is only one
+    {
+      if (trafficAreas.size() == 1)
+      {
+        SpawnZones zones{trafficAreas.front(), [threshold = GetLength(distributions.entities.front())](const Interval& interval)
+                         {
+                           return interval.GetLength() >= threshold;
+                         }};
+        while (values.numberOfEntities)
+        {
+          if (zones.empty())
+          {  // No room left
+            return false;
+          }
+        }
+      }
+    }
+  }
+
+  if (trafficAreas.size() == 1)  // Skip sampling areas if there is only one
+  {
+    auto& area{trafficAreas.front()};
+    // TODO: Not yet implemented
+    while (values.numberOfEntities)
+    {
+      --values.numberOfEntities;
+    }
+    return true;
+  }
+  std::vector<SpawnZones> spawnZones{Transform(trafficAreas, ToSpawnZones{})};
+  std::vector<units::length::meter_t> weights{Transform(spawnZones, ToAccumulatedLength{})};
+  while (values.numberOfEntities)
+  {
+    auto [sampleA, indexA]{Sample(weights)};
+    auto sampleB{sampleA - weights[indexA]};
+    const SpawnZones& zones{spawnZones[indexA]};
+    const auto upperBound{std::prev(std::upper_bound(std::next(zones.lengths.begin()), std::prev(zones.lengths.end()), sampleB))};
+    const auto indexB{static_cast<size_t>(std::distance(zones.lengths.begin(), upperBound))};
+    const SpawnZone& zone{zones[indexB]};
+    auto sampleC{sampleB - zones.lengths[indexB]};
+
+    // TODO: WIP
+    // spawner.CreateEntity()
+
+    --values.numberOfEntities;
+  }
+  // spawner.CreateEntity(values.trafficDistribution.entityDistribution);
+  // mantle.environment->GetEntityRepository().Create;
+
+  // Note:
+  // - Access to values parse to mantle/ose datatypes: this->values.xxx
+  // - Access to mantle interfaces: this->mantle.xxx
+  return true;
+}
+
+namespace Node
+{
+void TrafficAreaAction::onInit(){};
+
+yase::NodeStatus TrafficAreaAction::tick()
+{
+  assert(impl_);
+  const auto is_finished = impl_->Step();
+  return is_finished ? yase::NodeStatus::kSuccess : yase::NodeStatus::kRunning;
+};
+
+void TrafficAreaAction::lookupAndRegisterData(yase::Blackboard& blackboard)
+{
+  std::shared_ptr<mantle_api::IEnvironment> environment = blackboard.get<std::shared_ptr<mantle_api::IEnvironment>>("Environment");
+
+  impl_ = std::make_unique<OpenScenarioEngine::v1_3::TrafficAreaAction>(
+      Values<OpenScenarioEngine::v1_3::TrafficAreaAction>{
+          action_->GetContinuous(),
+          action_->GetNumberOfEntities(),
+          ConvertScenarioTrafficDistribution(action_->GetTrafficDistribution()),
+          ConvertScenarioTrafficArea(action_->GetTrafficArea())},
+      Interfaces<OpenScenarioEngine::v1_3::TrafficAreaAction>{environment});
+}
+}  // namespace Node
+}  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Storyboard/GenericAction/TrafficAreaAction.h b/engine/src/Storyboard/GenericAction/TrafficAreaAction.h
new file mode 100644
index 00000000..ea72bbd6
--- /dev/null
+++ b/engine/src/Storyboard/GenericAction/TrafficAreaAction.h
@@ -0,0 +1,76 @@
+/********************************************************************************
+ * Copyright (c) 2021-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+#pragma once
+
+#include <MantleAPI/Execution/i_environment.h>
+#include <MantleAPI/Traffic/i_traffic_area_service.h>
+#include <agnostic_behavior_tree/action_node.h>
+#include <openScenarioLib/generated/v1_3/api/ApiClassInterfacesV1_3.h>
+#include <units.h>
+
+#include <cassert>
+#include <utility>
+#include <vector>
+
+#include "Action.h"
+#include "Conversion/OscToMantle/ConvertScenarioTrafficDistribution.h"
+
+namespace OpenScenarioEngine::v1_3
+{
+class TrafficAreaAction;
+
+template <>
+struct Values<TrafficAreaAction>
+{
+  bool continuous;
+  unsigned int numberOfEntities;
+  TrafficDistributions trafficDistributions;
+  std::vector<mantle_api::RoadRange> trafficArea;
+};
+
+class TrafficAreaAction : public Action<TrafficAreaAction>
+{
+public:
+  using Action<TrafficAreaAction>::Action;
+
+  bool Step();
+
+private:
+  void UpdateTrafficAreas();
+
+  std::vector<mantle_api::TrafficArea> trafficAreas;
+
+  /// The accumulated size of all traffic areas up to and including the index of the respective element.
+  /// Does not account for overlapping areas.
+  std::vector<units::length::meter_t> lengths;
+};
+
+namespace Node
+{
+class TrafficAreaAction;
+SET_NODE_IDENTIFIER(TrafficAreaAction)
+SET_NODE_IMPLEMENTATION(TrafficAreaAction, OpenScenarioEngine::v1_3::TrafficAreaAction)
+SET_NODE_INTERFACE(TrafficAreaAction, NET_ASAM_OPENSCENARIO::v1_3::ITrafficAreaAction)
+
+class TrafficAreaAction : public ActionNode<TrafficAreaAction>
+{
+public:
+  using ActionNode<TrafficAreaAction>::ActionNode;
+
+  void onInit() override;
+
+private:
+  yase::NodeStatus tick() override;
+
+  void lookupAndRegisterData(yase::Blackboard& blackboard) final;
+};
+}  // namespace Node
+}  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/Common.h b/engine/src/Utils/Spawning/Common.h
new file mode 100644
index 00000000..16a5dac1
--- /dev/null
+++ b/engine/src/Utils/Spawning/Common.h
@@ -0,0 +1,45 @@
+/********************************************************************************
+ * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+#pragma once
+
+#include <units.h>
+
+#include <algorithm>
+#include <memory>
+
+namespace OpenScenarioEngine::v1_3
+{
+template <typename T>
+bool is_uninitialized(const std::weak_ptr<T>& pointer)
+{
+  return !pointer.owner_before(std::weak_ptr<T>{}) && !std::weak_ptr<T>{}.owner_before(pointer);
+}
+
+/// Functor returning the specified value when invoked.
+template <auto Value>
+struct Return
+{
+  template <typename... Input>
+  constexpr decltype(Value) operator()(Input&&...) const
+  {
+    return Value;
+  }
+};
+
+template <typename Type>
+std::pair<Type, size_t> Sample(const std::vector<Type>& range)
+{
+  const units::length::meter_t sample{range.back()};  // TODO: Use Stochastics
+  const auto upperBound{std::upper_bound(range.begin(), std::prev(range.end()), sample)};
+  const auto index{static_cast<size_t>(std::distance(range.begin(), upperBound))};
+  return {sample, index};
+}
+}  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/Interval.cpp b/engine/src/Utils/Spawning/Interval.cpp
new file mode 100644
index 00000000..847e7ad3
--- /dev/null
+++ b/engine/src/Utils/Spawning/Interval.cpp
@@ -0,0 +1,27 @@
+/********************************************************************************
+ * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+#include "Interval.h"
+
+#include <algorithm>
+
+#include "Common.h"
+
+namespace OpenScenarioEngine::v1_3
+{
+void OffsetIntervals(std::vector<Interval>& intervals, units::length::meter_t frontOffset, units::length::meter_t rearOffset)
+{
+  const auto shrinkage{frontOffset - rearOffset};
+  intervals.erase(std::remove_if(intervals.begin(), intervals.end(), [shrinkage](const Interval& interval)
+                                 { return interval.GetLength() < shrinkage; }),
+                  intervals.end());
+  std::for_each(intervals.begin(), intervals.end(), OffsetBy{frontOffset, rearOffset});
+}
+}  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/Interval.h b/engine/src/Utils/Spawning/Interval.h
new file mode 100644
index 00000000..755fb9d4
--- /dev/null
+++ b/engine/src/Utils/Spawning/Interval.h
@@ -0,0 +1,112 @@
+/********************************************************************************
+ * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+#pragma once
+
+#include <MantleAPI/Traffic/i_traffic_area_stream.h>
+#include <units.h>
+
+#include <vector>
+
+#include "Common.h"
+
+namespace OpenScenarioEngine::v1_3
+{
+struct Interval
+{
+  constexpr Interval(units::length::meter_t min, units::length::meter_t max)
+      : min{min}, max{max} {}
+
+  constexpr units::length::meter_t GetLength() const;
+
+  constexpr void OffsetBy(units::length::meter_t frontOffset, units::length::meter_t rearOffset);
+
+  units::length::meter_t min, max;
+};
+
+struct OffsetBy
+{
+  constexpr void operator()(Interval&) const;
+
+  units::length::meter_t frontOffset, rearOffset;
+};
+
+void OffsetIntervals(std::vector<Interval>& intervals, units::length::meter_t frontOffset, units::length::meter_t rearOffset);
+
+/// Returns all pairs of stream distances between which no entity is placed on the given stream
+std::vector<Interval> GetFreeIntervals(const mantle_api::ITrafficAreaStream&);
+}  // namespace OpenScenarioEngine::v1_3
+
+namespace OpenScenarioEngine::v1_3
+{
+constexpr units::length::meter_t Interval::GetLength() const
+{
+  return max - min;
+}
+
+constexpr void Interval::OffsetBy(units::length::meter_t frontOffset, units::length::meter_t rearOffset)
+{
+  min += frontOffset;
+  max += rearOffset;
+}
+
+constexpr void OffsetBy::operator()(Interval& interval) const
+{
+  interval.OffsetBy(frontOffset, rearOffset);
+}
+
+template <typename Filter = Return<true>>
+void EmplaceInterval(std::vector<Interval>& output, Interval&& input, Filter&& filter = {})
+{
+  if constexpr (std::is_same_v<Filter, Return<true>>)
+  {
+    output.push_back(std::move(input));
+  }
+  else
+  {
+    if (filter(input))
+    {
+      output.push_back(std::move(input));
+    }
+  }
+}
+
+template <typename Filter>
+std::vector<Interval> GetFreeIntervals(const mantle_api::ITrafficAreaStream& stream, Filter&& filter)
+{
+  // TODO: The interface of the ITrafficAreaStream does not provide an efficient way to compute free intervals. One must retrieve
+  // all entities on the stream, meaning the implementation must have computed their local positions, but they are not available
+  // through the interface. ITrafficAreaStream should be expanded to provide this information without redundant computation.
+
+  std::vector<Interval> result;
+  units::length::meter_t min;
+  const units::length::meter_t max{stream.GetLength()};
+  while (min < max)
+  {
+    std::weak_ptr<mantle_api::IEntity> pointer{stream.GetEntity(mantle_api::ITrafficAreaStream::SearchDirection::kDownstream, min, max - min)};
+    if (is_uninitialized(pointer))
+    {
+      EmplaceInterval(result, {min, max}, filter);
+      break;
+    }
+    auto entity{pointer.lock()};
+    const auto length{entity->GetProperties()->bounding_box.dimension.length};
+    const auto offset{entity->GetProperties()->bounding_box.geometric_center.x};
+    auto [s, t]{stream.Convert(entity->GetPosition()).value()};
+    if (const auto entityStartDistance{s + offset - length / units::dimensionless::scalar_t{2.0}}; entityStartDistance > min)
+    {
+      EmplaceInterval(result, {min, entityStartDistance}, filter);
+    }
+    min = s + offset + length / units::dimensionless::scalar_t{2.0};
+    // TODO: This would loop endlessly if the object has a length of 0 and MIGHT loop endlessly if its offset lies outside of its bounds.
+  }
+  return result;
+}
+}  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/Iterable.h b/engine/src/Utils/Spawning/Iterable.h
new file mode 100644
index 00000000..1801f311
--- /dev/null
+++ b/engine/src/Utils/Spawning/Iterable.h
@@ -0,0 +1,131 @@
+/********************************************************************************
+ * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+#pragma once
+
+#include <cstdint>
+
+namespace OpenScenarioEngine::v1_3
+{
+template <typename Container>
+struct Iterable
+{
+  using const_iterator = typename Container::const_iterator;
+  using iterator = typename Container::iterator;
+  using const_reference = typename Container::const_reference;
+  using reference = typename Container::reference;
+  using value_type = typename Container::value_type;
+
+  constexpr Iterable(Container&&);
+
+  constexpr const Container& GetContainer() const;
+  constexpr Container& GetContainer();
+
+  constexpr operator const Container&() const;
+  constexpr operator Container&();
+
+  constexpr const_iterator begin() const;
+  constexpr iterator begin();
+
+  constexpr const_iterator end() const;
+  constexpr iterator end();
+
+  constexpr size_t size() const;
+  constexpr bool empty() const;
+
+  const_reference operator[](size_t) const;
+  reference operator[](size_t);
+
+protected:
+  Container container;
+};
+}  // namespace OpenScenarioEngine::v1_3
+
+namespace OpenScenarioEngine::v1_3
+{
+
+template <typename Container>
+constexpr Iterable<Container>::Iterable(Container&& input)
+    : container{std::forward<Container>(input)}
+{
+}
+
+template <typename Container>
+constexpr const Container& Iterable<Container>::GetContainer() const
+{
+  return (container);
+}
+
+template <typename Container>
+constexpr Container& Iterable<Container>::GetContainer()
+{
+  return (container);
+}
+
+template <typename Container>
+constexpr Iterable<Container>::operator const Container&() const
+{
+  return GetContainer();
+}
+
+template <typename Container>
+constexpr Iterable<Container>::operator Container&()
+{
+  return GetContainer();
+}
+
+template <typename Container>
+constexpr typename Container::const_iterator Iterable<Container>::begin() const
+{
+  return container.begin();
+}
+
+template <typename Container>
+constexpr typename Container::iterator Iterable<Container>::begin()
+{
+  return container.begin();
+}
+
+template <typename Container>
+constexpr typename Container::const_iterator Iterable<Container>::end() const
+{
+  return container.end();
+}
+
+template <typename Container>
+constexpr typename Container::iterator Iterable<Container>::end()
+{
+  return container.end();
+}
+
+template <typename Container>
+constexpr size_t Iterable<Container>::size() const
+{
+  return container.size();
+}
+
+template <typename Container>
+constexpr bool Iterable<Container>::empty() const
+{
+  return container.empty();
+}
+
+template <typename Container>
+typename Container::const_reference Iterable<Container>::operator[](size_t i) const
+{
+  return container[i];
+}
+
+template <typename Container>
+typename Container::reference Iterable<Container>::operator[](size_t i)
+{
+  return container[i];
+}
+}  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/Length.cpp b/engine/src/Utils/Spawning/Length.cpp
new file mode 100644
index 00000000..67c9aa5e
--- /dev/null
+++ b/engine/src/Utils/Spawning/Length.cpp
@@ -0,0 +1,52 @@
+/********************************************************************************
+ * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+#include "Length.h"
+
+#include <cassert>
+#include <numeric>
+
+namespace OpenScenarioEngine::v1_3
+{
+units::length::meter_t ToLength::operator()(const mantle_api::ITrafficAreaStream& stream) const
+{
+  return stream.GetLength();
+}
+
+units::length::meter_t ToLength::operator()(const mantle_api::TrafficArea& area) const
+{
+  return std::transform_reduce(area.begin(), area.end(), units::length::meter_t{}, std::plus<>{}, *this);
+}
+
+units::length::meter_t ToLength::operator()(const SpawnZone& zone) const
+{
+  assert(!zone.lengths.empty());
+  return zone.lengths.back();
+}
+
+units::length::meter_t ToLength::operator()(const SpawnZones& zones) const
+{
+  assert(!zones.lengths.empty());
+  return zones.lengths.back();
+}
+
+units::length::meter_t ToLength::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IEntityObject& object) const
+{
+  if (decltype(auto) vehicle{object.GetVehicle()}; vehicle != nullptr)
+  {
+    return units::length::meter_t{vehicle->GetBoundingBox()->GetDimensions()->GetLength()};
+  }
+  if (decltype(auto) pedestrian{object.GetPedestrian()}; pedestrian != nullptr)
+  {
+    return units::length::meter_t{pedestrian->GetBoundingBox()->GetDimensions()->GetLength()};
+  }
+  return units::length::meter_t{std::numeric_limits<double>::quiet_NaN()};
+}
+}  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/Length.h b/engine/src/Utils/Spawning/Length.h
new file mode 100644
index 00000000..2839ab18
--- /dev/null
+++ b/engine/src/Utils/Spawning/Length.h
@@ -0,0 +1,96 @@
+/********************************************************************************
+ * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+#pragma once
+
+#include <MantleAPI/Traffic/i_traffic_area_service.h>
+#include <MantleAPI/Traffic/i_traffic_area_stream.h>
+#include <openScenarioLib/generated/v1_3/api/ApiClassInterfacesV1_3.h>
+#include <units.h>
+
+#include <memory>
+
+#include "Interval.h"
+#include "SpawnZone.h"
+#include "Transform.h"
+
+namespace OpenScenarioEngine::v1_3
+{
+struct ToLength
+{
+  constexpr units::length::meter_t operator()(const Interval&) const;
+
+  units::length::meter_t operator()(const mantle_api::ITrafficAreaStream&) const;
+
+  template <typename Type>
+  units::length::meter_t operator()(const std::shared_ptr<Type>&) const;
+
+  units::length::meter_t operator()(const mantle_api::TrafficArea&) const;
+
+  units::length::meter_t operator()(const SpawnZone&) const;
+
+  units::length::meter_t operator()(const SpawnZones&) const;
+
+  units::length::meter_t operator()(const NET_ASAM_OPENSCENARIO::v1_3::IEntityObject&) const;
+};
+
+template <typename Type>
+units::length::meter_t GetLength(const Type&);
+
+struct ToAccumulatedLength
+{
+  template <typename Type>
+  units::length::meter_t operator()(const Type&);
+
+  units::length::meter_t totalLength;
+};
+
+struct ToAccumulatedWeight
+{
+  template <typename Entry>
+  units::dimensionless::scalar_t operator()(const Entry&);
+
+  units::dimensionless::scalar_t totalWeight;
+};
+}  // namespace OpenScenarioEngine::v1_3
+
+namespace OpenScenarioEngine::v1_3
+{
+constexpr units::length::meter_t ToLength::operator()(const Interval& interval) const
+{
+  return interval.GetLength();
+}
+
+template <typename Type>
+units::length::meter_t ToLength::operator()(const std::shared_ptr<Type>& input) const
+{
+  return (*this)(*input);
+}
+
+template <typename Type>
+units::length::meter_t GetLength(const Type& item)
+{
+  return ToLength{}(item);
+}
+
+template <typename Type>
+units::length::meter_t ToAccumulatedLength::operator()(const Type& input)
+{
+  totalLength += GetLength(input);
+  return totalLength;
+}
+
+template <typename Entry>
+units::dimensionless::scalar_t ToAccumulatedWeight::operator()(const Entry& entry)
+{
+  totalWeight += units::dimensionless::scalar_t{entry.GetWeight()};
+  return totalWeight;
+}
+}  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/SpawnZone.cpp b/engine/src/Utils/Spawning/SpawnZone.cpp
new file mode 100644
index 00000000..88a4e93e
--- /dev/null
+++ b/engine/src/Utils/Spawning/SpawnZone.cpp
@@ -0,0 +1,104 @@
+/********************************************************************************
+ * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+#include "SpawnZone.h"
+
+#include <algorithm>
+#include <cassert>
+
+#include "Length.h"
+
+namespace OpenScenarioEngine::v1_3
+{
+SpawnZone::SpawnZone(std::vector<Interval> input)
+    : Iterable<std::vector<Interval>>{std::move(input)}, lengths{units::length::meter_t{}}
+{
+  Transform(GetContainer(), lengths, ToAccumulatedLength{});
+}
+
+void SpawnZone::UpdateIndexToLongest()
+{
+  indexToLongest = std::distance(begin(), std::max_element(begin(), end(), [](const auto& a, const auto& b)
+                                                           { return a.GetLength() < b.GetLength(); }));
+}
+
+void SpawnZone::RemoveInterval(Interval interval)
+{
+  auto candidate{std::upper_bound(begin(), end(), interval.max, [](units::length::meter_t threshold, const Interval& entry)
+                                  { return threshold <= entry.max; })};
+  assert(candidate != end());
+  assert(candidate->min <= interval.min);
+  assert(candidate->max >= interval.max);
+  const auto offset{std::distance(begin(), candidate)};
+  if (candidate->min == interval.min)
+  {
+    if (candidate->max == interval.max)
+    {
+      lengths.erase(std::next(lengths.begin(), offset + 1));
+      GetContainer().erase(candidate);
+      if (indexToLongest == offset)
+      {
+        UpdateIndexToLongest();
+      }
+      else if (indexToLongest > offset + 1)
+      {
+        --indexToLongest;
+      }
+    }
+    else
+    {
+      candidate->min = interval.max;
+      if (indexToLongest == offset)
+      {
+        UpdateIndexToLongest();
+      }
+    }
+    std::transform(std::next(begin(), offset),
+                   end(),
+                   std::next(lengths.begin(), offset),
+                   ToAccumulatedLength{offset ? *std::next(lengths.begin(), offset - 1) : units::length::meter_t{}});
+    return;
+  }
+  else if (candidate->max == interval.max)
+  {
+    candidate->max = interval.min;
+    if (indexToLongest == offset)
+    {
+      indexToLongest = std::distance(lengths.begin(), std::max_element(lengths.begin(), lengths.end()));
+    }
+    std::transform(std::next(begin(), offset),
+                   end(),
+                   std::next(lengths.begin(), offset),
+                   ToAccumulatedLength{offset ? *std::next(lengths.begin(), offset - 1) : units::length::meter_t{}});
+    return;
+  }
+  GetContainer().emplace(std::next(candidate), interval.max, candidate->max);
+  lengths.push_back(units::length::meter_t{});
+  std::next(begin(), offset)->max = interval.min;
+  std::transform(std::next(begin(), offset),
+                 end(),
+                 std::next(lengths.begin(), offset),
+                 ToAccumulatedLength{offset ? *std::next(lengths.begin(), offset - 1) : units::length::meter_t{}});
+}
+
+SpawnZones::SpawnZones(std::vector<SpawnZone> input)
+    : Iterable<std::vector<SpawnZone>>{std::move(input)}, lengths{units::length::meter_t{}}
+{
+  Transform(GetContainer(), lengths, ToAccumulatedLength{});
+}
+}  // namespace OpenScenarioEngine::v1_3
+
+namespace OpenScenarioEngine::v1_3
+{
+SpawnZones ToSpawnZones::operator()(const mantle_api::TrafficArea& area) const
+{
+  return {area};
+}
+}  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/SpawnZone.h b/engine/src/Utils/Spawning/SpawnZone.h
new file mode 100644
index 00000000..92389b28
--- /dev/null
+++ b/engine/src/Utils/Spawning/SpawnZone.h
@@ -0,0 +1,97 @@
+/********************************************************************************
+ * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+#pragma once
+
+#include <MantleAPI/Traffic/i_traffic_area_service.h>
+#include <MantleAPI/Traffic/i_traffic_area_stream.h>
+#include <units.h>
+
+#include <vector>
+
+#include "Common.h"
+#include "Interval.h"
+#include "Iterable.h"
+
+namespace OpenScenarioEngine::v1_3
+{
+struct SpawnZone : Iterable<std::vector<Interval>>
+{
+  SpawnZone(std::vector<Interval>);
+
+  template <typename Filter = Return<true>>
+  SpawnZone(const mantle_api::ITrafficAreaStream&, Filter&&);
+
+  /// Removes the given interval from this spawn zone and updates the interval lengths and index to the longest interval if necessary
+  ///
+  /// \param Interval Interval guaranteed to be a subset of an interval of this zone
+  void RemoveInterval(Interval);
+
+  void UpdateIndexToLongest();
+
+  std::vector<units::length::meter_t> lengths;
+  std::ptrdiff_t indexToLongest;
+};
+
+template <typename Filter = Return<true>>
+struct ToSpawnZone
+{
+  SpawnZone operator()(const mantle_api::ITrafficAreaStream&) const;
+
+  SpawnZone operator()(const std::shared_ptr<mantle_api::ITrafficAreaStream>&) const;
+
+  Filter filter;
+};
+
+template <typename Filter>
+ToSpawnZone(Filter&&) -> ToSpawnZone<Filter>;
+
+struct SpawnZones : Iterable<std::vector<SpawnZone>>
+{
+  SpawnZones(std::vector<SpawnZone>);
+
+  template <typename Filter = Return<true>>
+  SpawnZones(const mantle_api::TrafficArea&, Filter&& = {});
+
+  std::vector<units::length::meter_t> lengths;
+};
+
+struct ToSpawnZones
+{
+  SpawnZones operator()(const mantle_api::TrafficArea&) const;
+};
+}  // namespace OpenScenarioEngine::v1_3
+
+namespace OpenScenarioEngine::v1_3
+{
+template <typename Filter>
+SpawnZone ToSpawnZone<Filter>::operator()(const mantle_api::ITrafficAreaStream& stream) const
+{
+  return {stream, filter};
+}
+
+template <typename Filter>
+SpawnZone ToSpawnZone<Filter>::operator()(const std::shared_ptr<mantle_api::ITrafficAreaStream>& stream) const
+{
+  return (*this)(*stream);
+}
+
+template <typename Filter>
+SpawnZone::SpawnZone(const mantle_api::ITrafficAreaStream& stream, Filter&& filter)
+    : SpawnZone{GetFreeIntervals(stream, std::forward<Filter>(filter))}
+{
+}
+
+template <typename Filter>
+SpawnZones::SpawnZones(const mantle_api::TrafficArea& area, Filter&& filter)
+    : SpawnZones{Transform(area, ToSpawnZone{std::forward<Filter>(filter)})}
+{
+}
+}  // namespace OpenScenarioEngine::v1_3
\ No newline at end of file
diff --git a/engine/src/Utils/Spawning/Transform.h b/engine/src/Utils/Spawning/Transform.h
new file mode 100644
index 00000000..072c6733
--- /dev/null
+++ b/engine/src/Utils/Spawning/Transform.h
@@ -0,0 +1,49 @@
+/********************************************************************************
+ * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+#pragma once
+
+#include <algorithm>
+#include <memory>
+#include <type_traits>
+#include <vector>
+
+namespace OpenScenarioEngine::v1_3
+{
+template <typename Invocable, typename Type>
+std::vector<std::invoke_result_t<Invocable, const Type&>> Transform(const std::vector<Type>& input, Invocable&& invocable)
+{
+  std::vector<std::invoke_result_t<Invocable, const Type&>> result;
+  result.reserve(input.size());
+  std::transform(input.begin(), input.end(), std::back_inserter(result), std::forward<Invocable>(invocable));
+  return result;
+}
+
+template <typename Invocable, typename Type>
+void Transform(const std::vector<Type>& input, std::vector<std::invoke_result_t<Invocable, const Type&>>& output, Invocable&& invocable)
+{
+  output.reserve(output.size() + input.size());
+  std::transform(input.begin(), input.end(), std::back_inserter(output), std::forward<Invocable>(invocable));
+}
+
+template <typename Type, typename Invocable>
+std::vector<std::invoke_result_t<Invocable, const Type&>> TransformSharedPointers(
+    const std::vector<std::shared_ptr<Type>>& items, Invocable&& invocable)
+{
+  std::vector<std::invoke_result_t<Invocable, const Type&>> result;
+  result.reserve(items.size());
+  std::transform(items.begin(),
+                 items.end(),
+                 std::back_inserter(result),
+                 [&](const std::shared_ptr<Type>& item)
+                 { return invocable(*item); });
+  return result;
+}
+}  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/tests/Storyboard/GenericAction/ActivateControllerActionTest.cpp b/engine/tests/Storyboard/GenericAction/ActivateControllerActionTest.cpp
index d8f36438..389313cc 100644
--- a/engine/tests/Storyboard/GenericAction/ActivateControllerActionTest.cpp
+++ b/engine/tests/Storyboard/GenericAction/ActivateControllerActionTest.cpp
@@ -14,7 +14,7 @@
 #include <gtest/gtest.h>
 
 #include "MantleAPI/Common/i_identifiable.h"
-#include "Storyboard/GenericAction/ActivateControllerAction_impl.h"
+#include "Storyboard/GenericAction/ActivateControllerAction.h"
 #include "TestUtils/MockControllerService.h"
 #include "gmock/gmock.h"
 
-- 
GitLab


From d54bd0c352e40c147b22d5f407c7da9c6f497b2c Mon Sep 17 00:00:00 2001
From: Noah Schick <noah.schick@in-tech.com>
Date: Mon, 23 Dec 2024 17:38:47 +0100
Subject: [PATCH 05/26] Simplify TrafficAreaAction::Step

---
 .../ConvertScenarioTrafficDistribution.h      |  4 +-
 .../GenericAction/TrafficAreaAction.cpp       | 38 ++++---------------
 engine/src/Utils/Spawning/Iterable.h          | 37 ++++++++++++++++--
 3 files changed, 42 insertions(+), 37 deletions(-)

diff --git a/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.h b/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.h
index ae7421b7..50e4160a 100644
--- a/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.h
+++ b/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.h
@@ -16,6 +16,7 @@
 #include <memory>
 #include <vector>
 
+#include "Utils/Spawning/Iterable.h"
 #include "Utils/Spawning/Length.h"
 
 namespace OpenScenarioEngine::v1_3
@@ -26,9 +27,8 @@ struct TrafficDistribution
   units::dimensionless::scalar_t weight;
 };
 
-struct TrafficDistributions
+struct TrafficDistributions : Iterable<std::vector<TrafficDistribution>>
 {
-  std::vector<TrafficDistribution> distributions;
   std::vector<units::dimensionless::scalar_t> weights;
 };
 
diff --git a/engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp b/engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp
index 1733fd88..f7b4118e 100644
--- a/engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp
+++ b/engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp
@@ -48,7 +48,7 @@ bool TrafficAreaAction::Step()
   {
     return true;
   }
-  if (values.trafficDistributions.distributions.empty())  // No spawnable entities
+  if (values.trafficDistributions.empty())  // No spawnable entities
   {
     return false;
   }
@@ -62,38 +62,17 @@ bool TrafficAreaAction::Step()
   }
   EntityCreator spawner{mantle.environment};
 
-  if (values.trafficDistributions.distributions.size() == 1)
+  // SpawnSpace spawnSpace{values.trafficDistrbutions, trafficAreas};
+  while (values.numberOfEntities)
   {
-    auto& distributions{values.trafficDistributions.distributions.front()};
-    if (distributions.entities.size() == 1)  // Skip sampling entities if there is only one
+    if (/*!spawnSpace.Spawn()*/)
     {
-      if (trafficAreas.size() == 1)
-      {
-        SpawnZones zones{trafficAreas.front(), [threshold = GetLength(distributions.entities.front())](const Interval& interval)
-                         {
-                           return interval.GetLength() >= threshold;
-                         }};
-        while (values.numberOfEntities)
-        {
-          if (zones.empty())
-          {  // No room left
-            return false;
-          }
-        }
-      }
+      return false;
     }
+    --values.numberOfEntities;
   }
+  return true;
 
-  if (trafficAreas.size() == 1)  // Skip sampling areas if there is only one
-  {
-    auto& area{trafficAreas.front()};
-    // TODO: Not yet implemented
-    while (values.numberOfEntities)
-    {
-      --values.numberOfEntities;
-    }
-    return true;
-  }
   std::vector<SpawnZones> spawnZones{Transform(trafficAreas, ToSpawnZones{})};
   std::vector<units::length::meter_t> weights{Transform(spawnZones, ToAccumulatedLength{})};
   while (values.numberOfEntities)
@@ -114,9 +93,6 @@ bool TrafficAreaAction::Step()
   // spawner.CreateEntity(values.trafficDistribution.entityDistribution);
   // mantle.environment->GetEntityRepository().Create;
 
-  // Note:
-  // - Access to values parse to mantle/ose datatypes: this->values.xxx
-  // - Access to mantle interfaces: this->mantle.xxx
   return true;
 }
 
diff --git a/engine/src/Utils/Spawning/Iterable.h b/engine/src/Utils/Spawning/Iterable.h
index 1801f311..aadc9e88 100644
--- a/engine/src/Utils/Spawning/Iterable.h
+++ b/engine/src/Utils/Spawning/Iterable.h
@@ -40,8 +40,14 @@ struct Iterable
   constexpr size_t size() const;
   constexpr bool empty() const;
 
-  const_reference operator[](size_t) const;
-  reference operator[](size_t);
+  constexpr const_reference front() const;
+  constexpr reference front();
+
+  constexpr const_reference back() const;
+  constexpr reference back();
+
+  constexpr const_reference operator[](size_t) const;
+  constexpr reference operator[](size_t);
 
 protected:
   Container container;
@@ -118,13 +124,36 @@ constexpr bool Iterable<Container>::empty() const
 }
 
 template <typename Container>
-typename Container::const_reference Iterable<Container>::operator[](size_t i) const
+constexpr typename Container::const_reference Iterable<Container>::front() const
+{
+  return container.front();
+}
+template <typename Container>
+constexpr typename Container::reference Iterable<Container>::front()
+{
+  return container.front();
+}
+
+template <typename Container>
+constexpr typename Container::const_reference Iterable<Container>::back() const
+{
+  return container.back();
+}
+
+template <typename Container>
+constexpr typename Container::reference Iterable<Container>::back()
+{
+  return container.back();
+}
+
+template <typename Container>
+constexpr typename Container::const_reference Iterable<Container>::operator[](size_t i) const
 {
   return container[i];
 }
 
 template <typename Container>
-typename Container::reference Iterable<Container>::operator[](size_t i)
+constexpr typename Container::reference Iterable<Container>::operator[](size_t i)
 {
   return container[i];
 }
-- 
GitLab


From 928311ee1308c5bd6996e9418378c79e30d7ad7c Mon Sep 17 00:00:00 2001
From: Noah Schick <noah.schick@in-tech.com>
Date: Thu, 26 Dec 2024 12:57:08 +0100
Subject: [PATCH 06/26] Introduce SpawnSpace for TrafficAreaAction

---
 .../GenericAction/TrafficAreaAction.cpp       |  2 +-
 engine/src/Utils/Spawning/SpawnSpace.cpp      | 24 +++++++
 engine/src/Utils/Spawning/SpawnSpace.h        | 62 +++++++++++++++++++
 3 files changed, 87 insertions(+), 1 deletion(-)
 create mode 100644 engine/src/Utils/Spawning/SpawnSpace.cpp
 create mode 100644 engine/src/Utils/Spawning/SpawnSpace.h

diff --git a/engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp b/engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp
index f7b4118e..f68775c0 100644
--- a/engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp
+++ b/engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp
@@ -65,7 +65,7 @@ bool TrafficAreaAction::Step()
   // SpawnSpace spawnSpace{values.trafficDistrbutions, trafficAreas};
   while (values.numberOfEntities)
   {
-    if (/*!spawnSpace.Spawn()*/)
+    if (true /*!spawnSpace.Spawn(...)*/)
     {
       return false;
     }
diff --git a/engine/src/Utils/Spawning/SpawnSpace.cpp b/engine/src/Utils/Spawning/SpawnSpace.cpp
new file mode 100644
index 00000000..ca1490c4
--- /dev/null
+++ b/engine/src/Utils/Spawning/SpawnSpace.cpp
@@ -0,0 +1,24 @@
+/********************************************************************************
+ * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+#include "SpawnSpace.h"
+
+#include <cassert>
+
+namespace OpenScenarioEngine::v1_3
+{
+std::pair<SpawnZones &, units::length::meter_t> SpawnSpace::GetZonesAndOffset(units::length::meter_t value)
+{
+  assert(weights.size() >= 2);
+  const auto offset{std::prev(std::upper_bound(std::next(weights.begin()), std::prev(weights.end()), value))};
+  const auto index{std::distance(weights.begin(), offset)};
+  return {container[static_cast<size_t>(index)], *offset};
+}
+}  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/SpawnSpace.h b/engine/src/Utils/Spawning/SpawnSpace.h
new file mode 100644
index 00000000..88cec5d4
--- /dev/null
+++ b/engine/src/Utils/Spawning/SpawnSpace.h
@@ -0,0 +1,62 @@
+/********************************************************************************
+ * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+#pragma once
+
+#include <utility>
+#include <vector>
+
+#include "Conversion/OscToMantle/ConvertScenarioTrafficDistribution.h"
+#include "Iterable.h"
+#include "SpawnZone.h"
+
+namespace OpenScenarioEngine::v1_3
+{
+struct SpawnSpace : Iterable<std::vector<SpawnZones>>
+{
+  /// Tries to spawn an entity and returns whether spawning was successful.
+  /// If successful, also adjusts the weighting of the spawn space.
+  ///
+  /// \tparam EntityCreator Provides CreateEntity()
+  /// \tparam Sampler Invocable with signature double(double value) returning a number within [0, value)
+  template <typename EntityCreator, typename Sampler>
+  bool Spawn(EntityCreator &&, Sampler &&);
+
+  // Returns the spawn zones and weight offset containing the given sample
+  std::pair<SpawnZones &, units::length::meter_t> GetZonesAndOffset(units::length::meter_t);
+
+  TrafficDistributions distributions;
+  std::vector<units::length::meter_t> weights;
+};
+}  // namespace OpenScenarioEngine::v1_3
+
+namespace OpenScenarioEngine::v1_3
+{
+
+template <typename EntityCreator, typename Sampler>
+bool SpawnSpace::Spawn(EntityCreator &&spawner, Sampler &&sampler)
+{
+  if (empty() || distributions.empty())
+  {
+    return false;
+  }
+  if (distribution.size() == 1)
+  {
+    return Spawn(distributions.front(), std::forward<EntityCreator>(spawner), std::forward<Sample>(sampler));
+  }
+  if (size() == 1)
+  {
+    return front().Spawn(distributions, std::forward<EntityCreator>(spawner), std::forward<Sampler>(sampler));
+  }
+  const units::length::meter_t sample{sampler(weights.back())};
+  auto [zones, offset]{GetZonesAndOffset(sample)};
+  return zones.Spawn(sample - offset, distributions, std::forward<EntityCreator>(spawner), std::forward<Sampler>(sampler));
+}
+}  // namespace OpenScenarioEngine::v1_3
-- 
GitLab


From 0e861f2f46324084d875852a09121cf74a557329 Mon Sep 17 00:00:00 2001
From: Noah Schick <noah.schick@in-tech.com>
Date: Wed, 15 Jan 2025 20:50:18 +0100
Subject: [PATCH 07/26] Add stochastics & revamp spawning

---
 engine/CMakeLists.txt                         |  16 ++-
 engine/cmake/generated_files.cmake            |   4 +
 .../ConvertScenarioTrafficArea.cpp            |   2 +
 .../ConvertScenarioTrafficDistribution.cpp    |   1 +
 .../ConvertScenarioTrafficDistribution.h      |   6 +-
 .../GenericAction/TrafficAreaAction.cpp       |  32 ++----
 .../GenericAction/TrafficAreaAction.h         |   1 +
 engine/src/Utils/Spawning/Common.h            |  58 +++++++++-
 engine/src/Utils/Spawning/Interval.cpp        |  21 +++-
 engine/src/Utils/Spawning/Interval.h          |  50 +++++++-
 engine/src/Utils/Spawning/Iterable.h          |  83 +++++++++++---
 engine/src/Utils/Spawning/Length.cpp          |  11 +-
 engine/src/Utils/Spawning/Length.h            |  50 +++++++-
 engine/src/Utils/Spawning/LinkedInterval.cpp  |  34 ++++++
 engine/src/Utils/Spawning/LinkedInterval.h    |  36 ++++++
 engine/src/Utils/Spawning/Obstacle.cpp        |  54 +++++++++
 engine/src/Utils/Spawning/Obstacle.h          |  50 ++++++++
 engine/src/Utils/Spawning/SpawnSpace.cpp      |  81 ++++++++++++-
 engine/src/Utils/Spawning/SpawnSpace.h        | 107 ++++++++++++++----
 engine/src/Utils/Spawning/SpawnSpot.h         |  39 +++++++
 engine/src/Utils/Spawning/SpawnZone.cpp       |  82 +++-----------
 engine/src/Utils/Spawning/SpawnZone.h         |  79 ++++++-------
 engine/src/Utils/Spawning/VelocityRange.cpp   |  54 +++++++++
 engine/src/Utils/Spawning/VelocityRange.h     |  78 +++++++++++++
 engine/src/Utils/Spawning/Weight.h            |  92 +++++++++++++++
 25 files changed, 919 insertions(+), 202 deletions(-)
 create mode 100644 engine/src/Utils/Spawning/LinkedInterval.cpp
 create mode 100644 engine/src/Utils/Spawning/LinkedInterval.h
 create mode 100644 engine/src/Utils/Spawning/Obstacle.cpp
 create mode 100644 engine/src/Utils/Spawning/Obstacle.h
 create mode 100644 engine/src/Utils/Spawning/SpawnSpot.h
 create mode 100644 engine/src/Utils/Spawning/VelocityRange.cpp
 create mode 100644 engine/src/Utils/Spawning/VelocityRange.h
 create mode 100644 engine/src/Utils/Spawning/Weight.h

diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt
index 2e937ef7..fed70383 100644
--- a/engine/CMakeLists.txt
+++ b/engine/CMakeLists.txt
@@ -55,6 +55,12 @@ target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_17)
 find_package(OpenScenarioAPI REQUIRED MODULE)
 find_package(Antlr4Runtime REQUIRED MODULE)
 
+include(FetchContent)
+FetchContent_Declare(Stochastics
+ URL https://gitlab.eclipse.org/eclipse/openpass/stochastics-library/-/archive/main/stochastics-library-main.zip
+ EXCLUDE_FROM_ALL)
+FetchContent_MakeAvailable(Stochastics)
+
 include(cmake/generated_files.cmake)
 
 target_sources(
@@ -70,12 +76,12 @@ target_include_directories(
             $<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}/include>
 )
 
-target_link_libraries(${PROJECT_NAME} PUBLIC openscenario_api::shared
-                                             antlr4_runtime::shared
-                                             Yase::agnostic_behavior_tree
-                                             units::units
+target_link_libraries(${PROJECT_NAME} PUBLIC antlr4_runtime::shared 
                                              MantleAPI::MantleAPI
-                                      PRIVATE Stochastics::Stochastics)
+                                             openscenario_api::shared 
+                                             units::units 
+                                             Yase::agnostic_behavior_tree
+                                      PRIVATE Stochastics)
 
 if(USE_CCACHE)
   find_program(CCACHE_FOUND ccache)
diff --git a/engine/cmake/generated_files.cmake b/engine/cmake/generated_files.cmake
index 7a4e54a1..42b52e31 100644
--- a/engine/cmake/generated_files.cmake
+++ b/engine/cmake/generated_files.cmake
@@ -234,7 +234,11 @@ list(APPEND ${PROJECT_NAME}_SOURCES
     src/Storyboard/MotionControlAction/SpeedAction_impl.cpp
     src/Utils/Spawning/Interval.cpp
     src/Utils/Spawning/Length.cpp
+    src/Utils/Spawning/LinkedInterval.cpp
+    src/Utils/Spawning/Obstacle.cpp
+    src/Utils/Spawning/SpawnSpace.cpp
     src/Utils/Spawning/SpawnZone.cpp
+    src/Utils/Spawning/VelocityRange.cpp
     src/Utils/ConditionEdgeEvaluator.cpp
     src/Utils/ControllerCreator.cpp
     src/Utils/ControllerService.cpp
diff --git a/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficArea.cpp b/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficArea.cpp
index 9828f445..6a22f0ef 100644
--- a/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficArea.cpp
+++ b/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficArea.cpp
@@ -35,6 +35,8 @@ struct ToRoadRange
 {
   mantle_api::RoadRange operator()(const NET_ASAM_OPENSCENARIO::v1_3::IRoadRange& range) const
   {
+    // An IRoadRange always has a length, but a mantle_api::RoadRange can have a std::nullopt length. Thus, if
+    // the IRoadRange has a length 0 despite having more than one cursor, treat that length as representing std::nullopt:
     return {(range.GetRoadCursorSize() > 1 && range.GetLength() == .0) ? std::nullopt : std::optional<units::length::meter_t>{range.GetLength()},
             TransformSharedPointers(range.GetRoadCursor(), ToRoadCursor{})};
   }
diff --git a/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.cpp b/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.cpp
index 04461351..ae0eb73c 100644
--- a/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.cpp
+++ b/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.cpp
@@ -10,6 +10,7 @@
 
 #include "ConvertScenarioTrafficDistribution.h"
 
+#include "Utils/Spawning/Length.h"
 #include "Utils/Spawning/Transform.h"
 
 namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.h b/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.h
index 50e4160a..5442081e 100644
--- a/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.h
+++ b/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.h
@@ -1,5 +1,5 @@
 /********************************************************************************
- * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ * Copyright (c) 2024-2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License 2.0 which is available at
@@ -17,13 +17,11 @@
 #include <vector>
 
 #include "Utils/Spawning/Iterable.h"
-#include "Utils/Spawning/Length.h"
 
 namespace OpenScenarioEngine::v1_3
 {
-struct TrafficDistribution
+struct TrafficDistribution : Iterable<std::vector<std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IEntityObject>>>
 {
-  std::vector<std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IEntityObject>> entities;
   units::dimensionless::scalar_t weight;
 };
 
diff --git a/engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp b/engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp
index f68775c0..6777a273 100644
--- a/engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp
+++ b/engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp
@@ -10,10 +10,13 @@
 
 #include "Storyboard/GenericAction/TrafficAreaAction.h"
 
+#include <Stochastics/Stochastics.h>
+
 #include "Conversion/OscToMantle/ConvertScenarioTrafficArea.h"
 #include "Utils/EntityCreator.h"
 #include "Utils/Logger.h"
 #include "Utils/Spawning/Length.h"
+#include "Utils/Spawning/SpawnSpace.h"
 #include "Utils/Spawning/Transform.h"
 
 namespace OpenScenarioEngine::v1_3
@@ -61,39 +64,18 @@ bool TrafficAreaAction::Step()
     }
   }
   EntityCreator spawner{mantle.environment};
-
-  // SpawnSpace spawnSpace{values.trafficDistrbutions, trafficAreas};
+  std::vector<SpawnZones> spawnZones{Transform(trafficAreas, ToSpawnZones{})};
+  SpawnSpace spawnSpace{values.trafficDistributions, spawnZones};
+  Stochastics rng;
   while (values.numberOfEntities)
   {
-    if (true /*!spawnSpace.Spawn(...)*/)
+    if (!spawnSpace.Spawn(spawner, rng))
     {
       return false;
     }
     --values.numberOfEntities;
   }
   return true;
-
-  std::vector<SpawnZones> spawnZones{Transform(trafficAreas, ToSpawnZones{})};
-  std::vector<units::length::meter_t> weights{Transform(spawnZones, ToAccumulatedLength{})};
-  while (values.numberOfEntities)
-  {
-    auto [sampleA, indexA]{Sample(weights)};
-    auto sampleB{sampleA - weights[indexA]};
-    const SpawnZones& zones{spawnZones[indexA]};
-    const auto upperBound{std::prev(std::upper_bound(std::next(zones.lengths.begin()), std::prev(zones.lengths.end()), sampleB))};
-    const auto indexB{static_cast<size_t>(std::distance(zones.lengths.begin(), upperBound))};
-    const SpawnZone& zone{zones[indexB]};
-    auto sampleC{sampleB - zones.lengths[indexB]};
-
-    // TODO: WIP
-    // spawner.CreateEntity()
-
-    --values.numberOfEntities;
-  }
-  // spawner.CreateEntity(values.trafficDistribution.entityDistribution);
-  // mantle.environment->GetEntityRepository().Create;
-
-  return true;
 }
 
 namespace Node
diff --git a/engine/src/Storyboard/GenericAction/TrafficAreaAction.h b/engine/src/Storyboard/GenericAction/TrafficAreaAction.h
index ea72bbd6..1e63a341 100644
--- a/engine/src/Storyboard/GenericAction/TrafficAreaAction.h
+++ b/engine/src/Storyboard/GenericAction/TrafficAreaAction.h
@@ -12,6 +12,7 @@
 
 #include <MantleAPI/Execution/i_environment.h>
 #include <MantleAPI/Traffic/i_traffic_area_service.h>
+#include <Stochastics/StochasticsInterface.h>
 #include <agnostic_behavior_tree/action_node.h>
 #include <openScenarioLib/generated/v1_3/api/ApiClassInterfacesV1_3.h>
 #include <units.h>
diff --git a/engine/src/Utils/Spawning/Common.h b/engine/src/Utils/Spawning/Common.h
index 16a5dac1..3d16791a 100644
--- a/engine/src/Utils/Spawning/Common.h
+++ b/engine/src/Utils/Spawning/Common.h
@@ -13,7 +13,9 @@
 #include <units.h>
 
 #include <algorithm>
+#include <functional>
 #include <memory>
+#include <type_traits>
 
 namespace OpenScenarioEngine::v1_3
 {
@@ -34,12 +36,56 @@ struct Return
   }
 };
 
-template <typename Type>
-std::pair<Type, size_t> Sample(const std::vector<Type>& range)
+/// Functor returning the input as-is.
+struct Forward
 {
-  const units::length::meter_t sample{range.back()};  // TODO: Use Stochastics
-  const auto upperBound{std::upper_bound(range.begin(), std::prev(range.end()), sample)};
-  const auto index{static_cast<size_t>(std::distance(range.begin(), upperBound))};
-  return {sample, index};
+  template <typename Input>
+  constexpr decltype(auto) operator()(Input&& input) const
+  {
+    return std::forward<Input>(input);
+  }
+};
+
+template <typename Operation, typename Transformer = Forward>
+struct BinaryOperation
+{
+  using is_transparent = bool;
+
+  template <typename A, typename B>
+  constexpr decltype(auto) operator()(A&&, B&&) const;
+};
+
+template <typename Transform>
+using Less = BinaryOperation<std::less<>, Transform>;
+
+template <typename Transform>
+using Greater = BinaryOperation<std::greater<>, Transform>;
+
+template <typename Transform>
+using Add = BinaryOperation<std::plus<>, Transform>;
+
+template <typename Operation, typename Transformer>
+template <typename A, typename B>
+constexpr decltype(auto) BinaryOperation<Operation, Transformer>::operator()(A&& a, B&& b) const
+{
+  static_assert(std::is_invocable_v<Transformer, A&&> && std::is_invocable_v<Transformer, B&&>);
+  return Operation{}(Transformer{}(std::forward<A>(a)), Transformer{}(std::forward<B>(b)));
+}
+
+template <typename Elements, typename Thresholds>
+typename Elements::const_reference Sample(const Elements& elements, const Thresholds& thresholds, typename Thresholds::const_reference value)
+{
+  assert(elements.size() == thresholds.size());
+  const auto threshold{std::upper_bound(std::next(thresholds.begin()), thresholds.end(), value)};
+  const size_t index{static_cast<size_t>(std::distance(thresholds.begin(), threshold) - 1)};
+  return elements[index];
 }
+// template <typename Type>
+// std::pair<Type, size_t> Sample(const std::vector<Type>& range)
+// {
+//   const units::length::meter_t sample{range.back()};  // TODO: Use Stochastics
+//   const auto upperBound{std::upper_bound(range.begin(), std::prev(range.end()), sample)};
+//   const auto index{static_cast<size_t>(std::distance(range.begin(), upperBound))};
+//   return {sample, index};
+// }
 }  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/Interval.cpp b/engine/src/Utils/Spawning/Interval.cpp
index 847e7ad3..d35e5077 100644
--- a/engine/src/Utils/Spawning/Interval.cpp
+++ b/engine/src/Utils/Spawning/Interval.cpp
@@ -1,5 +1,5 @@
 /********************************************************************************
- * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ * Copyright (c) 2024-2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License 2.0 which is available at
@@ -11,6 +11,7 @@
 #include "Interval.h"
 
 #include <algorithm>
+#include <cassert>
 
 #include "Common.h"
 
@@ -24,4 +25,22 @@ void OffsetIntervals(std::vector<Interval>& intervals, units::length::meter_t fr
                   intervals.end());
   std::for_each(intervals.begin(), intervals.end(), OffsetBy{frontOffset, rearOffset});
 }
+
+std::vector<Interval> Interval::SplitBy(Interval input) const
+{
+  assert(min <= input.min && input.max <= max);
+  if (min < input.min)
+  {
+    if (input.max < max)
+    {
+      return {{min, input.min}, {input.max, max}};
+    }
+    return {{min, input.min}};
+  }
+  if (input.max < max)
+  {
+    return {{input.max, max}};
+  }
+  return {};
+}
 }  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/Interval.h b/engine/src/Utils/Spawning/Interval.h
index 755fb9d4..1f16640c 100644
--- a/engine/src/Utils/Spawning/Interval.h
+++ b/engine/src/Utils/Spawning/Interval.h
@@ -1,5 +1,5 @@
 /********************************************************************************
- * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ * Copyright (c) 2024-2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License 2.0 which is available at
@@ -13,6 +13,7 @@
 #include <MantleAPI/Traffic/i_traffic_area_stream.h>
 #include <units.h>
 
+#include <set>
 #include <vector>
 
 #include "Common.h"
@@ -24,10 +25,23 @@ struct Interval
   constexpr Interval(units::length::meter_t min, units::length::meter_t max)
       : min{min}, max{max} {}
 
+  /// Returns the length of this interval
+  ///
+  /// \return Difference between min and max
   constexpr units::length::meter_t GetLength() const;
 
+  /// Moves the min and max of this interval by the given offsets
+  ///
+  /// \param frontOffset The amount to be added to min
+  /// \param rearOffset The amount to be added to max
   constexpr void OffsetBy(units::length::meter_t frontOffset, units::length::meter_t rearOffset);
 
+  /// Returns the resulting list of intervals if the given interval were cut out of this interval.
+  ///
+  /// \param Interval A subrange of this interval
+  /// \return 0, 1 or 2 elements sorted by ascending (min/max) value.
+  std::vector<Interval> SplitBy(Interval) const;
+
   units::length::meter_t min, max;
 };
 
@@ -42,6 +56,20 @@ void OffsetIntervals(std::vector<Interval>& intervals, units::length::meter_t fr
 
 /// Returns all pairs of stream distances between which no entity is placed on the given stream
 std::vector<Interval> GetFreeIntervals(const mantle_api::ITrafficAreaStream&);
+
+struct ToMin
+{
+  constexpr units::length::meter_t operator()(units::length::meter_t) const;
+
+  constexpr units::length::meter_t operator()(const Interval&) const;
+};
+
+struct ToMax
+{
+  constexpr units::length::meter_t operator()(units::length::meter_t) const;
+
+  constexpr units::length::meter_t operator()(const Interval&) const;
+};
 }  // namespace OpenScenarioEngine::v1_3
 
 namespace OpenScenarioEngine::v1_3
@@ -109,4 +137,24 @@ std::vector<Interval> GetFreeIntervals(const mantle_api::ITrafficAreaStream& str
   }
   return result;
 }
+
+constexpr units::length::meter_t ToMin::operator()(units::length::meter_t value) const
+{
+  return value;
+}
+
+constexpr units::length::meter_t ToMin::operator()(const Interval& interval) const
+{
+  return interval.min;
+}
+
+constexpr units::length::meter_t ToMax::operator()(units::length::meter_t value) const
+{
+  return value;
+}
+
+constexpr units::length::meter_t ToMax::operator()(const Interval& interval) const
+{
+  return interval.max;
+}
 }  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/Iterable.h b/engine/src/Utils/Spawning/Iterable.h
index aadc9e88..00baa36f 100644
--- a/engine/src/Utils/Spawning/Iterable.h
+++ b/engine/src/Utils/Spawning/Iterable.h
@@ -14,11 +14,31 @@
 
 namespace OpenScenarioEngine::v1_3
 {
+namespace detail
+{
+template <typename Type>
+struct is_vector
+{
+  static constexpr bool value{false};
+};
+
+template <typename Type>
+struct is_vector<std::vector<Type>>
+{
+  static constexpr bool value{true};
+};
+}  // namespace detail
+
+template <typename Type>
+constexpr bool is_vector{detail::is_vector<Type>::value};
+
 template <typename Container>
 struct Iterable
 {
   using const_iterator = typename Container::const_iterator;
   using iterator = typename Container::iterator;
+  using const_reverse_iterator = typename Container::const_reverse_iterator;
+  using reverse_iterator = typename Container::reverse_iterator;
   using const_reference = typename Container::const_reference;
   using reference = typename Container::reference;
   using value_type = typename Container::value_type;
@@ -37,6 +57,12 @@ struct Iterable
   constexpr const_iterator end() const;
   constexpr iterator end();
 
+  constexpr const_reverse_iterator rbegin() const;
+  constexpr reverse_iterator rbegin();
+
+  constexpr const_reverse_iterator rend() const;
+  constexpr reverse_iterator rend();
+
   constexpr size_t size() const;
   constexpr bool empty() const;
 
@@ -46,8 +72,17 @@ struct Iterable
   constexpr const_reference back() const;
   constexpr reference back();
 
-  constexpr const_reference operator[](size_t) const;
-  constexpr reference operator[](size_t);
+  template <typename Type = Container, std::enable_if_t<is_vector<Type>, bool> = true>
+  constexpr const_reference operator[](size_t i) const
+  {
+    return container[i];
+  }
+
+  template <typename Type = Container, std::enable_if_t<is_vector<Type>, bool> = true>
+  constexpr reference operator[](size_t i)
+  {
+    return container[i];
+  }
 
 protected:
   Container container;
@@ -112,49 +147,61 @@ constexpr typename Container::iterator Iterable<Container>::end()
 }
 
 template <typename Container>
-constexpr size_t Iterable<Container>::size() const
+constexpr typename Container::const_reverse_iterator Iterable<Container>::rbegin() const
 {
-  return container.size();
+  return container.rbegin();
 }
 
 template <typename Container>
-constexpr bool Iterable<Container>::empty() const
+constexpr typename Container::reverse_iterator Iterable<Container>::rbegin()
 {
-  return container.empty();
+  return container.rbegin();
 }
 
 template <typename Container>
-constexpr typename Container::const_reference Iterable<Container>::front() const
+constexpr typename Container::const_reverse_iterator Iterable<Container>::rend() const
 {
-  return container.front();
+  return container.rend();
 }
+
 template <typename Container>
-constexpr typename Container::reference Iterable<Container>::front()
+constexpr typename Container::reverse_iterator Iterable<Container>::rend()
 {
-  return container.front();
+  return container.rend();
 }
 
 template <typename Container>
-constexpr typename Container::const_reference Iterable<Container>::back() const
+constexpr size_t Iterable<Container>::size() const
 {
-  return container.back();
+  return container.size();
 }
 
 template <typename Container>
-constexpr typename Container::reference Iterable<Container>::back()
+constexpr bool Iterable<Container>::empty() const
 {
-  return container.back();
+  return container.empty();
 }
 
 template <typename Container>
-constexpr typename Container::const_reference Iterable<Container>::operator[](size_t i) const
+constexpr typename Container::const_reference Iterable<Container>::front() const
 {
-  return container[i];
+  return *begin();
+}
+template <typename Container>
+constexpr typename Container::reference Iterable<Container>::front()
+{
+  return *begin();
+}
+
+template <typename Container>
+constexpr typename Container::const_reference Iterable<Container>::back() const
+{
+  return *rbegin();
 }
 
 template <typename Container>
-constexpr typename Container::reference Iterable<Container>::operator[](size_t i)
+constexpr typename Container::reference Iterable<Container>::back()
 {
-  return container[i];
+  return *rbegin();
 }
 }  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/Length.cpp b/engine/src/Utils/Spawning/Length.cpp
index 67c9aa5e..cd6310c9 100644
--- a/engine/src/Utils/Spawning/Length.cpp
+++ b/engine/src/Utils/Spawning/Length.cpp
@@ -27,14 +27,17 @@ units::length::meter_t ToLength::operator()(const mantle_api::TrafficArea& area)
 
 units::length::meter_t ToLength::operator()(const SpawnZone& zone) const
 {
-  assert(!zone.lengths.empty());
-  return zone.lengths.back();
+  return zone.GetLength() - std::transform_reduce(zone.obstacles.begin(), zone.obstacles.end(), units::length::meter_t{}, std::plus<>{}, *this);
 }
 
 units::length::meter_t ToLength::operator()(const SpawnZones& zones) const
 {
-  assert(!zones.lengths.empty());
-  return zones.lengths.back();
+  return std::transform_reduce(zones.begin(), zones.end(), units::length::meter_t{}, std::plus<>{}, *this);
+}
+
+units::length::meter_t ToLength::operator()(const LinkedInterval& interval) const
+{
+  return ToLength{}(*interval.interval);
 }
 
 units::length::meter_t ToLength::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IEntityObject& object) const
diff --git a/engine/src/Utils/Spawning/Length.h b/engine/src/Utils/Spawning/Length.h
index 2839ab18..01a96046 100644
--- a/engine/src/Utils/Spawning/Length.h
+++ b/engine/src/Utils/Spawning/Length.h
@@ -17,7 +17,9 @@
 
 #include <memory>
 
+#include "Conversion/OscToMantle/ConvertScenarioTrafficDistribution.h"
 #include "Interval.h"
+#include "LinkedInterval.h"
 #include "SpawnZone.h"
 #include "Transform.h"
 
@@ -25,13 +27,20 @@ namespace OpenScenarioEngine::v1_3
 {
 struct ToLength
 {
+  constexpr units::length::meter_t operator()(units::length::meter_t) const;
+
   constexpr units::length::meter_t operator()(const Interval&) const;
 
+  units::length::meter_t operator()(const LinkedInterval&) const;
+
   units::length::meter_t operator()(const mantle_api::ITrafficAreaStream&) const;
 
   template <typename Type>
   units::length::meter_t operator()(const std::shared_ptr<Type>&) const;
 
+  template <typename Type>
+  units::length::meter_t operator()(const Type*) const;
+
   units::length::meter_t operator()(const mantle_api::TrafficArea&) const;
 
   units::length::meter_t operator()(const SpawnZone&) const;
@@ -41,6 +50,17 @@ struct ToLength
   units::length::meter_t operator()(const NET_ASAM_OPENSCENARIO::v1_3::IEntityObject&) const;
 };
 
+struct ToSize
+{
+  constexpr size_t operator()(size_t) const;
+
+  template <typename Type>
+  constexpr size_t operator()(const Iterable<Type>&) const;
+
+  template <typename Type>
+  constexpr size_t operator()(const std::vector<Type>&) const;
+};
+
 template <typename Type>
 units::length::meter_t GetLength(const Type&);
 
@@ -63,6 +83,11 @@ struct ToAccumulatedWeight
 
 namespace OpenScenarioEngine::v1_3
 {
+constexpr units::length::meter_t ToLength::operator()(units::length::meter_t length) const
+{
+  return length;
+}
+
 constexpr units::length::meter_t ToLength::operator()(const Interval& interval) const
 {
   return interval.GetLength();
@@ -71,7 +96,30 @@ constexpr units::length::meter_t ToLength::operator()(const Interval& interval)
 template <typename Type>
 units::length::meter_t ToLength::operator()(const std::shared_ptr<Type>& input) const
 {
-  return (*this)(*input);
+  return ToLength{}(*input);
+}
+
+template <typename Type>
+units::length::meter_t ToLength::operator()(const Type* input) const
+{
+  return ToLength{}(*input);
+}
+
+constexpr size_t ToSize::operator()(size_t size) const
+{
+  return size;
+}
+
+template <typename Type>
+constexpr size_t ToSize::operator()(const Iterable<Type>& container) const
+{
+  return container.size();
+}
+
+template <typename Type>
+constexpr size_t ToSize::operator()(const std::vector<Type>& container) const
+{
+  return container.size();
 }
 
 template <typename Type>
diff --git a/engine/src/Utils/Spawning/LinkedInterval.cpp b/engine/src/Utils/Spawning/LinkedInterval.cpp
new file mode 100644
index 00000000..6556f22d
--- /dev/null
+++ b/engine/src/Utils/Spawning/LinkedInterval.cpp
@@ -0,0 +1,34 @@
+/********************************************************************************
+ * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+#include "LinkedInterval.h"
+
+#include "SpawnZone.h"
+
+namespace OpenScenarioEngine::v1_3
+{
+VelocityRange LinkedInterval::GetVelocityRange() const
+{
+  const size_t index{static_cast<size_t>(std::distance(zone->begin(), interval))};
+  return {zone->velocities[index], zone->velocities[index + 1]};
+}
+
+units::velocity::meters_per_second_t LinkedInterval::GetMinVelocity() const
+{
+  const size_t index{static_cast<size_t>(std::distance(zone->begin(), interval))};
+  return zone->velocities[index];
+}
+
+units::velocity::meters_per_second_t LinkedInterval::GetMaxVelocity() const
+{
+  const size_t index{static_cast<size_t>(std::distance(zone->begin(), interval))};
+  return zone->velocities[index + 1];
+}
+}  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/LinkedInterval.h b/engine/src/Utils/Spawning/LinkedInterval.h
new file mode 100644
index 00000000..cb3780ec
--- /dev/null
+++ b/engine/src/Utils/Spawning/LinkedInterval.h
@@ -0,0 +1,36 @@
+/********************************************************************************
+ * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+#pragma once
+
+#include <vector>
+
+#include "Interval.h"
+#include "VelocityRange.h"
+
+namespace OpenScenarioEngine::v1_3
+{
+struct SpawnZone;
+
+// A LinkedInterval holds an iterator to an Interval and a pointer to the container of that interval (its SpawnZone).
+// This is done so that there can be a list of all Intervals regardless of their SpawnZone while still being able to refer
+// back to the zone they are a part of. That way, an interval can be sampled with a fair distribution efficiently while
+// also reducing the amount of length recomputations needed when an interval is changed/removed. (See SpawnSpace)
+struct LinkedInterval
+{
+  units::velocity::meters_per_second_t GetMinVelocity() const;
+
+  units::velocity::meters_per_second_t GetMaxVelocity() const;
+
+  VelocityRange GetVelocityRange() const;
+
+  std::vector<Interval>::iterator interval;
+  SpawnZone* zone;
+};
+}  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/Obstacle.cpp b/engine/src/Utils/Spawning/Obstacle.cpp
new file mode 100644
index 00000000..fc2c97bd
--- /dev/null
+++ b/engine/src/Utils/Spawning/Obstacle.cpp
@@ -0,0 +1,54 @@
+/********************************************************************************
+ * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+#include "Obstacle.h"
+
+namespace OpenScenarioEngine::v1_3
+{
+Obstacle::Obstacle(Interval interval, const std::shared_ptr<mantle_api::IEntity>& entity, units::velocity::meters_per_second_t velocity)
+    : Interval{std::move(interval)}, entity{entity}, velocity{velocity} {}
+
+Obstacle Obstacle::GetStartingSentinel(units::length::meter_t value)
+{
+  return {{value, value}, nullptr, units::velocity::meters_per_second_t{std::numeric_limits<double>::lowest()}};
+}
+
+Obstacle Obstacle::GetEndingSentinel(units::length::meter_t value)
+{
+  return {{value, value}, nullptr, units::velocity::meters_per_second_t{std::numeric_limits<double>::max()}};
+}
+
+Obstacle ToObstacle(const std::shared_ptr<mantle_api::IEntity>& entity, const mantle_api::ITrafficAreaStream& stream)
+{
+  const auto halfLength{entity->GetProperties()->bounding_box.dimension.length / units::dimensionless::scalar_t{2.0}};
+  const auto offset{entity->GetProperties()->bounding_box.geometric_center.x};
+  auto pose{stream.Convert(mantle_api::Pose{entity->GetPosition(), {}}).value()};
+  const Interval interval{pose.s + offset - halfLength, pose.s + offset + halfLength};
+  const auto velocity{detail::rotate_2d(entity->GetVelocity(), pose.hdg).x};
+  return {interval, entity, velocity};
+}
+
+std::set<Obstacle, Less<ToMin>> GetObstacles(const mantle_api::ITrafficAreaStream& stream)
+{
+  std::set<Obstacle, Less<ToMin>> result;
+  units::length::meter_t min;
+  const units::length::meter_t max{stream.GetLength()};
+  while (min < max)
+  {
+    std::weak_ptr<mantle_api::IEntity> pointer{stream.GetEntity(mantle_api::ITrafficAreaStream::SearchDirection::kDownstream, min, max - min)};
+    if (is_uninitialized(pointer))
+    {
+      break;
+    }
+    min = result.emplace(ToObstacle(pointer.lock(), stream)).first->max;
+  }
+  return result;
+}
+}  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/Obstacle.h b/engine/src/Utils/Spawning/Obstacle.h
new file mode 100644
index 00000000..79dae2d1
--- /dev/null
+++ b/engine/src/Utils/Spawning/Obstacle.h
@@ -0,0 +1,50 @@
+/********************************************************************************
+ * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+#pragma once
+
+#include <memory>
+
+#include "Common.h"
+#include "Interval.h"
+
+namespace OpenScenarioEngine::v1_3
+{
+struct Obstacle : Interval
+{
+  Obstacle(Interval, const std::shared_ptr<mantle_api::IEntity>&, units::velocity::meters_per_second_t);
+
+  static Obstacle GetStartingSentinel(units::length::meter_t);
+
+  static Obstacle GetEndingSentinel(units::length::meter_t);
+
+  std::shared_ptr<mantle_api::IEntity> entity;
+
+  units::velocity::meters_per_second_t velocity;
+};
+
+std::set<Obstacle, Less<ToMin>> GetObstacles(const mantle_api::ITrafficAreaStream&);
+}  // namespace OpenScenarioEngine::v1_3
+
+namespace OpenScenarioEngine::v1_3::detail
+{
+template <typename Type>
+struct Vec2
+{
+  Type x, y;
+};
+
+template <typename Type>
+inline Vec2<Type> rotate_2d(const mantle_api::Vec3<Type>& input, units::angle::radian_t angle)
+{
+  const auto cos{units::math::cos(angle)};
+  const auto sin{units::math::sin(angle)};
+  return {input.x * cos - input.y * sin, input.x * sin + input.y * cos};
+}
+}  // namespace OpenScenarioEngine::v1_3::detail
diff --git a/engine/src/Utils/Spawning/SpawnSpace.cpp b/engine/src/Utils/Spawning/SpawnSpace.cpp
index ca1490c4..1b1a5761 100644
--- a/engine/src/Utils/Spawning/SpawnSpace.cpp
+++ b/engine/src/Utils/Spawning/SpawnSpace.cpp
@@ -10,15 +10,86 @@
 
 #include "SpawnSpace.h"
 
+#include <algorithm>
 #include <cassert>
+#include <numeric>
+
+#include "Length.h"
+#include "Weight.h"
 
 namespace OpenScenarioEngine::v1_3
 {
-std::pair<SpawnZones &, units::length::meter_t> SpawnSpace::GetZonesAndOffset(units::length::meter_t value)
+SpawnSpace::SpawnSpace(const OpenScenarioEngine::v1_3::TrafficDistributions &distributions, std::vector<SpawnZones> &zones)
+{
+  // Add entities
+  {
+    const size_t number_of_entities{std::transform_reduce(distributions.begin(), distributions.end(), size_t{}, std::plus<>{}, ToSize{})};
+    entities.reserve(number_of_entities);
+  }
+  for (auto &distribution : distributions)
+  {
+    std::transform(distribution.begin(), distribution.end(), std::back_inserter(entities),  //
+                   [multiplier = distribution.weight](const Entity &entity) -> WeightedEntity
+                   { return {entity, GetWeight(entity) * multiplier}; });
+  }
+  std::sort(entities.begin(), entities.end(), Less<ToLength>{});
+  weights = Transform(entities, ToAccumulatedWeight{});
+
+  // Add intervals
+  for (const SpawnZones &regions : zones)
+  {
+    for (const SpawnZone &zone : regions)
+    {
+      for (auto obstacle{std::next(zone.obstacles.begin())}; obstacle != zone.obstacles.end(); ++obstacle)
+      {
+        spots.emplace(*std::prev(obstacle), *obstacle);
+      }
+    }
+  }
+}
+
+units::length::meter_t SpawnSpace::GetMaxIntervalLength() const
+{
+  return GetLength(*spots.rbegin());
+}
+
+std::vector<WeightedEntity>::const_iterator SpawnSpace::FindSpawnableEntityEnd(units::length::meter_t upper_bound) const
+{
+  return std::upper_bound(entities.begin(), entities.end(), [upper_bound](const Entity &entity)
+                          { return GetLength(entity) < upper_bound; });
+}
+std::vector<WeightedEntity>::iterator SpawnSpace::FindSpawnableEntityEnd(units::length::meter_t upper_bound)
+{
+  return std::upper_bound(entities.begin(), entities.end(), [upper_bound](const Entity &entity)
+                          { return GetLength(entity) < upper_bound; });
+}
+
+std::set<SpawnSpot, Less<ToLength>>::const_iterator SpawnSpace::SampleInterval(units::length::meter_t min_length, VelocityRange velocity, StochasticsInterface &rng) const
 {
-  assert(weights.size() >= 2);
-  const auto offset{std::prev(std::upper_bound(std::next(weights.begin()), std::prev(weights.end()), value))};
-  const auto index{std::distance(weights.begin(), offset)};
-  return {container[static_cast<size_t>(index)], *offset};
+  auto spot{spots.lower_bound(min_length)};
+  if (spot == spots.end())
+  {
+    return spots.end();
+  }
+  std::vector<decltype(spot)> candidates;
+  for (; spot != spots.end(); ++spot)
+  {
+    const bool can_keep_up{spot->velocity.Contains(velocity)};
+    const bool is_enough_room_after_2_seconds{GetLength(*spot) - spot->velocity.GetDifference() * units::time::second_t{2.0} > min_length};
+    if (can_keep_up && is_enough_room_after_2_seconds)
+    {
+      candidates.push_back(spot);
+    }
+  }
+  if (candidates.empty())
+  {
+    return spots.end();
+  }
+  ToAccumulatedLength accumulator;
+  std::vector<units::length::meter_t> lengths{Transform(candidates,
+                                                        [&accumulator](const auto iterator)
+                                                        { return accumulator(*iterator); })};
+  const units::length::meter_t sampled_length{rng.GetUniformDistributed(.0, lengths.back().value())};
+  return Sample(candidates, lengths, sampled_length);
 }
 }  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/SpawnSpace.h b/engine/src/Utils/Spawning/SpawnSpace.h
index 88cec5d4..d277b4b0 100644
--- a/engine/src/Utils/Spawning/SpawnSpace.h
+++ b/engine/src/Utils/Spawning/SpawnSpace.h
@@ -1,5 +1,5 @@
 /********************************************************************************
- * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License 2.0 which is available at
@@ -10,53 +10,114 @@
 
 #pragma once
 
+#include <Stochastics/StochasticsInterface.h>
+
+#include <cassert>
+#include <set>
 #include <utility>
 #include <vector>
 
 #include "Conversion/OscToMantle/ConvertScenarioTrafficDistribution.h"
 #include "Iterable.h"
+#include "Length.h"
+#include "SpawnSpot.h"
 #include "SpawnZone.h"
+#include "VelocityRange.h"
 
 namespace OpenScenarioEngine::v1_3
 {
-struct SpawnSpace : Iterable<std::vector<SpawnZones>>
+using Entity = typename std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IEntityObject>;
+
+struct WeightedEntity
+{
+  Entity handle;
+  units::dimensionless::scalar_t weight;  // Product of the distribution's weight and the weight of this entity's handle
+};
+
+/// A SpawnSpace is essentially a self-managing, depleting list of TrafficAreas and weighted entities optimized
+/// for the use-case of spawning entities at the rear end of a TrafficArea (with offsets). It restructures the
+/// data to be sorted by interval and entity length, allowing efficient filtering of remaining spawnable entities
+/// and intervals as the space is filled.
+struct SpawnSpace
 {
+  SpawnSpace(const OpenScenarioEngine::v1_3::TrafficDistributions &, std::vector<SpawnZones> &);
+
+  /// Returns the entity out of the set of entities in this space based on the given sample
+  ///
+  Entity GetEntity(units::dimensionless::scalar_t sample);
+
+  Entity SampleEntity(StochasticsInterface &) const;
+
+  std::set<SpawnSpot, Less<ToLength>>::const_iterator SampleSpot(units::length::meter_t min_length, VelocityRange, StochasticsInterface &) const;
+
   /// Tries to spawn an entity and returns whether spawning was successful.
-  /// If successful, also adjusts the weighting of the spawn space.
+  /// If successful, also adjusts the spawn space's intervals.
   ///
-  /// \tparam EntityCreator Provides CreateEntity()
-  /// \tparam Sampler Invocable with signature double(double value) returning a number within [0, value)
-  template <typename EntityCreator, typename Sampler>
-  bool Spawn(EntityCreator &&, Sampler &&);
+  /// \tparam EntityCreator Provides CreateEntity(const std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IScenarioObject> &)
+  /// \param StochasticsInterface Used to sample the entity, its position and its velocity
+  template <typename EntityCreator>
+  bool Spawn(EntityCreator &&, StochasticsInterface &);
+
+  units::length::meter_t GetMaxIntervalLength() const;
 
-  // Returns the spawn zones and weight offset containing the given sample
-  std::pair<SpawnZones &, units::length::meter_t> GetZonesAndOffset(units::length::meter_t);
+  std::vector<WeightedEntity>::const_iterator FindSpawnableEntityEnd(units::length::meter_t upper_bound) const;
+  std::vector<WeightedEntity>::iterator FindSpawnableEntityEnd(units::length::meter_t upper_bound);
 
-  TrafficDistributions distributions;
-  std::vector<units::length::meter_t> weights;
+  std::set<SpawnSpot, Less<ToLength>> spots;
+
+  std::vector<WeightedEntity> entities;                 // Entities sorted by ascending length
+  std::vector<units::dimensionless::scalar_t> weights;  // Used to sample an entity
 };
+
 }  // namespace OpenScenarioEngine::v1_3
 
 namespace OpenScenarioEngine::v1_3
 {
-
-template <typename EntityCreator, typename Sampler>
-bool SpawnSpace::Spawn(EntityCreator &&spawner, Sampler &&sampler)
+Entity SpawnSpace::SampleEntity(StochasticsInterface &rng) const
 {
-  if (empty() || distributions.empty())
+  const units::length::meter_t max_length{GetMaxIntervalLength()};
+  const auto entity_end{FindSpawnableEntityEnd(max_length)};
+  if (entity_end == entities.begin())
   {
-    return false;
+    return nullptr;
   }
-  if (distribution.size() == 1)
+  assert(weights.size() == entities.size() + 1);
+  const units::dimensionless::scalar_t max_weight{weights[static_cast<size_t>(std::distance(entities.begin(), entity_end))]};
+  const units::dimensionless::scalar_t sampled_weight{rng.GetUniformDistributed(.0, max_weight.value())};
+  return Sample(entities, weights, sampled_weight).handle;
+}
+
+template <typename EntityCreator>
+bool SpawnSpace::Spawn(EntityCreator &&spawner, StochasticsInterface &rng)
+{
+  Entity entity{SampleEntity(rng)};
+  if (entity == nullptr)
   {
-    return Spawn(distributions.front(), std::forward<EntityCreator>(spawner), std::forward<Sample>(sampler));
+    return false;
   }
-  if (size() == 1)
+  const auto velocity_range{GetSpawnVelocityRange(entity)};
+  const auto spot{SampleSpot(GetLength(entity), velocity_range, rng)};
+  if (spot == spots.end())
   {
-    return front().Spawn(distributions, std::forward<EntityCreator>(spawner), std::forward<Sampler>(sampler));
+    return false;
   }
-  const units::length::meter_t sample{sampler(weights.back())};
-  auto [zones, offset]{GetZonesAndOffset(sample)};
-  return zones.Spawn(sample - offset, distributions, std::forward<EntityCreator>(spawner), std::forward<Sampler>(sampler));
+
+  // TODO: Find last spawnable spot on the zone of the sampled interval
+  // spawner.CreateEntity(*entity, place.zone->GetPosition(distance));
+  // TODO: Remove/replace interval
+  // interval->zone->RemoveInterval(*interval->interval, sampler(restricted_velocity.min, restricted_velocity.max));
+  return true;
 }
+
+// std::pair<const SpawnZones &, const SpawnZone &> SpawnSpace::SampleZone(units::dimensionless::scalar_t sample) const
+// {
+//   assert(units::dimensionless::scalar_t{} <= sample && sample < units::dimensionless::scalar_t{1.0});
+//   const units::length::meter_t weight{sample * weights.back()};
+//   auto index{static_cast<size_t>(std::distance(weights.begin(), std::prev(std::upper_bound(std::next(weights.begin()), weights.end(), weight))))};
+//   assert(index > 1);
+//   const SpawnZones &zones{(*this)[index]};
+//   const units::length::meter_t zone_weight{weight - weights[index]};
+//   auto zone_index{static_cast<size_t>(std::distance(zones.lengths.begin(), std::prev(std::upper_bound(std::next(zones.lengths.begin()), zones.lengths.end(), zone_weight))))};
+//   return {zones, zones[zone_index]};
+// }
 }  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/SpawnSpot.h b/engine/src/Utils/Spawning/SpawnSpot.h
new file mode 100644
index 00000000..84804855
--- /dev/null
+++ b/engine/src/Utils/Spawning/SpawnSpot.h
@@ -0,0 +1,39 @@
+/********************************************************************************
+ * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+#pragma once
+
+#include "Interval.h"
+#include "Obstacle.h"
+#include "VelocityRange.h"
+
+namespace OpenScenarioEngine::v1_3
+{
+/// A SpawnSpot is an interval that holds necessary information for a SpawnSpace to determine whether an
+/// entity can and should be spawned within it. This includes the velocities of the obstacle before and after it
+/// and a pointer to the spot's zone so that the SpawnSpace can look for the further spots within that zone.
+struct SpawnSpot : Interval
+{
+  constexpr SpawnSpot(Interval &&, VelocityRange &&);
+
+  constexpr SpawnSpot(const Obstacle &before, const Obstacle &after);
+
+  VelocityRange velocity;
+};
+}  // namespace OpenScenarioEngine::v1_3
+
+namespace OpenScenarioEngine::v1_3
+{
+constexpr SpawnSpot::SpawnSpot(Interval &&interval, VelocityRange &&velocity)
+    : Interval{std::move(interval)}, velocity{std::move(velocity)} {}
+
+constexpr SpawnSpot::SpawnSpot(const Obstacle &before, const Obstacle &after)
+    : Interval{before.max, after.min}, velocity{before.velocity, after.velocity} {}
+}  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/SpawnZone.cpp b/engine/src/Utils/Spawning/SpawnZone.cpp
index 88a4e93e..d682f779 100644
--- a/engine/src/Utils/Spawning/SpawnZone.cpp
+++ b/engine/src/Utils/Spawning/SpawnZone.cpp
@@ -17,86 +17,34 @@
 
 namespace OpenScenarioEngine::v1_3
 {
-SpawnZone::SpawnZone(std::vector<Interval> input)
-    : Iterable<std::vector<Interval>>{std::move(input)}, lengths{units::length::meter_t{}}
+SpawnZone::SpawnZone(Interval&& interval, std::set<Obstacle, Less<ToMin>>&& obstacles)
+    : Interval{std::move(interval)}, obstacles{std::move(obstacles)} {}
+
+SpawnZone::SpawnZone(const mantle_api::ITrafficAreaStream& stream)
+    : SpawnZone{Interval{{}, ToLength{}(stream)}, GetObstacles(stream)}
 {
-  Transform(GetContainer(), lengths, ToAccumulatedLength{});
 }
 
-void SpawnZone::UpdateIndexToLongest()
+SpawnZones::SpawnZones(std::vector<SpawnZone> input)
+    : Iterable<std::vector<SpawnZone>>{std::move(input)}
 {
-  indexToLongest = std::distance(begin(), std::max_element(begin(), end(), [](const auto& a, const auto& b)
-                                                           { return a.GetLength() < b.GetLength(); }));
 }
 
-void SpawnZone::RemoveInterval(Interval interval)
+SpawnZone ToSpawnZone::operator()(const mantle_api::ITrafficAreaStream& stream) const
 {
-  auto candidate{std::upper_bound(begin(), end(), interval.max, [](units::length::meter_t threshold, const Interval& entry)
-                                  { return threshold <= entry.max; })};
-  assert(candidate != end());
-  assert(candidate->min <= interval.min);
-  assert(candidate->max >= interval.max);
-  const auto offset{std::distance(begin(), candidate)};
-  if (candidate->min == interval.min)
-  {
-    if (candidate->max == interval.max)
-    {
-      lengths.erase(std::next(lengths.begin(), offset + 1));
-      GetContainer().erase(candidate);
-      if (indexToLongest == offset)
-      {
-        UpdateIndexToLongest();
-      }
-      else if (indexToLongest > offset + 1)
-      {
-        --indexToLongest;
-      }
-    }
-    else
-    {
-      candidate->min = interval.max;
-      if (indexToLongest == offset)
-      {
-        UpdateIndexToLongest();
-      }
-    }
-    std::transform(std::next(begin(), offset),
-                   end(),
-                   std::next(lengths.begin(), offset),
-                   ToAccumulatedLength{offset ? *std::next(lengths.begin(), offset - 1) : units::length::meter_t{}});
-    return;
-  }
-  else if (candidate->max == interval.max)
-  {
-    candidate->max = interval.min;
-    if (indexToLongest == offset)
-    {
-      indexToLongest = std::distance(lengths.begin(), std::max_element(lengths.begin(), lengths.end()));
-    }
-    std::transform(std::next(begin(), offset),
-                   end(),
-                   std::next(lengths.begin(), offset),
-                   ToAccumulatedLength{offset ? *std::next(lengths.begin(), offset - 1) : units::length::meter_t{}});
-    return;
-  }
-  GetContainer().emplace(std::next(candidate), interval.max, candidate->max);
-  lengths.push_back(units::length::meter_t{});
-  std::next(begin(), offset)->max = interval.min;
-  std::transform(std::next(begin(), offset),
-                 end(),
-                 std::next(lengths.begin(), offset),
-                 ToAccumulatedLength{offset ? *std::next(lengths.begin(), offset - 1) : units::length::meter_t{}});
+  return {stream};
 }
 
-SpawnZones::SpawnZones(std::vector<SpawnZone> input)
-    : Iterable<std::vector<SpawnZone>>{std::move(input)}, lengths{units::length::meter_t{}}
+SpawnZone ToSpawnZone::operator()(const std::shared_ptr<mantle_api::ITrafficAreaStream>& stream) const
 {
-  Transform(GetContainer(), lengths, ToAccumulatedLength{});
+  return (*this)(*stream);
 }
-}  // namespace OpenScenarioEngine::v1_3
 
-namespace OpenScenarioEngine::v1_3
+SpawnZones::SpawnZones(const mantle_api::TrafficArea& area)
+    : SpawnZones{Transform(area, ToSpawnZone{})}
 {
+}
+
 SpawnZones ToSpawnZones::operator()(const mantle_api::TrafficArea& area) const
 {
   return {area};
diff --git a/engine/src/Utils/Spawning/SpawnZone.h b/engine/src/Utils/Spawning/SpawnZone.h
index 92389b28..d3c182c4 100644
--- a/engine/src/Utils/Spawning/SpawnZone.h
+++ b/engine/src/Utils/Spawning/SpawnZone.h
@@ -14,53 +14,44 @@
 #include <MantleAPI/Traffic/i_traffic_area_stream.h>
 #include <units.h>
 
+#include <optional>
+#include <set>
 #include <vector>
 
 #include "Common.h"
 #include "Interval.h"
 #include "Iterable.h"
+#include "SpawnSpot.h"
 
 namespace OpenScenarioEngine::v1_3
 {
-struct SpawnZone : Iterable<std::vector<Interval>>
+struct SpawnZone : Interval
 {
-  SpawnZone(std::vector<Interval>);
+  SpawnZone(Interval&&, std::set<Obstacle, Less<ToMin>>&&);
 
-  template <typename Filter = Return<true>>
-  SpawnZone(const mantle_api::ITrafficAreaStream&, Filter&&);
+  SpawnZone(const mantle_api::ITrafficAreaStream&);
 
-  /// Removes the given interval from this spawn zone and updates the interval lengths and index to the longest interval if necessary
-  ///
-  /// \param Interval Interval guaranteed to be a subset of an interval of this zone
-  void RemoveInterval(Interval);
+  const Interval& GetLongestInterval() const;
+  Interval& GetLongestInterval();
 
-  void UpdateIndexToLongest();
+  template <typename Predicate>
+  std::optional<SpawnSpot> GetLastSpawnSpot(Predicate&&) const;
 
-  std::vector<units::length::meter_t> lengths;
-  std::ptrdiff_t indexToLongest;
+  std::set<Obstacle, Less<ToMin>> obstacles;
 };
 
-template <typename Filter = Return<true>>
 struct ToSpawnZone
 {
   SpawnZone operator()(const mantle_api::ITrafficAreaStream&) const;
 
   SpawnZone operator()(const std::shared_ptr<mantle_api::ITrafficAreaStream>&) const;
-
-  Filter filter;
 };
 
-template <typename Filter>
-ToSpawnZone(Filter&&) -> ToSpawnZone<Filter>;
-
 struct SpawnZones : Iterable<std::vector<SpawnZone>>
 {
   SpawnZones(std::vector<SpawnZone>);
 
-  template <typename Filter = Return<true>>
-  SpawnZones(const mantle_api::TrafficArea&, Filter&& = {});
-
-  std::vector<units::length::meter_t> lengths;
+  SpawnZones(const mantle_api::TrafficArea&);
 };
 
 struct ToSpawnZones
@@ -71,27 +62,31 @@ struct ToSpawnZones
 
 namespace OpenScenarioEngine::v1_3
 {
-template <typename Filter>
-SpawnZone ToSpawnZone<Filter>::operator()(const mantle_api::ITrafficAreaStream& stream) const
-{
-  return {stream, filter};
-}
-
-template <typename Filter>
-SpawnZone ToSpawnZone<Filter>::operator()(const std::shared_ptr<mantle_api::ITrafficAreaStream>& stream) const
-{
-  return (*this)(*stream);
-}
-
-template <typename Filter>
-SpawnZone::SpawnZone(const mantle_api::ITrafficAreaStream& stream, Filter&& filter)
-    : SpawnZone{GetFreeIntervals(stream, std::forward<Filter>(filter))}
-{
-}
-
-template <typename Filter>
-SpawnZones::SpawnZones(const mantle_api::TrafficArea& area, Filter&& filter)
-    : SpawnZones{Transform(area, ToSpawnZone{std::forward<Filter>(filter)})}
+template <typename Predicate>
+std::optional<SpawnSpot> SpawnZone::GetLastSpawnSpot(Predicate&& pred) const
 {
+  auto obstacle_before{std::find_if(obstacles.rbegin(),
+                                    obstacles.rend(),
+                                    [this](const Obstacle& obstacle)
+                                    { return obstacle.min < max; })};
+  if (obstacle_before == obstacles.rend())
+  {  // There are no obstacles
+    return pred(*this) ? std::optional<SpawnSpot>{*this, {}} : std::nullopt;
+  }
+  auto obstacle_after{obstacle_before++};
+  while (obstacle_before != obstacles.rend())
+  {
+    if (SpawnSpot candidate{*obstacle_before, *obstacle_after}; pred(candidate))
+    {  // Obstacle before and after the spawn spot
+      return std::move(candidate);
+    }
+    ++obstacle_before;
+    ++obstacle_after;
+  }
+  if (SpawnSpot candidate{Obstacle::GetStartingSentinel(min), *obstacle_after}; obstacle_after->min > min && pred(candidate))
+  {  // No obstacle before, but an obstacle after the spawn spot
+    return std::move(candidate);
+  }
+  return std::nullopt;
 }
 }  // namespace OpenScenarioEngine::v1_3
\ No newline at end of file
diff --git a/engine/src/Utils/Spawning/VelocityRange.cpp b/engine/src/Utils/Spawning/VelocityRange.cpp
new file mode 100644
index 00000000..ba65e1a1
--- /dev/null
+++ b/engine/src/Utils/Spawning/VelocityRange.cpp
@@ -0,0 +1,54 @@
+/********************************************************************************
+ * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+#include "VelocityRange.h"
+
+#include <exception>
+#include <string>
+
+namespace OpenScenarioEngine::v1_3
+{
+VelocityRange ToSpawnVelocityRange::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IEntityObject &object) const
+{
+  return object.GetVehicle() ? (*this)(object.GetVehicle()) : (*this)(object.GetPedestrian());
+}
+
+VelocityRange ToSpawnVelocityRange::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IVehicle &vehicle) const
+{
+  return (*this)(vehicle.GetProperties());
+}
+
+VelocityRange ToSpawnVelocityRange::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IPedestrian &pedestrian) const
+{
+  return (*this)(pedestrian.GetProperties());
+}
+
+VelocityRange ToSpawnVelocityRange::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IProperties &properties) const
+{
+  const auto &entries{properties.GetProperties()};
+  const auto min_match{std::find_if(entries.begin(),
+                                    entries.end(),
+                                    [](const std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IProperty> &entry)
+                                    { return entry->GetName() == "MinVelocity"; })};
+  if (min_match == entries.end())
+  {
+    throw std::range_error("ToSpawnVelocityRange: 'MinVelocity' not found in properties");
+  }
+  const auto max_match{std::find_if(entries.begin(),
+                                    entries.end(),
+                                    [](const std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IProperty> &entry)
+                                    { return entry->GetName() == "MaxVelocity"; })};
+  if (max_match == entries.end())
+  {
+    throw std::range_error("ToSpawnVelocityRange: 'MaxVelocity' not found in properties");
+  }
+  return {units::length::meter_t{std::stod((*min_match)->GetValue())}, units::length::meter_t{std::stod((*max_match)->GetValue())}};
+}
+}  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/VelocityRange.h b/engine/src/Utils/Spawning/VelocityRange.h
new file mode 100644
index 00000000..e851efc0
--- /dev/null
+++ b/engine/src/Utils/Spawning/VelocityRange.h
@@ -0,0 +1,78 @@
+/********************************************************************************
+ * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+#pragma once
+
+#include <units.h>
+
+#include <memory>
+
+#include "openScenarioLib/generated/v1_3/api/ApiClassInterfacesV1_3.h"
+
+namespace OpenScenarioEngine::v1_3
+{
+struct SpawnZone;
+
+struct VelocityRange
+{
+  units::velocity::meters_per_second_t min{std::numeric_limits<double>::lowest()}, max{std::numeric_limits<double>::max()};
+
+  constexpr bool Contains(const VelocityRange &) const;
+
+  constexpr units::velocity::meters_per_second_t GetDifference() const;
+};
+
+struct ToSpawnVelocityRange
+{
+  constexpr VelocityRange operator()(VelocityRange) const;
+
+  template <typename Type>
+  VelocityRange operator()(const std::shared_ptr<Type> &) const;
+
+  VelocityRange operator()(const NET_ASAM_OPENSCENARIO::v1_3::IEntityObject &) const;
+  VelocityRange operator()(const NET_ASAM_OPENSCENARIO::v1_3::IMiscObject &) const;
+  VelocityRange operator()(const NET_ASAM_OPENSCENARIO::v1_3::IVehicle &) const;
+  VelocityRange operator()(const NET_ASAM_OPENSCENARIO::v1_3::IPedestrian &) const;
+  VelocityRange operator()(const NET_ASAM_OPENSCENARIO::v1_3::IProperties &) const;
+};
+
+template <typename Input>
+VelocityRange GetSpawnVelocityRange(Input &&);
+
+}  // namespace OpenScenarioEngine::v1_3
+
+namespace OpenScenarioEngine::v1_3
+{
+constexpr bool VelocityRange::Contains(const VelocityRange &other) const
+{
+  return min <= other.min && other.max <= max;
+}
+
+constexpr units::velocity::meters_per_second_t VelocityRange::GetDifference() const
+{
+  return max - min;
+}
+
+constexpr VelocityRange ToSpawnVelocityRange::operator()(VelocityRange input) const
+{
+  return input;
+}
+
+template <typename Type>
+VelocityRange ToSpawnVelocityRange::operator()(const std::shared_ptr<Type> &input) const
+{
+  return ToSpawnVelocityRange{}(*input);
+}
+
+template <typename Input>
+VelocityRange GetSpawnVelocityRange(Input &&input)
+{
+  return ToSpawnVelocityRange{}(std::forward<Input>(input));
+}
+}  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/Weight.h b/engine/src/Utils/Spawning/Weight.h
new file mode 100644
index 00000000..374efefa
--- /dev/null
+++ b/engine/src/Utils/Spawning/Weight.h
@@ -0,0 +1,92 @@
+/********************************************************************************
+ * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+#pragma once
+
+#include <openScenarioLib/generated/v1_3/api/ApiClassInterfacesV1_3.h>
+#include <units.h>
+
+#include <memory>
+
+namespace OpenScenarioEngine::v1_3
+{
+struct ToWeight
+{
+  constexpr units::dimensionless::scalar_t operator()(units::dimensionless::scalar_t) const;
+
+  template <typename Type>
+  units::dimensionless::scalar_t operator()(const std::shared_ptr<Type>&) const;
+
+  units::dimensionless::scalar_t operator()(const NET_ASAM_OPENSCENARIO::v1_3::IScenarioObjectTemplate&) const;
+
+  units::dimensionless::scalar_t operator()(const NET_ASAM_OPENSCENARIO::v1_3::IEntityObject&) const;
+
+  units::dimensionless::scalar_t operator()(const NET_ASAM_OPENSCENARIO::v1_3::IPedestrian&) const;
+
+  units::dimensionless::scalar_t operator()(const NET_ASAM_OPENSCENARIO::v1_3::IVehicle&) const;
+
+  units::dimensionless::scalar_t operator()(const NET_ASAM_OPENSCENARIO::v1_3::IProperties&) const;
+};
+
+template <typename Input>
+units::dimensionless::scalar_t GetWeight(Input&&);
+}  // namespace OpenScenarioEngine::v1_3
+
+namespace OpenScenarioEngine::v1_3
+{
+constexpr units::dimensionless::scalar_t ToWeight::operator()(units::dimensionless::scalar_t value) const
+{
+  return value;
+}
+
+template <typename Type>
+units::dimensionless::scalar_t ToWeight::operator()(const std::shared_ptr<Type>& input) const
+{
+  return ToWeight{}(*input);
+}
+
+template <typename Input>
+units::dimensionless::scalar_t GetWeight(Input&& input)
+{
+  return ToWeight{}(std::forward<Input>(input));
+}
+
+units::dimensionless::scalar_t ToWeight::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IScenarioObjectTemplate& object) const
+{
+  return ToWeight{}(object.GetEntityObject());
+}
+
+units::dimensionless::scalar_t ToWeight::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IEntityObject& object) const
+{
+  return object.GetVehicle() ? ToWeight{}(object.GetVehicle()) : ToWeight{}(object.GetPedestrian());
+}
+
+units::dimensionless::scalar_t ToWeight::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IPedestrian& pedestrian) const
+{
+  return ToWeight{}(pedestrian.GetProperties());
+}
+
+units::dimensionless::scalar_t ToWeight::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IVehicle& vehicle) const
+{
+  return ToWeight{}(vehicle.GetProperties());
+}
+
+units::dimensionless::scalar_t ToWeight::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IProperties& properties) const
+{
+  const auto& entries{properties.GetProperties()};
+  auto match{std::find_if(entries.begin(), entries.end(), [](const std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IProperty>& entry)
+                          { return entry->GetName() == "Weight"; })};
+  if (match != entries.end())
+  {
+    return units::dimensionless::scalar_t{std::stod((*match)->GetValue())};
+  }
+  throw std::runtime_error("ToWeight: 'Weight' not found in properties");
+}
+}  // namespace OpenScenarioEngine::v1_3
-- 
GitLab


From 7e6be64ef0db0d9c2006ab529d6340bebb5d8f18 Mon Sep 17 00:00:00 2001
From: Noah Schick <noah.schick@in-tech.com>
Date: Fri, 17 Jan 2025 10:58:40 +0100
Subject: [PATCH 08/26] Apply various spawning adjustments

---
 engine/CMakeLists.txt                         |  8 +----
 engine/cmake/generated_files.cmake            |  1 -
 .../ConvertScenarioTrafficDistribution.cpp    |  2 +-
 .../GenericAction/TrafficAreaAction.cpp       |  3 --
 .../GenericAction/TrafficAreaAction.h         |  4 ++-
 engine/src/Utils/Spawning/Common.h            |  1 +
 engine/src/Utils/Spawning/Length.cpp          | 22 +++++++++---
 engine/src/Utils/Spawning/Length.h            | 27 +++++---------
 engine/src/Utils/Spawning/LinkedInterval.cpp  | 34 ------------------
 engine/src/Utils/Spawning/LinkedInterval.h    | 36 -------------------
 engine/src/Utils/Spawning/SpawnSpace.cpp      | 34 +++++++++---------
 engine/src/Utils/Spawning/SpawnSpace.h        |  2 ++
 engine/src/Utils/Spawning/SpawnZone.h         |  3 +-
 engine/src/Utils/Spawning/Weight.h            | 24 +++++++++++++
 utils/ci/conan/conanfile.txt                  |  4 +--
 .../conan/recipe/mantleapi/all/conandata.yml  |  4 ---
 .../openscenario_engine/all/conanfile.py      |  2 +-
 17 files changed, 80 insertions(+), 131 deletions(-)
 delete mode 100644 engine/src/Utils/Spawning/LinkedInterval.cpp
 delete mode 100644 engine/src/Utils/Spawning/LinkedInterval.h

diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt
index fed70383..b339fbfe 100644
--- a/engine/CMakeLists.txt
+++ b/engine/CMakeLists.txt
@@ -55,12 +55,6 @@ target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_17)
 find_package(OpenScenarioAPI REQUIRED MODULE)
 find_package(Antlr4Runtime REQUIRED MODULE)
 
-include(FetchContent)
-FetchContent_Declare(Stochastics
- URL https://gitlab.eclipse.org/eclipse/openpass/stochastics-library/-/archive/main/stochastics-library-main.zip
- EXCLUDE_FROM_ALL)
-FetchContent_MakeAvailable(Stochastics)
-
 include(cmake/generated_files.cmake)
 
 target_sources(
@@ -81,7 +75,7 @@ target_link_libraries(${PROJECT_NAME} PUBLIC antlr4_runtime::shared
                                              openscenario_api::shared 
                                              units::units 
                                              Yase::agnostic_behavior_tree
-                                      PRIVATE Stochastics)
+                                      PRIVATE Stochastics::Stochastics)
 
 if(USE_CCACHE)
   find_program(CCACHE_FOUND ccache)
diff --git a/engine/cmake/generated_files.cmake b/engine/cmake/generated_files.cmake
index 42b52e31..3700be3b 100644
--- a/engine/cmake/generated_files.cmake
+++ b/engine/cmake/generated_files.cmake
@@ -234,7 +234,6 @@ list(APPEND ${PROJECT_NAME}_SOURCES
     src/Storyboard/MotionControlAction/SpeedAction_impl.cpp
     src/Utils/Spawning/Interval.cpp
     src/Utils/Spawning/Length.cpp
-    src/Utils/Spawning/LinkedInterval.cpp
     src/Utils/Spawning/Obstacle.cpp
     src/Utils/Spawning/SpawnSpace.cpp
     src/Utils/Spawning/SpawnZone.cpp
diff --git a/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.cpp b/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.cpp
index ae0eb73c..28cfdb42 100644
--- a/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.cpp
+++ b/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.cpp
@@ -10,8 +10,8 @@
 
 #include "ConvertScenarioTrafficDistribution.h"
 
-#include "Utils/Spawning/Length.h"
 #include "Utils/Spawning/Transform.h"
+#include "Utils/Spawning/Weight.h"
 
 namespace OpenScenarioEngine::v1_3
 {
diff --git a/engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp b/engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp
index 6777a273..5a1bfbf5 100644
--- a/engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp
+++ b/engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp
@@ -10,8 +10,6 @@
 
 #include "Storyboard/GenericAction/TrafficAreaAction.h"
 
-#include <Stochastics/Stochastics.h>
-
 #include "Conversion/OscToMantle/ConvertScenarioTrafficArea.h"
 #include "Utils/EntityCreator.h"
 #include "Utils/Logger.h"
@@ -66,7 +64,6 @@ bool TrafficAreaAction::Step()
   EntityCreator spawner{mantle.environment};
   std::vector<SpawnZones> spawnZones{Transform(trafficAreas, ToSpawnZones{})};
   SpawnSpace spawnSpace{values.trafficDistributions, spawnZones};
-  Stochastics rng;
   while (values.numberOfEntities)
   {
     if (!spawnSpace.Spawn(spawner, rng))
diff --git a/engine/src/Storyboard/GenericAction/TrafficAreaAction.h b/engine/src/Storyboard/GenericAction/TrafficAreaAction.h
index 1e63a341..22cd970d 100644
--- a/engine/src/Storyboard/GenericAction/TrafficAreaAction.h
+++ b/engine/src/Storyboard/GenericAction/TrafficAreaAction.h
@@ -1,5 +1,5 @@
 /********************************************************************************
- * Copyright (c) 2021-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ * Copyright (c) 2021-2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License 2.0 which is available at
@@ -12,6 +12,7 @@
 
 #include <MantleAPI/Execution/i_environment.h>
 #include <MantleAPI/Traffic/i_traffic_area_service.h>
+#include <Stochastics/Stochastics.h>
 #include <Stochastics/StochasticsInterface.h>
 #include <agnostic_behavior_tree/action_node.h>
 #include <openScenarioLib/generated/v1_3/api/ApiClassInterfacesV1_3.h>
@@ -52,6 +53,7 @@ private:
   /// The accumulated size of all traffic areas up to and including the index of the respective element.
   /// Does not account for overlapping areas.
   std::vector<units::length::meter_t> lengths;
+  Stochastics rng;
 };
 
 namespace Node
diff --git a/engine/src/Utils/Spawning/Common.h b/engine/src/Utils/Spawning/Common.h
index 3d16791a..eff3b449 100644
--- a/engine/src/Utils/Spawning/Common.h
+++ b/engine/src/Utils/Spawning/Common.h
@@ -13,6 +13,7 @@
 #include <units.h>
 
 #include <algorithm>
+#include <cassert>
 #include <functional>
 #include <memory>
 #include <type_traits>
diff --git a/engine/src/Utils/Spawning/Length.cpp b/engine/src/Utils/Spawning/Length.cpp
index cd6310c9..8643a25f 100644
--- a/engine/src/Utils/Spawning/Length.cpp
+++ b/engine/src/Utils/Spawning/Length.cpp
@@ -13,6 +13,8 @@
 #include <cassert>
 #include <numeric>
 
+#include "SpawnSpace.h"
+
 namespace OpenScenarioEngine::v1_3
 {
 units::length::meter_t ToLength::operator()(const mantle_api::ITrafficAreaStream& stream) const
@@ -25,6 +27,21 @@ units::length::meter_t ToLength::operator()(const mantle_api::TrafficArea& area)
   return std::transform_reduce(area.begin(), area.end(), units::length::meter_t{}, std::plus<>{}, *this);
 }
 
+units::length::meter_t ToLength::operator()(const WeightedEntity& entity) const
+{
+  return (*this)(entity.handle);
+}
+
+units::length::meter_t ToLength::operator()(const mantle_api::IEntity& entity) const
+{
+  return (*this)(entity.GetProperties());
+}
+
+units::length::meter_t ToLength::operator()(const mantle_api::EntityProperties& properties) const
+{
+  return properties.bounding_box.dimension.length;
+}
+
 units::length::meter_t ToLength::operator()(const SpawnZone& zone) const
 {
   return zone.GetLength() - std::transform_reduce(zone.obstacles.begin(), zone.obstacles.end(), units::length::meter_t{}, std::plus<>{}, *this);
@@ -35,11 +52,6 @@ units::length::meter_t ToLength::operator()(const SpawnZones& zones) const
   return std::transform_reduce(zones.begin(), zones.end(), units::length::meter_t{}, std::plus<>{}, *this);
 }
 
-units::length::meter_t ToLength::operator()(const LinkedInterval& interval) const
-{
-  return ToLength{}(*interval.interval);
-}
-
 units::length::meter_t ToLength::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IEntityObject& object) const
 {
   if (decltype(auto) vehicle{object.GetVehicle()}; vehicle != nullptr)
diff --git a/engine/src/Utils/Spawning/Length.h b/engine/src/Utils/Spawning/Length.h
index 01a96046..7c81c36f 100644
--- a/engine/src/Utils/Spawning/Length.h
+++ b/engine/src/Utils/Spawning/Length.h
@@ -10,6 +10,7 @@
 
 #pragma once
 
+#include <MantleAPI/Traffic/i_entity.h>
 #include <MantleAPI/Traffic/i_traffic_area_service.h>
 #include <MantleAPI/Traffic/i_traffic_area_stream.h>
 #include <openScenarioLib/generated/v1_3/api/ApiClassInterfacesV1_3.h>
@@ -19,25 +20,30 @@
 
 #include "Conversion/OscToMantle/ConvertScenarioTrafficDistribution.h"
 #include "Interval.h"
-#include "LinkedInterval.h"
 #include "SpawnZone.h"
 #include "Transform.h"
 
 namespace OpenScenarioEngine::v1_3
 {
+struct WeightedEntity;
+
 struct ToLength
 {
   constexpr units::length::meter_t operator()(units::length::meter_t) const;
 
   constexpr units::length::meter_t operator()(const Interval&) const;
 
-  units::length::meter_t operator()(const LinkedInterval&) const;
-
   units::length::meter_t operator()(const mantle_api::ITrafficAreaStream&) const;
 
   template <typename Type>
   units::length::meter_t operator()(const std::shared_ptr<Type>&) const;
 
+  units::length::meter_t operator()(const WeightedEntity&) const;
+
+  units::length::meter_t operator()(const mantle_api::IEntity&) const;
+
+  units::length::meter_t operator()(const mantle_api::EntityProperties&) const;
+
   template <typename Type>
   units::length::meter_t operator()(const Type*) const;
 
@@ -71,14 +77,6 @@ struct ToAccumulatedLength
 
   units::length::meter_t totalLength;
 };
-
-struct ToAccumulatedWeight
-{
-  template <typename Entry>
-  units::dimensionless::scalar_t operator()(const Entry&);
-
-  units::dimensionless::scalar_t totalWeight;
-};
 }  // namespace OpenScenarioEngine::v1_3
 
 namespace OpenScenarioEngine::v1_3
@@ -134,11 +132,4 @@ units::length::meter_t ToAccumulatedLength::operator()(const Type& input)
   totalLength += GetLength(input);
   return totalLength;
 }
-
-template <typename Entry>
-units::dimensionless::scalar_t ToAccumulatedWeight::operator()(const Entry& entry)
-{
-  totalWeight += units::dimensionless::scalar_t{entry.GetWeight()};
-  return totalWeight;
-}
 }  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/LinkedInterval.cpp b/engine/src/Utils/Spawning/LinkedInterval.cpp
deleted file mode 100644
index 6556f22d..00000000
--- a/engine/src/Utils/Spawning/LinkedInterval.cpp
+++ /dev/null
@@ -1,34 +0,0 @@
-/********************************************************************************
- * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0.
- *
- * SPDX-License-Identifier: EPL-2.0
- ********************************************************************************/
-
-#include "LinkedInterval.h"
-
-#include "SpawnZone.h"
-
-namespace OpenScenarioEngine::v1_3
-{
-VelocityRange LinkedInterval::GetVelocityRange() const
-{
-  const size_t index{static_cast<size_t>(std::distance(zone->begin(), interval))};
-  return {zone->velocities[index], zone->velocities[index + 1]};
-}
-
-units::velocity::meters_per_second_t LinkedInterval::GetMinVelocity() const
-{
-  const size_t index{static_cast<size_t>(std::distance(zone->begin(), interval))};
-  return zone->velocities[index];
-}
-
-units::velocity::meters_per_second_t LinkedInterval::GetMaxVelocity() const
-{
-  const size_t index{static_cast<size_t>(std::distance(zone->begin(), interval))};
-  return zone->velocities[index + 1];
-}
-}  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/LinkedInterval.h b/engine/src/Utils/Spawning/LinkedInterval.h
deleted file mode 100644
index cb3780ec..00000000
--- a/engine/src/Utils/Spawning/LinkedInterval.h
+++ /dev/null
@@ -1,36 +0,0 @@
-/********************************************************************************
- * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0.
- *
- * SPDX-License-Identifier: EPL-2.0
- ********************************************************************************/
-#pragma once
-
-#include <vector>
-
-#include "Interval.h"
-#include "VelocityRange.h"
-
-namespace OpenScenarioEngine::v1_3
-{
-struct SpawnZone;
-
-// A LinkedInterval holds an iterator to an Interval and a pointer to the container of that interval (its SpawnZone).
-// This is done so that there can be a list of all Intervals regardless of their SpawnZone while still being able to refer
-// back to the zone they are a part of. That way, an interval can be sampled with a fair distribution efficiently while
-// also reducing the amount of length recomputations needed when an interval is changed/removed. (See SpawnSpace)
-struct LinkedInterval
-{
-  units::velocity::meters_per_second_t GetMinVelocity() const;
-
-  units::velocity::meters_per_second_t GetMaxVelocity() const;
-
-  VelocityRange GetVelocityRange() const;
-
-  std::vector<Interval>::iterator interval;
-  SpawnZone* zone;
-};
-}  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/SpawnSpace.cpp b/engine/src/Utils/Spawning/SpawnSpace.cpp
index 1b1a5761..7bf14331 100644
--- a/engine/src/Utils/Spawning/SpawnSpace.cpp
+++ b/engine/src/Utils/Spawning/SpawnSpace.cpp
@@ -23,8 +23,8 @@ SpawnSpace::SpawnSpace(const OpenScenarioEngine::v1_3::TrafficDistributions &dis
 {
   // Add entities
   {
-    const size_t number_of_entities{std::transform_reduce(distributions.begin(), distributions.end(), size_t{}, std::plus<>{}, ToSize{})};
-    entities.reserve(number_of_entities);
+    const size_t numberOfEntities{std::transform_reduce(distributions.begin(), distributions.end(), size_t{}, std::plus<>{}, ToSize{})};
+    entities.reserve(numberOfEntities);
   }
   for (auto &distribution : distributions)
   {
@@ -53,20 +53,20 @@ units::length::meter_t SpawnSpace::GetMaxIntervalLength() const
   return GetLength(*spots.rbegin());
 }
 
-std::vector<WeightedEntity>::const_iterator SpawnSpace::FindSpawnableEntityEnd(units::length::meter_t upper_bound) const
+std::vector<WeightedEntity>::const_iterator SpawnSpace::FindSpawnableEntityEnd(units::length::meter_t upperBound) const
 {
-  return std::upper_bound(entities.begin(), entities.end(), [upper_bound](const Entity &entity)
-                          { return GetLength(entity) < upper_bound; });
+  return std::upper_bound(entities.begin(), entities.end(), upperBound, [](units::length::meter_t upperBound, const WeightedEntity &entity)
+                          { return GetLength(entity) < upperBound; });
 }
-std::vector<WeightedEntity>::iterator SpawnSpace::FindSpawnableEntityEnd(units::length::meter_t upper_bound)
+std::vector<WeightedEntity>::iterator SpawnSpace::FindSpawnableEntityEnd(units::length::meter_t upperBound)
 {
-  return std::upper_bound(entities.begin(), entities.end(), [upper_bound](const Entity &entity)
-                          { return GetLength(entity) < upper_bound; });
+  return std::upper_bound(entities.begin(), entities.end(), upperBound, [](units::length::meter_t upperBound, const WeightedEntity &entity)
+                          { return GetLength(entity) < upperBound; });
 }
 
-std::set<SpawnSpot, Less<ToLength>>::const_iterator SpawnSpace::SampleInterval(units::length::meter_t min_length, VelocityRange velocity, StochasticsInterface &rng) const
+std::set<SpawnSpot, Less<ToLength>>::const_iterator SpawnSpace::SampleSpot(units::length::meter_t minLength, VelocityRange velocity, StochasticsInterface &rng) const
 {
-  auto spot{spots.lower_bound(min_length)};
+  auto spot{spots.lower_bound(minLength)};
   if (spot == spots.end())
   {
     return spots.end();
@@ -74,22 +74,22 @@ std::set<SpawnSpot, Less<ToLength>>::const_iterator SpawnSpace::SampleInterval(u
   std::vector<decltype(spot)> candidates;
   for (; spot != spots.end(); ++spot)
   {
-    const bool can_keep_up{spot->velocity.Contains(velocity)};
-    const bool is_enough_room_after_2_seconds{GetLength(*spot) - spot->velocity.GetDifference() * units::time::second_t{2.0} > min_length};
-    if (can_keep_up && is_enough_room_after_2_seconds)
+    const bool canKeepUp{spot->velocity.Contains(velocity)};
+    const bool isEnoughRoomAfter2Seconds{GetLength(*spot) - spot->velocity.GetDifference() * units::time::second_t{2.0} > minLength};
+    if (canKeepUp && isEnoughRoomAfter2Seconds)
     {
       candidates.push_back(spot);
     }
   }
-  if (candidates.empty())
+  if (candidates.size() <= 1)
   {
-    return spots.end();
+    return candidates.empty() ? spots.end() : candidiates.begin();
   }
   ToAccumulatedLength accumulator;
   std::vector<units::length::meter_t> lengths{Transform(candidates,
                                                         [&accumulator](const auto iterator)
                                                         { return accumulator(*iterator); })};
-  const units::length::meter_t sampled_length{rng.GetUniformDistributed(.0, lengths.back().value())};
-  return Sample(candidates, lengths, sampled_length);
+  const units::length::meter_t sampledLength{rng.GetUniformDistributed(.0, lengths.back().value())};
+  return Sample(candidates, lengths, sampledLength);
 }
 }  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/SpawnSpace.h b/engine/src/Utils/Spawning/SpawnSpace.h
index d277b4b0..8b257a53 100644
--- a/engine/src/Utils/Spawning/SpawnSpace.h
+++ b/engine/src/Utils/Spawning/SpawnSpace.h
@@ -11,8 +11,10 @@
 #pragma once
 
 #include <Stochastics/StochasticsInterface.h>
+#include <units.h>
 
 #include <cassert>
+#include <memory>
 #include <set>
 #include <utility>
 #include <vector>
diff --git a/engine/src/Utils/Spawning/SpawnZone.h b/engine/src/Utils/Spawning/SpawnZone.h
index d3c182c4..4f8336f9 100644
--- a/engine/src/Utils/Spawning/SpawnZone.h
+++ b/engine/src/Utils/Spawning/SpawnZone.h
@@ -25,8 +25,9 @@
 
 namespace OpenScenarioEngine::v1_3
 {
-struct SpawnZone : Interval
+class SpawnZone : public Interval
 {
+public:
   SpawnZone(Interval&&, std::set<Obstacle, Less<ToMin>>&&);
 
   SpawnZone(const mantle_api::ITrafficAreaStream&);
diff --git a/engine/src/Utils/Spawning/Weight.h b/engine/src/Utils/Spawning/Weight.h
index 374efefa..944cb6c2 100644
--- a/engine/src/Utils/Spawning/Weight.h
+++ b/engine/src/Utils/Spawning/Weight.h
@@ -26,6 +26,8 @@ struct ToWeight
 
   units::dimensionless::scalar_t operator()(const NET_ASAM_OPENSCENARIO::v1_3::IScenarioObjectTemplate&) const;
 
+  units::dimensionless::scalar_t operator()(const NET_ASAM_OPENSCENARIO::v1_3::ITrafficDistributionEntry&) const;
+
   units::dimensionless::scalar_t operator()(const NET_ASAM_OPENSCENARIO::v1_3::IEntityObject&) const;
 
   units::dimensionless::scalar_t operator()(const NET_ASAM_OPENSCENARIO::v1_3::IPedestrian&) const;
@@ -33,10 +35,20 @@ struct ToWeight
   units::dimensionless::scalar_t operator()(const NET_ASAM_OPENSCENARIO::v1_3::IVehicle&) const;
 
   units::dimensionless::scalar_t operator()(const NET_ASAM_OPENSCENARIO::v1_3::IProperties&) const;
+
+  units::dimensionless::scalar_t operator()(const WeightedEntity&) const;
 };
 
 template <typename Input>
 units::dimensionless::scalar_t GetWeight(Input&&);
+
+struct ToAccumulatedWeight
+{
+  template <typename Entry>
+  units::dimensionless::scalar_t operator()(const Entry&);
+
+  units::dimensionless::scalar_t totalWeight;
+};
 }  // namespace OpenScenarioEngine::v1_3
 
 namespace OpenScenarioEngine::v1_3
@@ -63,6 +75,11 @@ units::dimensionless::scalar_t ToWeight::operator()(const NET_ASAM_OPENSCENARIO:
   return ToWeight{}(object.GetEntityObject());
 }
 
+units::dimensionless::scalar_t ToWeight::operator()(const NET_ASAM_OPENSCENARIO::v1_3::ITrafficDistributionEntry& entry) const
+{
+  return units::dimensionless::scalar_t{entry.GetWeight()};
+}
+
 units::dimensionless::scalar_t ToWeight::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IEntityObject& object) const
 {
   return object.GetVehicle() ? ToWeight{}(object.GetVehicle()) : ToWeight{}(object.GetPedestrian());
@@ -89,4 +106,11 @@ units::dimensionless::scalar_t ToWeight::operator()(const NET_ASAM_OPENSCENARIO:
   }
   throw std::runtime_error("ToWeight: 'Weight' not found in properties");
 }
+
+template <typename Entry>
+units::dimensionless::scalar_t ToAccumulatedWeight::operator()(const Entry& entry)
+{
+  totalWeight += GetWeight(entry);
+  return totalWeight;
+}
 }  // namespace OpenScenarioEngine::v1_3
diff --git a/utils/ci/conan/conanfile.txt b/utils/ci/conan/conanfile.txt
index 849dff88..7e405f78 100644
--- a/utils/ci/conan/conanfile.txt
+++ b/utils/ci/conan/conanfile.txt
@@ -1,9 +1,9 @@
 [requires]
-units/2.3.4@openscenarioengine/testing
 mantleapi/v11.0.0@openscenarioengine/testing
-yase/d0c0e58d17358044cc9018c74308b45f6097ecfb@openscenarioengine/testing
 openscenario_api/v1.4.0@openscenarioengine/testing
 stochastics/0.11.0@openscenarioengine/testing
+units/2.3.4@openscenarioengine/testing
+yase/d0c0e58d17358044cc9018c74308b45f6097ecfb@openscenarioengine/testing
 
 [options]
 
diff --git a/utils/ci/conan/recipe/mantleapi/all/conandata.yml b/utils/ci/conan/recipe/mantleapi/all/conandata.yml
index 56d0b1a5..30e9ad2a 100644
--- a/utils/ci/conan/recipe/mantleapi/all/conandata.yml
+++ b/utils/ci/conan/recipe/mantleapi/all/conandata.yml
@@ -41,10 +41,6 @@ sources:
     url: https://gitlab.eclipse.org/eclipse/openpass/mantle-api.git
     sha256: "a5530013edcd2028ce48d1dcd7e90a512c158f27"
 
-  "6.0.1":
-    url: https://gitlab.eclipse.org/eclipse/openpass/mantle-api.git
-    sha256: "4c04afb373d26b9e4ddcae3fd920f6c5543a19f2"
-
   "8.0.0":
     url: https://gitlab.eclipse.org/eclipse/openpass/mantle-api.git
     sha256: "18eadd8ad4f3ddd78c955ce3d4d637a8ff912091"
diff --git a/utils/ci/conan/recipe/openscenario_engine/all/conanfile.py b/utils/ci/conan/recipe/openscenario_engine/all/conanfile.py
index b79fb3fa..43f2201c 100644
--- a/utils/ci/conan/recipe/openscenario_engine/all/conanfile.py
+++ b/utils/ci/conan/recipe/openscenario_engine/all/conanfile.py
@@ -25,7 +25,7 @@ class OpenScenarioEngineConan(ConanFile):
     settings = "os", "compiler", "build_type", "arch"
     options = {"shared": [True, False],
                "fPIC": [True, False],
-               "MantleAPI_version":["6.0.1"],
+               "MantleAPI_version":["ANY"],
                "openscenario_api_version":["ANY"],
                "units_version":["ANY"],
                "Yase_version":["ANY"]
-- 
GitLab


From 72b7da93b865e1f022e226679a87ee9d24c3d2d1 Mon Sep 17 00:00:00 2001
From: Naida Goro <naida.goro@in-tech.com>
Date: Thu, 30 Jan 2025 11:12:36 +0000
Subject: [PATCH 09/26] Fix some build errors in the branch traffic-area-action

---
 engine/CMakeLists.txt                          |  2 ++
 engine/cmake/generated_files.cmake             |  1 +
 .../ConvertScenarioTrafficDistribution.cpp     |  6 +++---
 engine/src/Utils/Spawning/SpawnSpace.cpp       | 18 ++++++++++++++++--
 engine/src/Utils/Spawning/SpawnSpace.h         | 13 -------------
 engine/src/Utils/Spawning/VelocityRange.cpp    |  2 +-
 engine/src/Utils/Spawning/Weight.h             |  4 +++-
 7 files changed, 26 insertions(+), 20 deletions(-)

diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt
index b339fbfe..d81c00c2 100644
--- a/engine/CMakeLists.txt
+++ b/engine/CMakeLists.txt
@@ -48,6 +48,8 @@ find_package(Stochastics REQUIRED)
 # see https://stackoverflow.com/a/58495612
 set(CMAKE_INSTALL_RPATH $ORIGIN)
 
+find_package(Stochastics REQUIRED CONFIG)
+
 add_library(${PROJECT_NAME} SHARED "")
 
 target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_17)
diff --git a/engine/cmake/generated_files.cmake b/engine/cmake/generated_files.cmake
index 3700be3b..5e4d2eb2 100644
--- a/engine/cmake/generated_files.cmake
+++ b/engine/cmake/generated_files.cmake
@@ -598,6 +598,7 @@ list(APPEND ${PROJECT_NAME}_HEADERS
     src/Utils/Spawning/Length.h
     src/Utils/Spawning/SpawnZone.h
     src/Utils/Spawning/Transform.h
+    src/Utils/Spawning/Weight.h
     src/Utils/ConditionEdgeEvaluator.h
     src/Utils/Constants.h
     src/Utils/ControllerCreator.h
diff --git a/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.cpp b/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.cpp
index 28cfdb42..bba5d9a4 100644
--- a/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.cpp
+++ b/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.cpp
@@ -1,5 +1,5 @@
 /********************************************************************************
- * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ * Copyright (c) 2024-2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License 2.0 which is available at
@@ -19,7 +19,7 @@ struct ToEntities
 {
   std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IEntityObject> operator()(const NET_ASAM_OPENSCENARIO::v1_3::IEntityDistributionEntry& entry) const
   {
-    return entry.GetScenarioObjectTemplate()->GetEntityObject();
+    return entry.GetScenarioObjectTemplate()->GetEntitiyObject(); // https://github.com/RA-Consulting-GmbH/openscenario.api.test/issues/222
   }
 
   std::vector<std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IEntityObject>> operator()(const NET_ASAM_OPENSCENARIO::v1_3::ITrafficDistributionEntry& entry) const
@@ -33,7 +33,7 @@ struct ToTrafficDistribution
   TrafficDistribution operator()(const NET_ASAM_OPENSCENARIO::v1_3::ITrafficDistributionEntry& entry) const
   {
     return {TransformSharedPointers(entry.GetEntityDistribution()->GetEntityDistributionEntry(), [](const NET_ASAM_OPENSCENARIO::v1_3::IEntityDistributionEntry& entry)
-                                    { return entry.GetScenarioObjectTemplate()->GetEntityObject(); }),
+                                    { return entry.GetScenarioObjectTemplate()->GetEntitiyObject(); }), // https://github.com/RA-Consulting-GmbH/openscenario.api.test/issues/222
             entry.GetWeight()};
   }
 };
diff --git a/engine/src/Utils/Spawning/SpawnSpace.cpp b/engine/src/Utils/Spawning/SpawnSpace.cpp
index 7bf14331..a1090f7b 100644
--- a/engine/src/Utils/Spawning/SpawnSpace.cpp
+++ b/engine/src/Utils/Spawning/SpawnSpace.cpp
@@ -1,5 +1,5 @@
 /********************************************************************************
- * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ * Copyright (c) 2024-2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License 2.0 which is available at
@@ -48,6 +48,20 @@ SpawnSpace::SpawnSpace(const OpenScenarioEngine::v1_3::TrafficDistributions &dis
   }
 }
 
+Entity SpawnSpace::SampleEntity(StochasticsInterface &rng) const
+{
+  const units::length::meter_t max_length{GetMaxIntervalLength()};
+  const auto entity_end{FindSpawnableEntityEnd(max_length)};
+  if (entity_end == entities.begin())
+  {
+    return nullptr;
+  }
+  assert(weights.size() == entities.size() + 1);
+  const units::dimensionless::scalar_t max_weight{weights[static_cast<size_t>(std::distance(entities.begin(), entity_end))]};
+  const units::dimensionless::scalar_t sampled_weight{rng.GetUniformDistributed(.0, max_weight.value())};
+  return Sample(entities, weights, sampled_weight).handle;
+}
+
 units::length::meter_t SpawnSpace::GetMaxIntervalLength() const
 {
   return GetLength(*spots.rbegin());
@@ -83,7 +97,7 @@ std::set<SpawnSpot, Less<ToLength>>::const_iterator SpawnSpace::SampleSpot(units
   }
   if (candidates.size() <= 1)
   {
-    return candidates.empty() ? spots.end() : candidiates.begin();
+    return candidates.empty() ? spots.end() : *candidates.begin();
   }
   ToAccumulatedLength accumulator;
   std::vector<units::length::meter_t> lengths{Transform(candidates,
diff --git a/engine/src/Utils/Spawning/SpawnSpace.h b/engine/src/Utils/Spawning/SpawnSpace.h
index 8b257a53..2b07296f 100644
--- a/engine/src/Utils/Spawning/SpawnSpace.h
+++ b/engine/src/Utils/Spawning/SpawnSpace.h
@@ -75,19 +75,6 @@ struct SpawnSpace
 
 namespace OpenScenarioEngine::v1_3
 {
-Entity SpawnSpace::SampleEntity(StochasticsInterface &rng) const
-{
-  const units::length::meter_t max_length{GetMaxIntervalLength()};
-  const auto entity_end{FindSpawnableEntityEnd(max_length)};
-  if (entity_end == entities.begin())
-  {
-    return nullptr;
-  }
-  assert(weights.size() == entities.size() + 1);
-  const units::dimensionless::scalar_t max_weight{weights[static_cast<size_t>(std::distance(entities.begin(), entity_end))]};
-  const units::dimensionless::scalar_t sampled_weight{rng.GetUniformDistributed(.0, max_weight.value())};
-  return Sample(entities, weights, sampled_weight).handle;
-}
 
 template <typename EntityCreator>
 bool SpawnSpace::Spawn(EntityCreator &&spawner, StochasticsInterface &rng)
diff --git a/engine/src/Utils/Spawning/VelocityRange.cpp b/engine/src/Utils/Spawning/VelocityRange.cpp
index ba65e1a1..a1326dac 100644
--- a/engine/src/Utils/Spawning/VelocityRange.cpp
+++ b/engine/src/Utils/Spawning/VelocityRange.cpp
@@ -49,6 +49,6 @@ VelocityRange ToSpawnVelocityRange::operator()(const NET_ASAM_OPENSCENARIO::v1_3
   {
     throw std::range_error("ToSpawnVelocityRange: 'MaxVelocity' not found in properties");
   }
-  return {units::length::meter_t{std::stod((*min_match)->GetValue())}, units::length::meter_t{std::stod((*max_match)->GetValue())}};
+  return {units::velocity::meters_per_second_t{std::stod((*min_match)->GetValue())}, units::velocity::meters_per_second_t{std::stod((*max_match)->GetValue())}};
 }
 }  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/Weight.h b/engine/src/Utils/Spawning/Weight.h
index 944cb6c2..b068658b 100644
--- a/engine/src/Utils/Spawning/Weight.h
+++ b/engine/src/Utils/Spawning/Weight.h
@@ -17,6 +17,8 @@
 
 namespace OpenScenarioEngine::v1_3
 {
+struct WeightedEntity;
+
 struct ToWeight
 {
   constexpr units::dimensionless::scalar_t operator()(units::dimensionless::scalar_t) const;
@@ -72,7 +74,7 @@ units::dimensionless::scalar_t GetWeight(Input&& input)
 
 units::dimensionless::scalar_t ToWeight::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IScenarioObjectTemplate& object) const
 {
-  return ToWeight{}(object.GetEntityObject());
+  return ToWeight{}(object.GetEntitiyObject()); // https://github.com/RA-Consulting-GmbH/openscenario.api.test/issues/222
 }
 
 units::dimensionless::scalar_t ToWeight::operator()(const NET_ASAM_OPENSCENARIO::v1_3::ITrafficDistributionEntry& entry) const
-- 
GitLab


From e920427eb1d1a1370820d2b58540a836ef521c36 Mon Sep 17 00:00:00 2001
From: Naida Goro <naida.goro@in-tech.com>
Date: Fri, 31 Jan 2025 05:53:05 +0000
Subject: [PATCH 10/26] Add TrafficAreaActionTest

---
 engine/CMakeLists.txt                         |   1 +
 .../GenericAction/TrafficAreaActionTest.cpp   |  53 ++++++++
 .../scenario_with_traffic_area_action.xosc    | 118 ++++++++++++++++++
 3 files changed, 172 insertions(+)
 create mode 100644 engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp
 create mode 100644 engine/tests/data/Scenarios/scenario_with_traffic_area_action.xosc

diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt
index d81c00c2..2b61c49a 100644
--- a/engine/CMakeLists.txt
+++ b/engine/CMakeLists.txt
@@ -207,6 +207,7 @@ target_sources(
           ${CMAKE_CURRENT_LIST_DIR}/tests/Storyboard/GenericAction/EnvironmentActionTest.cpp
           ${CMAKE_CURRENT_LIST_DIR}/tests/Storyboard/GenericAction/LightStateActionTests.cpp
           ${CMAKE_CURRENT_LIST_DIR}/tests/Storyboard/GenericAction/TeleportActionTest.cpp
+          ${CMAKE_CURRENT_LIST_DIR}/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp
           ${CMAKE_CURRENT_LIST_DIR}/tests/Storyboard/GenericAction/TrafficSignalStateActionTest.cpp
           ${CMAKE_CURRENT_LIST_DIR}/tests/Storyboard/GenericAction/TrafficSinkActionTest.cpp
           ${CMAKE_CURRENT_LIST_DIR}/tests/Storyboard/GenericAction/TrafficSwarmActionTest.cpp
diff --git a/engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp b/engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp
new file mode 100644
index 00000000..33317b2c
--- /dev/null
+++ b/engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp
@@ -0,0 +1,53 @@
+/********************************************************************************
+ * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+#include <MantleAPI/Test/test_utils.h>
+#include <gtest/gtest.h>
+
+#include "OpenScenarioEngine/OpenScenarioEngine.h"
+#include "TestUtils.h"
+#include "TestUtils/TestLogger.h"
+
+using testing::_;
+using testing::Return;
+using testing::ReturnRef;
+using testing::HasSubstr;
+using testing::OpenScenarioEngine::v1_3::OpenScenarioEngineTestBase;
+using testing::OpenScenarioEngine::v1_3::GetScenariosPath;
+
+class TrafficAreaActionTestFixture : public OpenScenarioEngineTestBase
+{
+protected:
+  void SetUp() override
+  { 
+    //LOGGER = std::make_unique<testing::OpenScenarioEngine::v1_3::TestLogger>();
+    //mock_environment_ = std::make_shared<mantle_api::MockEnvironment>();
+    auto& mockTrafficAreaService{dynamic_cast<mantle_api::MockTrafficAreaService&>(env_->GetTrafficAreaService())};
+    ON_CALL(mockTrafficAreaService, CreateTrafficArea(_)).WillByDefault([this]() -> std::vector<std::shared_ptr<mantle_api::ITrafficAreaStream>>
+                                                                        {
+      auto mock_traffic_area_stream{std::make_shared<mantle_api::MockTrafficAreaStream>()};
+      traffic_area_stream_vec_.emplace_back(std::move(mock_traffic_area_stream));
+      return traffic_area_stream_vec_; });
+  }
+
+  //std::shared_ptr<mantle_api::MockEnvironment> mock_environment_;
+  std::vector<std::shared_ptr<mantle_api::ITrafficAreaStream>> traffic_area_stream_vec_;
+  //std::unique_ptr<testing::OpenScenarioEngine::v1_3::TestLogger> LOGGER;
+};
+
+TEST_F(TrafficAreaActionTestFixture, GivenScenarioWithTrafficAreaAction_WhenInitScenario_Then)
+{
+  std::string xosc_file_path{GetScenariosPath(test_info_->file()) + "scenario_with_traffic_area_action.xosc"};
+  OpenScenarioEngine::v1_3::OpenScenarioEngine engine(xosc_file_path, env_);
+  engine.Init();
+
+  //EXPECT_THAT(LOGGER->LastLogLevel(), mantle_api::LogLevel::kError);
+  //EXPECT_THAT(LOGGER->LastLogMessage(), HasSubstr("Method TrafficAreaAction::Step() not implemented yet (returning \"true\" by default)"));
+}
diff --git a/engine/tests/data/Scenarios/scenario_with_traffic_area_action.xosc b/engine/tests/data/Scenarios/scenario_with_traffic_area_action.xosc
new file mode 100644
index 00000000..caa81f9a
--- /dev/null
+++ b/engine/tests/data/Scenarios/scenario_with_traffic_area_action.xosc
@@ -0,0 +1,118 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<OpenSCENARIO>
+  <FileHeader revMajor="1" revMinor="3" date="2024-12-06T10:00:00"
+    description="TrafficAreaAction Test" author="BMW AG" />
+  <CatalogLocations>
+    <VehicleCatalog>
+      <Directory path="Vehicles" />
+    </VehicleCatalog>
+    <PedestrianCatalog>
+      <Directory path="Vehicles" />
+    </PedestrianCatalog>
+  </CatalogLocations>
+  <RoadNetwork>
+    <LogicFile filepath="SceneryConfiguration.xodr" />
+    <SceneGraphFile filepath="" />
+  </RoadNetwork>
+  <Entities>
+    <ScenarioObject name="Ego">
+      <CatalogReference catalogName="VehicleCatalog" entryName="car_mini_cooper" />
+      <ObjectController>
+        <Controller name="Ego">
+          <Properties>
+            <Property name="AgentProfile" value="MiddleClassCarAgent" />
+          </Properties>
+        </Controller>
+      </ObjectController>
+    </ScenarioObject>
+  </Entities>
+  <Storyboard>
+    <Init>
+      <Actions>
+        <GlobalAction>
+          <TrafficAction>
+            <!--
+            https://publications.pages.asam.net/standards/ASAM_OpenSCENARIO/ASAM_OpenSCENARIO_XML/latest/generated/content/TrafficAreaAction.html -->
+            <TrafficAreaAction continuous="false" numberOfEntities="100">
+              <TrafficDistribution>
+                <TrafficDistributionEntiry weight="1.0">
+                  <EntityDistribution>
+                    <EntityDistributionEntry weight="1.0">
+                      <ScenarioObjectTemplate>
+                        <CatalogReference catalogName="VehicleCatalog" entryName="car_mini_cooper" />
+                        <ObjectController>
+                          <Controller name="Common">
+                            <Properties>
+                              <Property name="AgentProfile" value="MiddleClassCarAgent" />
+                            </Properties>
+                          </Controller>
+                        </ObjectController>
+                      </ScenarioObjectTemplate>
+                    </EntityDistributionEntry>
+                  </EntityDistribution>
+                </TrafficDistributionEntiry>
+              </TrafficDistribution>
+              <TrafficArea>
+                <RoadRange>
+                  <RoadCursor roadId="road00" s="100" />
+                  <RoadCursor roadId="road00" s="500" />
+                </RoadRange>
+              </TrafficArea>
+            </TrafficAreaAction>
+          </TrafficAction>
+        </GlobalAction>
+        <Private entityRef="Ego">
+          <PrivateAction>
+            <TeleportAction>
+              <Position>
+                <LanePosition roadId="1" laneId="-1" offset="0.0" s="0.0">
+                  <Orientation type="relative" />
+                </LanePosition>
+              </Position>
+            </TeleportAction>
+          </PrivateAction>
+          <PrivateAction>
+            <LongitudinalAction>
+              <SpeedAction>
+                <SpeedActionDynamics dynamicsShape="step" value="0.0" dynamicsDimension="rate" />
+                <SpeedActionTarget>
+                  <AbsoluteTargetSpeed value="43.5" />
+                </SpeedActionTarget>
+              </SpeedAction>
+            </LongitudinalAction>
+          </PrivateAction>
+          <PrivateAction>
+            <ControllerAction>
+              <ActivateControllerAction controllerRef="Ego" lateral="true" longitudinal="true" />
+            </ControllerAction>
+          </PrivateAction>
+        </Private>
+      </Actions>
+    </Init>
+    <Story name="">
+      <Act name="">
+        <ManeuverGroup name="" maximumExecutionCount="1">
+          <Actors selectTriggeringEntities="false" />
+        </ManeuverGroup>
+        <StartTrigger>
+          <ConditionGroup>
+            <Condition name="StartTime" delay="0" conditionEdge="rising">
+              <ByValueCondition>
+                <SimulationTimeCondition value="0.0" rule="greaterOrEqual" />
+              </ByValueCondition>
+            </Condition>
+          </ConditionGroup>
+        </StartTrigger>
+      </Act>
+    </Story>
+    <StopTrigger>
+      <ConditionGroup>
+        <Condition name="EndTime" delay="0" conditionEdge="rising">
+          <ByValueCondition>
+            <SimulationTimeCondition value="30.0" rule="greaterThan" />
+          </ByValueCondition>
+        </Condition>
+      </ConditionGroup>
+    </StopTrigger>
+  </Storyboard>
+</OpenSCENARIO>
\ No newline at end of file
-- 
GitLab


From 626ae71c8b6531bc81c65d7458b706f18543e579 Mon Sep 17 00:00:00 2001
From: Noah Schick <noah.schick@in-tech.com>
Date: Wed, 12 Feb 2025 16:25:45 +0100
Subject: [PATCH 11/26] Fix build errors

---
 engine/cmake/generated_files.cmake            |  1 +
 engine/src/Utils/Spawning/Interval.h          |  6 +-
 engine/src/Utils/Spawning/SpawnSpace.h        |  9 +--
 engine/src/Utils/Spawning/Weight.cpp          | 57 +++++++++++++++++++
 engine/src/Utils/Spawning/Weight.h            | 37 ------------
 engine/src/Utils/Spawning/WeightedEntity.h    | 26 +++++++++
 .../GenericAction/TrafficAreaActionTest.cpp   | 19 ++++---
 7 files changed, 98 insertions(+), 57 deletions(-)
 create mode 100644 engine/src/Utils/Spawning/Weight.cpp
 create mode 100644 engine/src/Utils/Spawning/WeightedEntity.h

diff --git a/engine/cmake/generated_files.cmake b/engine/cmake/generated_files.cmake
index 5e4d2eb2..1c4c3ab5 100644
--- a/engine/cmake/generated_files.cmake
+++ b/engine/cmake/generated_files.cmake
@@ -238,6 +238,7 @@ list(APPEND ${PROJECT_NAME}_SOURCES
     src/Utils/Spawning/SpawnSpace.cpp
     src/Utils/Spawning/SpawnZone.cpp
     src/Utils/Spawning/VelocityRange.cpp
+    src/Utils/Spawning/Weight.cpp
     src/Utils/ConditionEdgeEvaluator.cpp
     src/Utils/ControllerCreator.cpp
     src/Utils/ControllerService.cpp
diff --git a/engine/src/Utils/Spawning/Interval.h b/engine/src/Utils/Spawning/Interval.h
index 1f16640c..f1378417 100644
--- a/engine/src/Utils/Spawning/Interval.h
+++ b/engine/src/Utils/Spawning/Interval.h
@@ -127,12 +127,12 @@ std::vector<Interval> GetFreeIntervals(const mantle_api::ITrafficAreaStream& str
     auto entity{pointer.lock()};
     const auto length{entity->GetProperties()->bounding_box.dimension.length};
     const auto offset{entity->GetProperties()->bounding_box.geometric_center.x};
-    auto [s, t]{stream.Convert(entity->GetPosition()).value()};
-    if (const auto entityStartDistance{s + offset - length / units::dimensionless::scalar_t{2.0}}; entityStartDistance > min)
+    auto stream_pose{stream.Convert(mantle_api::Pose{entity->GetPosition(), entity->GetOrientation()}).value()};
+    if (const auto entityStartDistance{stream_pose.s + offset - length / units::dimensionless::scalar_t{2.0}}; entityStartDistance > min)
     {
       EmplaceInterval(result, {min, entityStartDistance}, filter);
     }
-    min = s + offset + length / units::dimensionless::scalar_t{2.0};
+    min = stream_pose.s + offset + length / units::dimensionless::scalar_t{2.0};
     // TODO: This would loop endlessly if the object has a length of 0 and MIGHT loop endlessly if its offset lies outside of its bounds.
   }
   return result;
diff --git a/engine/src/Utils/Spawning/SpawnSpace.h b/engine/src/Utils/Spawning/SpawnSpace.h
index 2b07296f..9d1404d3 100644
--- a/engine/src/Utils/Spawning/SpawnSpace.h
+++ b/engine/src/Utils/Spawning/SpawnSpace.h
@@ -25,17 +25,10 @@
 #include "SpawnSpot.h"
 #include "SpawnZone.h"
 #include "VelocityRange.h"
+#include "WeightedEntity.h"
 
 namespace OpenScenarioEngine::v1_3
 {
-using Entity = typename std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IEntityObject>;
-
-struct WeightedEntity
-{
-  Entity handle;
-  units::dimensionless::scalar_t weight;  // Product of the distribution's weight and the weight of this entity's handle
-};
-
 /// A SpawnSpace is essentially a self-managing, depleting list of TrafficAreas and weighted entities optimized
 /// for the use-case of spawning entities at the rear end of a TrafficArea (with offsets). It restructures the
 /// data to be sorted by interval and entity length, allowing efficient filtering of remaining spawnable entities
diff --git a/engine/src/Utils/Spawning/Weight.cpp b/engine/src/Utils/Spawning/Weight.cpp
new file mode 100644
index 00000000..5d45041a
--- /dev/null
+++ b/engine/src/Utils/Spawning/Weight.cpp
@@ -0,0 +1,57 @@
+/********************************************************************************
+ * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+#include "Weight.h"
+
+#include "WeightedEntity.h"
+
+namespace OpenScenarioEngine::v1_3
+{
+units::dimensionless::scalar_t ToWeight::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IScenarioObjectTemplate& object) const
+{
+  return ToWeight{}(object.GetEntitiyObject());  // https://github.com/RA-Consulting-GmbH/openscenario.api.test/issues/222
+}
+
+units::dimensionless::scalar_t ToWeight::operator()(const NET_ASAM_OPENSCENARIO::v1_3::ITrafficDistributionEntry& entry) const
+{
+  return units::dimensionless::scalar_t{entry.GetWeight()};
+}
+
+units::dimensionless::scalar_t ToWeight::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IEntityObject& object) const
+{
+  return object.GetVehicle() ? ToWeight{}(object.GetVehicle()) : ToWeight{}(object.GetPedestrian());
+}
+
+units::dimensionless::scalar_t ToWeight::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IPedestrian& pedestrian) const
+{
+  return ToWeight{}(pedestrian.GetProperties());
+}
+
+units::dimensionless::scalar_t ToWeight::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IVehicle& vehicle) const
+{
+  return ToWeight{}(vehicle.GetProperties());
+}
+
+units::dimensionless::scalar_t ToWeight::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IProperties& properties) const
+{
+  const auto& entries{properties.GetProperties()};
+  auto match{std::find_if(entries.begin(), entries.end(), [](const std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IProperty>& entry)
+                          { return entry->GetName() == "Weight"; })};
+  if (match != entries.end())
+  {
+    return units::dimensionless::scalar_t{std::stod((*match)->GetValue())};
+  }
+  throw std::runtime_error("ToWeight: 'Weight' not found in properties");
+}
+
+units::dimensionless::scalar_t ToWeight::operator()(const WeightedEntity& entity) const
+{
+  return ToWeight{}(entity.handle);
+}
+}  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/Weight.h b/engine/src/Utils/Spawning/Weight.h
index b068658b..2e003f12 100644
--- a/engine/src/Utils/Spawning/Weight.h
+++ b/engine/src/Utils/Spawning/Weight.h
@@ -72,43 +72,6 @@ units::dimensionless::scalar_t GetWeight(Input&& input)
   return ToWeight{}(std::forward<Input>(input));
 }
 
-units::dimensionless::scalar_t ToWeight::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IScenarioObjectTemplate& object) const
-{
-  return ToWeight{}(object.GetEntitiyObject()); // https://github.com/RA-Consulting-GmbH/openscenario.api.test/issues/222
-}
-
-units::dimensionless::scalar_t ToWeight::operator()(const NET_ASAM_OPENSCENARIO::v1_3::ITrafficDistributionEntry& entry) const
-{
-  return units::dimensionless::scalar_t{entry.GetWeight()};
-}
-
-units::dimensionless::scalar_t ToWeight::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IEntityObject& object) const
-{
-  return object.GetVehicle() ? ToWeight{}(object.GetVehicle()) : ToWeight{}(object.GetPedestrian());
-}
-
-units::dimensionless::scalar_t ToWeight::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IPedestrian& pedestrian) const
-{
-  return ToWeight{}(pedestrian.GetProperties());
-}
-
-units::dimensionless::scalar_t ToWeight::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IVehicle& vehicle) const
-{
-  return ToWeight{}(vehicle.GetProperties());
-}
-
-units::dimensionless::scalar_t ToWeight::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IProperties& properties) const
-{
-  const auto& entries{properties.GetProperties()};
-  auto match{std::find_if(entries.begin(), entries.end(), [](const std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IProperty>& entry)
-                          { return entry->GetName() == "Weight"; })};
-  if (match != entries.end())
-  {
-    return units::dimensionless::scalar_t{std::stod((*match)->GetValue())};
-  }
-  throw std::runtime_error("ToWeight: 'Weight' not found in properties");
-}
-
 template <typename Entry>
 units::dimensionless::scalar_t ToAccumulatedWeight::operator()(const Entry& entry)
 {
diff --git a/engine/src/Utils/Spawning/WeightedEntity.h b/engine/src/Utils/Spawning/WeightedEntity.h
new file mode 100644
index 00000000..e17b8183
--- /dev/null
+++ b/engine/src/Utils/Spawning/WeightedEntity.h
@@ -0,0 +1,26 @@
+/********************************************************************************
+ * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+#pragma once
+
+#include <openScenarioLib/generated/v1_3/api/ApiClassInterfacesV1_3.h>
+#include <units.h>
+
+#include <memory>
+
+namespace OpenScenarioEngine::v1_3
+{
+using Entity = typename std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IEntityObject>;
+
+struct WeightedEntity
+{
+  Entity handle;
+  units::dimensionless::scalar_t weight;  // Product of the distribution's weight and the weight of this entity's handle
+};
+}  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp b/engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp
index 33317b2c..defa65af 100644
--- a/engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp
+++ b/engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp
@@ -16,19 +16,20 @@
 #include "TestUtils/TestLogger.h"
 
 using testing::_;
+using testing::HasSubstr;
 using testing::Return;
 using testing::ReturnRef;
-using testing::HasSubstr;
-using testing::OpenScenarioEngine::v1_3::OpenScenarioEngineTestBase;
 using testing::OpenScenarioEngine::v1_3::GetScenariosPath;
+using testing::OpenScenarioEngine::v1_3::OpenScenarioEngineTestBase;
 
 class TrafficAreaActionTestFixture : public OpenScenarioEngineTestBase
 {
 protected:
   void SetUp() override
-  { 
-    //LOGGER = std::make_unique<testing::OpenScenarioEngine::v1_3::TestLogger>();
-    //mock_environment_ = std::make_shared<mantle_api::MockEnvironment>();
+  {
+    OpenScenarioEngineTestBase::SetUp();
+    // LOGGER = std::make_unique<testing::OpenScenarioEngine::v1_3::TestLogger>();
+    // mock_environment_ = std::make_shared<mantle_api::MockEnvironment>();
     auto& mockTrafficAreaService{dynamic_cast<mantle_api::MockTrafficAreaService&>(env_->GetTrafficAreaService())};
     ON_CALL(mockTrafficAreaService, CreateTrafficArea(_)).WillByDefault([this]() -> std::vector<std::shared_ptr<mantle_api::ITrafficAreaStream>>
                                                                         {
@@ -37,9 +38,9 @@ protected:
       return traffic_area_stream_vec_; });
   }
 
-  //std::shared_ptr<mantle_api::MockEnvironment> mock_environment_;
+  // std::shared_ptr<mantle_api::MockEnvironment> mock_environment_;
   std::vector<std::shared_ptr<mantle_api::ITrafficAreaStream>> traffic_area_stream_vec_;
-  //std::unique_ptr<testing::OpenScenarioEngine::v1_3::TestLogger> LOGGER;
+  // std::unique_ptr<testing::OpenScenarioEngine::v1_3::TestLogger> LOGGER;
 };
 
 TEST_F(TrafficAreaActionTestFixture, GivenScenarioWithTrafficAreaAction_WhenInitScenario_Then)
@@ -48,6 +49,6 @@ TEST_F(TrafficAreaActionTestFixture, GivenScenarioWithTrafficAreaAction_WhenInit
   OpenScenarioEngine::v1_3::OpenScenarioEngine engine(xosc_file_path, env_);
   engine.Init();
 
-  //EXPECT_THAT(LOGGER->LastLogLevel(), mantle_api::LogLevel::kError);
-  //EXPECT_THAT(LOGGER->LastLogMessage(), HasSubstr("Method TrafficAreaAction::Step() not implemented yet (returning \"true\" by default)"));
+  // EXPECT_THAT(LOGGER->LastLogLevel(), mantle_api::LogLevel::kError);
+  // EXPECT_THAT(LOGGER->LastLogMessage(), HasSubstr("Method TrafficAreaAction::Step() not implemented yet (returning \"true\" by default)"));
 }
-- 
GitLab


From 57b90bbbbb2b5506936445982da85a0d006f967c Mon Sep 17 00:00:00 2001
From: Noah Schick <noah.schick@in-tech.com>
Date: Fri, 14 Feb 2025 15:56:33 +0100
Subject: [PATCH 12/26] Fix errors in TrafficAreaAction test configuration file

---
 .../GenericAction/TrafficAreaActionTest.cpp   |   4 +-
 .../scenario_with_traffic_area_action.xosc    | 117 ++++++++++++++++++
 .../scenario_with_traffic_area_action.xosc    |  11 +-
 3 files changed, 124 insertions(+), 8 deletions(-)
 create mode 100644 engine/tests/Storyboard/GenericAction/data/Scenarios/scenario_with_traffic_area_action.xosc

diff --git a/engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp b/engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp
index defa65af..59bfdd93 100644
--- a/engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp
+++ b/engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp
@@ -28,7 +28,7 @@ protected:
   void SetUp() override
   {
     OpenScenarioEngineTestBase::SetUp();
-    // LOGGER = std::make_unique<testing::OpenScenarioEngine::v1_3::TestLogger>();
+    LOGGER = std::make_unique<testing::OpenScenarioEngine::v1_3::TestLogger>();
     // mock_environment_ = std::make_shared<mantle_api::MockEnvironment>();
     auto& mockTrafficAreaService{dynamic_cast<mantle_api::MockTrafficAreaService&>(env_->GetTrafficAreaService())};
     ON_CALL(mockTrafficAreaService, CreateTrafficArea(_)).WillByDefault([this]() -> std::vector<std::shared_ptr<mantle_api::ITrafficAreaStream>>
@@ -40,7 +40,7 @@ protected:
 
   // std::shared_ptr<mantle_api::MockEnvironment> mock_environment_;
   std::vector<std::shared_ptr<mantle_api::ITrafficAreaStream>> traffic_area_stream_vec_;
-  // std::unique_ptr<testing::OpenScenarioEngine::v1_3::TestLogger> LOGGER;
+  std::shared_ptr<testing::OpenScenarioEngine::v1_3::TestLogger> LOGGER;
 };
 
 TEST_F(TrafficAreaActionTestFixture, GivenScenarioWithTrafficAreaAction_WhenInitScenario_Then)
diff --git a/engine/tests/Storyboard/GenericAction/data/Scenarios/scenario_with_traffic_area_action.xosc b/engine/tests/Storyboard/GenericAction/data/Scenarios/scenario_with_traffic_area_action.xosc
new file mode 100644
index 00000000..f83d832a
--- /dev/null
+++ b/engine/tests/Storyboard/GenericAction/data/Scenarios/scenario_with_traffic_area_action.xosc
@@ -0,0 +1,117 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<OpenSCENARIO>
+  <FileHeader revMajor="1" revMinor="3" date="2024-12-06T10:00:00" description="TrafficAreaAction Test" author="BMW AG" />
+  <CatalogLocations>
+    <VehicleCatalog>
+      <Directory path="Vehicles" />
+    </VehicleCatalog>
+    <PedestrianCatalog>
+      <Directory path="Vehicles" />
+    </PedestrianCatalog>
+  </CatalogLocations>
+  <RoadNetwork>
+    <LogicFile filepath="SceneryConfiguration.xodr" />
+    <SceneGraphFile filepath="" />
+  </RoadNetwork>
+  <Entities>
+    <ScenarioObject name="Ego">
+      <CatalogReference catalogName="VehicleCatalog" entryName="car_mini_cooper" />
+      <ObjectController name="Ego">
+        <Controller name="Ego">
+          <Properties>
+            <Property name="AgentProfile" value="MiddleClassCarAgent" />
+          </Properties>
+        </Controller>
+      </ObjectController>
+    </ScenarioObject>
+  </Entities>
+  <Storyboard>
+    <Init>
+      <Actions>
+        <GlobalAction>
+          <TrafficAction>
+            <!--
+            https://publications.pages.asam.net/standards/ASAM_OpenSCENARIO/ASAM_OpenSCENARIO_XML/latest/generated/content/TrafficAreaAction.html -->
+            <TrafficAreaAction continuous="false" numberOfEntities="100">
+              <TrafficDistribution>
+                <TrafficDistributionEntry weight="1.0">
+                  <EntityDistribution>
+                    <EntityDistributionEntry weight="1.0">
+                      <ScenarioObjectTemplate>
+                        <CatalogReference catalogName="VehicleCatalog" entryName="car_mini_cooper" />
+                        <ObjectController>
+                          <Controller name="Common">
+                            <Properties>
+                              <Property name="AgentProfile" value="MiddleClassCarAgent" />
+                            </Properties>
+                          </Controller>
+                        </ObjectController>
+                      </ScenarioObjectTemplate>
+                    </EntityDistributionEntry>
+                  </EntityDistribution>
+                </TrafficDistributionEntry>
+              </TrafficDistribution>
+              <TrafficArea>
+                <RoadRange>
+                  <RoadCursor roadId="road00" s="100" />
+                  <RoadCursor roadId="road00" s="500" />
+                </RoadRange>
+              </TrafficArea>
+            </TrafficAreaAction>
+          </TrafficAction>
+        </GlobalAction>
+        <Private entityRef="Ego">
+          <PrivateAction>
+            <TeleportAction>
+              <Position>
+                <LanePosition roadId="1" laneId="-1" offset="0.0" s="0.0">
+                  <Orientation type="relative" />
+                </LanePosition>
+              </Position>
+            </TeleportAction>
+          </PrivateAction>
+          <PrivateAction>
+            <LongitudinalAction>
+              <SpeedAction>
+                <SpeedActionDynamics dynamicsShape="step" value="0.0" dynamicsDimension="rate" />
+                <SpeedActionTarget>
+                  <AbsoluteTargetSpeed value="43.5" />
+                </SpeedActionTarget>
+              </SpeedAction>
+            </LongitudinalAction>
+          </PrivateAction>
+          <PrivateAction>
+            <ControllerAction>
+              <ActivateControllerAction objectControllerRef="Ego" lateral="true" longitudinal="true" />
+            </ControllerAction>
+          </PrivateAction>
+        </Private>
+      </Actions>
+    </Init>
+    <Story name="">
+      <Act name="">
+        <ManeuverGroup name="" maximumExecutionCount="1">
+          <Actors selectTriggeringEntities="false" />
+        </ManeuverGroup>
+        <StartTrigger>
+          <ConditionGroup>
+            <Condition name="StartTime" delay="0" conditionEdge="rising">
+              <ByValueCondition>
+                <SimulationTimeCondition value="0.0" rule="greaterOrEqual" />
+              </ByValueCondition>
+            </Condition>
+          </ConditionGroup>
+        </StartTrigger>
+      </Act>
+    </Story>
+    <StopTrigger>
+      <ConditionGroup>
+        <Condition name="EndTime" delay="0" conditionEdge="rising">
+          <ByValueCondition>
+            <SimulationTimeCondition value="30.0" rule="greaterThan" />
+          </ByValueCondition>
+        </Condition>
+      </ConditionGroup>
+    </StopTrigger>
+  </Storyboard>
+</OpenSCENARIO>
\ No newline at end of file
diff --git a/engine/tests/data/Scenarios/scenario_with_traffic_area_action.xosc b/engine/tests/data/Scenarios/scenario_with_traffic_area_action.xosc
index caa81f9a..f83d832a 100644
--- a/engine/tests/data/Scenarios/scenario_with_traffic_area_action.xosc
+++ b/engine/tests/data/Scenarios/scenario_with_traffic_area_action.xosc
@@ -1,7 +1,6 @@
 <?xml version='1.0' encoding='UTF-8'?>
 <OpenSCENARIO>
-  <FileHeader revMajor="1" revMinor="3" date="2024-12-06T10:00:00"
-    description="TrafficAreaAction Test" author="BMW AG" />
+  <FileHeader revMajor="1" revMinor="3" date="2024-12-06T10:00:00" description="TrafficAreaAction Test" author="BMW AG" />
   <CatalogLocations>
     <VehicleCatalog>
       <Directory path="Vehicles" />
@@ -17,7 +16,7 @@
   <Entities>
     <ScenarioObject name="Ego">
       <CatalogReference catalogName="VehicleCatalog" entryName="car_mini_cooper" />
-      <ObjectController>
+      <ObjectController name="Ego">
         <Controller name="Ego">
           <Properties>
             <Property name="AgentProfile" value="MiddleClassCarAgent" />
@@ -35,7 +34,7 @@
             https://publications.pages.asam.net/standards/ASAM_OpenSCENARIO/ASAM_OpenSCENARIO_XML/latest/generated/content/TrafficAreaAction.html -->
             <TrafficAreaAction continuous="false" numberOfEntities="100">
               <TrafficDistribution>
-                <TrafficDistributionEntiry weight="1.0">
+                <TrafficDistributionEntry weight="1.0">
                   <EntityDistribution>
                     <EntityDistributionEntry weight="1.0">
                       <ScenarioObjectTemplate>
@@ -50,7 +49,7 @@
                       </ScenarioObjectTemplate>
                     </EntityDistributionEntry>
                   </EntityDistribution>
-                </TrafficDistributionEntiry>
+                </TrafficDistributionEntry>
               </TrafficDistribution>
               <TrafficArea>
                 <RoadRange>
@@ -83,7 +82,7 @@
           </PrivateAction>
           <PrivateAction>
             <ControllerAction>
-              <ActivateControllerAction controllerRef="Ego" lateral="true" longitudinal="true" />
+              <ActivateControllerAction objectControllerRef="Ego" lateral="true" longitudinal="true" />
             </ControllerAction>
           </PrivateAction>
         </Private>
-- 
GitLab


From f4a32a34594a8629257eea15857a0533233c976a Mon Sep 17 00:00:00 2001
From: Noah Schick <noah.schick@in-tech.com>
Date: Tue, 4 Mar 2025 17:21:35 +0100
Subject: [PATCH 13/26] Advance TrafficAreaAction test

---
 engine/CMakeLists.txt                         |  4 +-
 .../ConvertScenarioTrafficArea.cpp            |  6 +-
 engine/src/Utils/Spawning/Weight.cpp          | 19 ++++-
 engine/src/Utils/Spawning/Weight.h            |  2 +
 .../GenericAction/TrafficAreaActionTest.cpp   | 13 +++-
 .../data/Scenarios/VehicleCatalog.xosc        | 20 +++++
 .../scenario_with_traffic_area_action.xosc    | 78 ++-----------------
 7 files changed, 64 insertions(+), 78 deletions(-)
 create mode 100644 engine/tests/Storyboard/GenericAction/data/Scenarios/VehicleCatalog.xosc

diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt
index 2b61c49a..c54e100d 100644
--- a/engine/CMakeLists.txt
+++ b/engine/CMakeLists.txt
@@ -75,9 +75,9 @@ target_include_directories(
 target_link_libraries(${PROJECT_NAME} PUBLIC antlr4_runtime::shared 
                                              MantleAPI::MantleAPI
                                              openscenario_api::shared 
+                                             Stochastics::Stochastics
                                              units::units 
-                                             Yase::agnostic_behavior_tree
-                                      PRIVATE Stochastics::Stochastics)
+                                             Yase::agnostic_behavior_tree)
 
 if(USE_CCACHE)
   find_program(CCACHE_FOUND ccache)
diff --git a/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficArea.cpp b/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficArea.cpp
index 6a22f0ef..1c13fcfd 100644
--- a/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficArea.cpp
+++ b/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficArea.cpp
@@ -44,10 +44,10 @@ struct ToRoadRange
 
 TrafficArea ConvertScenarioTrafficArea(const std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::ITrafficArea>& trafficArea)
 {
-  if (trafficArea->GetPolygon() != nullptr)
+  if (!trafficArea->GetRoadRange().empty())
   {
-    throw std::runtime_error("ConvertScenarioTrafficArea: Conversion of polygon not yet supported");
+    return TransformSharedPointers(trafficArea->GetRoadRange(), ToRoadRange{});
   }
-  return TransformSharedPointers(trafficArea->GetRoadRange(), ToRoadRange{});
+  throw std::runtime_error("ConvertScenarioTrafficArea: Conversion of polygon not yet supported");
 }
 }  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/Weight.cpp b/engine/src/Utils/Spawning/Weight.cpp
index 5d45041a..fdb7e4ec 100644
--- a/engine/src/Utils/Spawning/Weight.cpp
+++ b/engine/src/Utils/Spawning/Weight.cpp
@@ -25,7 +25,19 @@ units::dimensionless::scalar_t ToWeight::operator()(const NET_ASAM_OPENSCENARIO:
 
 units::dimensionless::scalar_t ToWeight::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IEntityObject& object) const
 {
-  return object.GetVehicle() ? ToWeight{}(object.GetVehicle()) : ToWeight{}(object.GetPedestrian());
+  if (object.IsSetVehicle())
+  {
+    return ToWeight{}(object.GetVehicle());
+  }
+  else if (object.IsSetPedestrian())
+  {
+    return ToWeight{}(object.GetPedestrian());
+  }
+  else if (object.IsSetMiscObject())
+  {
+    return ToWeight{}(object.GetMiscObject());
+  }
+  throw std::runtime_error("ToWeight: Invalid entity. Is neither a vehicle, pedestrian nor a misc. object");
 }
 
 units::dimensionless::scalar_t ToWeight::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IPedestrian& pedestrian) const
@@ -38,6 +50,11 @@ units::dimensionless::scalar_t ToWeight::operator()(const NET_ASAM_OPENSCENARIO:
   return ToWeight{}(vehicle.GetProperties());
 }
 
+units::dimensionless::scalar_t ToWeight::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IMiscObject& object) const
+{
+  return ToWeight{}(object.GetProperties());
+}
+
 units::dimensionless::scalar_t ToWeight::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IProperties& properties) const
 {
   const auto& entries{properties.GetProperties()};
diff --git a/engine/src/Utils/Spawning/Weight.h b/engine/src/Utils/Spawning/Weight.h
index 2e003f12..e5d83854 100644
--- a/engine/src/Utils/Spawning/Weight.h
+++ b/engine/src/Utils/Spawning/Weight.h
@@ -36,6 +36,8 @@ struct ToWeight
 
   units::dimensionless::scalar_t operator()(const NET_ASAM_OPENSCENARIO::v1_3::IVehicle&) const;
 
+  units::dimensionless::scalar_t operator()(const NET_ASAM_OPENSCENARIO::v1_3::IMiscObject&) const;
+
   units::dimensionless::scalar_t operator()(const NET_ASAM_OPENSCENARIO::v1_3::IProperties&) const;
 
   units::dimensionless::scalar_t operator()(const WeightedEntity&) const;
diff --git a/engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp b/engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp
index 59bfdd93..3212d377 100644
--- a/engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp
+++ b/engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp
@@ -9,8 +9,11 @@
  ********************************************************************************/
 
 #include <MantleAPI/Test/test_utils.h>
+#include <Storyboard/GenericAction/TrafficAreaAction.h>
 #include <gtest/gtest.h>
 
+#include <string>
+
 #include "OpenScenarioEngine/OpenScenarioEngine.h"
 #include "TestUtils.h"
 #include "TestUtils/TestLogger.h"
@@ -28,8 +31,9 @@ protected:
   void SetUp() override
   {
     OpenScenarioEngineTestBase::SetUp();
+    ON_CALL(controller_, GetName()).WillByDefault(ReturnRef(controller_name));
+    ON_CALL(controller_, GetUniqueId()).WillByDefault(Return(1234));
     LOGGER = std::make_unique<testing::OpenScenarioEngine::v1_3::TestLogger>();
-    // mock_environment_ = std::make_shared<mantle_api::MockEnvironment>();
     auto& mockTrafficAreaService{dynamic_cast<mantle_api::MockTrafficAreaService&>(env_->GetTrafficAreaService())};
     ON_CALL(mockTrafficAreaService, CreateTrafficArea(_)).WillByDefault([this]() -> std::vector<std::shared_ptr<mantle_api::ITrafficAreaStream>>
                                                                         {
@@ -38,9 +42,10 @@ protected:
       return traffic_area_stream_vec_; });
   }
 
-  // std::shared_ptr<mantle_api::MockEnvironment> mock_environment_;
   std::vector<std::shared_ptr<mantle_api::ITrafficAreaStream>> traffic_area_stream_vec_;
   std::shared_ptr<testing::OpenScenarioEngine::v1_3::TestLogger> LOGGER;
+
+  std::string controller_name{"TestController"};
 };
 
 TEST_F(TrafficAreaActionTestFixture, GivenScenarioWithTrafficAreaAction_WhenInitScenario_Then)
@@ -48,6 +53,10 @@ TEST_F(TrafficAreaActionTestFixture, GivenScenarioWithTrafficAreaAction_WhenInit
   std::string xosc_file_path{GetScenariosPath(test_info_->file()) + "scenario_with_traffic_area_action.xosc"};
   OpenScenarioEngine::v1_3::OpenScenarioEngine engine(xosc_file_path, env_);
   engine.Init();
+  engine.SetupDynamicContent();
+  engine.Step({});
+
+  // OpenScenarioEngine::v1_3::TrafficAreaAction action;
 
   // EXPECT_THAT(LOGGER->LastLogLevel(), mantle_api::LogLevel::kError);
   // EXPECT_THAT(LOGGER->LastLogMessage(), HasSubstr("Method TrafficAreaAction::Step() not implemented yet (returning \"true\" by default)"));
diff --git a/engine/tests/Storyboard/GenericAction/data/Scenarios/VehicleCatalog.xosc b/engine/tests/Storyboard/GenericAction/data/Scenarios/VehicleCatalog.xosc
new file mode 100644
index 00000000..c1510b1e
--- /dev/null
+++ b/engine/tests/Storyboard/GenericAction/data/Scenarios/VehicleCatalog.xosc
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<OpenSCENARIO>
+  <FileHeader revMajor="1" revMinor="1" date="2025-02-26T00:00:00" author="BMW AG" description="Vehicle catalog" />
+  <Catalog name="VehicleCatalog">
+    <Vehicle name="my_car" vehicleCategory="car" model3d="medium_car">
+      <BoundingBox>
+        <Center x="1.4" y="0.0" z="0.9" />
+        <Dimensions width="2.0" length="5.0" height="1.8" />
+      </BoundingBox>
+      <Performance maxSpeed="70" maxDeceleration="10" maxAcceleration="10" />
+      <Axles>
+        <FrontAxle maxSteering="0.5" wheelDiameter="0.8" trackWidth="1.68" positionX="2.98" positionZ="0.4" />
+        <RearAxle maxSteering="0" wheelDiameter="0.8" trackWidth="1.68" positionX="0" positionZ="0.4" />
+      </Axles>
+      <Properties>
+        <Property name="custom_property" value="5" />
+      </Properties>
+    </Vehicle>
+  </Catalog>
+</OpenSCENARIO>
diff --git a/engine/tests/Storyboard/GenericAction/data/Scenarios/scenario_with_traffic_area_action.xosc b/engine/tests/Storyboard/GenericAction/data/Scenarios/scenario_with_traffic_area_action.xosc
index f83d832a..ef8d43c5 100644
--- a/engine/tests/Storyboard/GenericAction/data/Scenarios/scenario_with_traffic_area_action.xosc
+++ b/engine/tests/Storyboard/GenericAction/data/Scenarios/scenario_with_traffic_area_action.xosc
@@ -1,21 +1,18 @@
 <?xml version='1.0' encoding='UTF-8'?>
 <OpenSCENARIO>
-  <FileHeader revMajor="1" revMinor="3" date="2024-12-06T10:00:00" description="TrafficAreaAction Test" author="BMW AG" />
+  <FileHeader revMajor="1" revMinor="3" date="2025-02-26T00:00:00" description="TrafficAreaAction Test" author="BMW AG" />
   <CatalogLocations>
     <VehicleCatalog>
-      <Directory path="Vehicles" />
+      <Directory path="." />
     </VehicleCatalog>
-    <PedestrianCatalog>
-      <Directory path="Vehicles" />
-    </PedestrianCatalog>
   </CatalogLocations>
   <RoadNetwork>
-    <LogicFile filepath="SceneryConfiguration.xodr" />
+    <LogicFile filepath="RoadNetwork.xodr" />
     <SceneGraphFile filepath="" />
   </RoadNetwork>
   <Entities>
     <ScenarioObject name="Ego">
-      <CatalogReference catalogName="VehicleCatalog" entryName="car_mini_cooper" />
+      <CatalogReference catalogName="VehicleCatalog" entryName="my_car" />
       <ObjectController name="Ego">
         <Controller name="Ego">
           <Properties>
@@ -30,22 +27,14 @@
       <Actions>
         <GlobalAction>
           <TrafficAction>
-            <!--
-            https://publications.pages.asam.net/standards/ASAM_OpenSCENARIO/ASAM_OpenSCENARIO_XML/latest/generated/content/TrafficAreaAction.html -->
+            <!-- https://publications.pages.asam.net/standards/ASAM_OpenSCENARIO/ASAM_OpenSCENARIO_XML/latest/generated/content/TrafficAreaAction.html -->
             <TrafficAreaAction continuous="false" numberOfEntities="100">
               <TrafficDistribution>
                 <TrafficDistributionEntry weight="1.0">
                   <EntityDistribution>
                     <EntityDistributionEntry weight="1.0">
                       <ScenarioObjectTemplate>
-                        <CatalogReference catalogName="VehicleCatalog" entryName="car_mini_cooper" />
-                        <ObjectController>
-                          <Controller name="Common">
-                            <Properties>
-                              <Property name="AgentProfile" value="MiddleClassCarAgent" />
-                            </Properties>
-                          </Controller>
-                        </ObjectController>
+                        <CatalogReference catalogName="VehicleCatalog" entryName="my_car" />
                       </ScenarioObjectTemplate>
                     </EntityDistributionEntry>
                   </EntityDistribution>
@@ -53,65 +42,14 @@
               </TrafficDistribution>
               <TrafficArea>
                 <RoadRange>
-                  <RoadCursor roadId="road00" s="100" />
-                  <RoadCursor roadId="road00" s="500" />
+                  <RoadCursor roadId="DOES_NOT_EXIST" s="100" />
+                  <RoadCursor roadId="DOES_NOT_EXIST" s="500" />
                 </RoadRange>
               </TrafficArea>
             </TrafficAreaAction>
           </TrafficAction>
         </GlobalAction>
-        <Private entityRef="Ego">
-          <PrivateAction>
-            <TeleportAction>
-              <Position>
-                <LanePosition roadId="1" laneId="-1" offset="0.0" s="0.0">
-                  <Orientation type="relative" />
-                </LanePosition>
-              </Position>
-            </TeleportAction>
-          </PrivateAction>
-          <PrivateAction>
-            <LongitudinalAction>
-              <SpeedAction>
-                <SpeedActionDynamics dynamicsShape="step" value="0.0" dynamicsDimension="rate" />
-                <SpeedActionTarget>
-                  <AbsoluteTargetSpeed value="43.5" />
-                </SpeedActionTarget>
-              </SpeedAction>
-            </LongitudinalAction>
-          </PrivateAction>
-          <PrivateAction>
-            <ControllerAction>
-              <ActivateControllerAction objectControllerRef="Ego" lateral="true" longitudinal="true" />
-            </ControllerAction>
-          </PrivateAction>
-        </Private>
       </Actions>
     </Init>
-    <Story name="">
-      <Act name="">
-        <ManeuverGroup name="" maximumExecutionCount="1">
-          <Actors selectTriggeringEntities="false" />
-        </ManeuverGroup>
-        <StartTrigger>
-          <ConditionGroup>
-            <Condition name="StartTime" delay="0" conditionEdge="rising">
-              <ByValueCondition>
-                <SimulationTimeCondition value="0.0" rule="greaterOrEqual" />
-              </ByValueCondition>
-            </Condition>
-          </ConditionGroup>
-        </StartTrigger>
-      </Act>
-    </Story>
-    <StopTrigger>
-      <ConditionGroup>
-        <Condition name="EndTime" delay="0" conditionEdge="rising">
-          <ByValueCondition>
-            <SimulationTimeCondition value="30.0" rule="greaterThan" />
-          </ByValueCondition>
-        </Condition>
-      </ConditionGroup>
-    </StopTrigger>
   </Storyboard>
 </OpenSCENARIO>
\ No newline at end of file
-- 
GitLab


From 537c4ad06b0de090c6eff92e7cde2da2cee677a2 Mon Sep 17 00:00:00 2001
From: Noah Schick <noah.schick@in-tech.com>
Date: Tue, 4 Mar 2025 17:21:59 +0100
Subject: [PATCH 14/26] Hotfix parser for RoadRange

---
 utils/ci/conan/recipe/openscenario_api/all/conanfile.py | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/utils/ci/conan/recipe/openscenario_api/all/conanfile.py b/utils/ci/conan/recipe/openscenario_api/all/conanfile.py
index dbe00a8f..4e4482b5 100644
--- a/utils/ci/conan/recipe/openscenario_api/all/conanfile.py
+++ b/utils/ci/conan/recipe/openscenario_api/all/conanfile.py
@@ -1,5 +1,5 @@
 ################################################################################
-# Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+# Copyright (c) 2023-2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
 #               2024 Volkswagen AG
 #
 # This program and the accompanying materials are made available under the
@@ -15,7 +15,7 @@
 import os
 
 from conan import ConanFile
-from conan.tools.files import copy, export_conandata_patches, apply_conandata_patches
+from conan.tools.files import apply_conandata_patches, copy, export_conandata_patches, replace_in_file
 from conan.tools.scm import Git
 
 required_conan_version = ">=1.53.0"
@@ -55,6 +55,10 @@ class OpenScenarioApiConan(ConanFile):
         self._repo_source = os.path.join(self.source_folder, self.name)
         self._artifact_path = os.path.join(self._repo_source, "cpp", "buildArtifact")
         apply_conandata_patches(self)
+        replace_in_file(self, 
+                        os.path.join(self._repo_source, "cpp/openScenarioLib/src/parser/modelgroup/XmlChoiceParser.cpp"), 
+                        "if (currentOccurs < parser->GetMaxOccur())", 
+                        "if (currentOccurs < parser->GetMaxOccur() || parser->GetMaxOccur() == -1)")
 
 
     def build(self):
-- 
GitLab


From dda49c1ebfd7262bcf4dc46249b9703afb80c3a4 Mon Sep 17 00:00:00 2001
From: Noah Schick <noah.schick@in-tech.com>
Date: Wed, 5 Mar 2025 13:55:15 +0100
Subject: [PATCH 15/26] Fix issues in TrafficAreaAction implementation

---
 engine/src/Utils/Spawning/Common.h            | 15 +++--------
 engine/src/Utils/Spawning/SpawnSpace.cpp      | 27 +++++++++++--------
 engine/src/Utils/Spawning/VelocityRange.cpp   | 19 ++++++++++++-
 engine/src/Utils/Spawning/VelocityRange.h     |  2 +-
 engine/src/Utils/Spawning/Weight.cpp          |  2 +-
 .../GenericAction/TrafficAreaActionTest.cpp   |  5 ----
 .../scenario_with_traffic_area_action.xosc    |  2 +-
 7 files changed, 41 insertions(+), 31 deletions(-)

diff --git a/engine/src/Utils/Spawning/Common.h b/engine/src/Utils/Spawning/Common.h
index eff3b449..4814e145 100644
--- a/engine/src/Utils/Spawning/Common.h
+++ b/engine/src/Utils/Spawning/Common.h
@@ -76,17 +76,10 @@ constexpr decltype(auto) BinaryOperation<Operation, Transformer>::operator()(A&&
 template <typename Elements, typename Thresholds>
 typename Elements::const_reference Sample(const Elements& elements, const Thresholds& thresholds, typename Thresholds::const_reference value)
 {
-  assert(elements.size() == thresholds.size());
-  const auto threshold{std::upper_bound(std::next(thresholds.begin()), thresholds.end(), value)};
-  const size_t index{static_cast<size_t>(std::distance(thresholds.begin(), threshold) - 1)};
+  assert(elements.size() <= thresholds.size());
+  const size_t index_offset{thresholds.size() - elements.size()};
+  const auto threshold{std::upper_bound(std::next(thresholds.begin(), index_offset), thresholds.end(), value)};
+  const size_t index{static_cast<size_t>(std::distance(thresholds.begin(), threshold) - index_offset)};
   return elements[index];
 }
-// template <typename Type>
-// std::pair<Type, size_t> Sample(const std::vector<Type>& range)
-// {
-//   const units::length::meter_t sample{range.back()};  // TODO: Use Stochastics
-//   const auto upperBound{std::upper_bound(range.begin(), std::prev(range.end()), sample)};
-//   const auto index{static_cast<size_t>(std::distance(range.begin(), upperBound))};
-//   return {sample, index};
-// }
 }  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/SpawnSpace.cpp b/engine/src/Utils/Spawning/SpawnSpace.cpp
index a1090f7b..35640587 100644
--- a/engine/src/Utils/Spawning/SpawnSpace.cpp
+++ b/engine/src/Utils/Spawning/SpawnSpace.cpp
@@ -20,6 +20,7 @@
 namespace OpenScenarioEngine::v1_3
 {
 SpawnSpace::SpawnSpace(const OpenScenarioEngine::v1_3::TrafficDistributions &distributions, std::vector<SpawnZones> &zones)
+    : weights{{}}
 {
   // Add entities
   {
@@ -29,20 +30,23 @@ SpawnSpace::SpawnSpace(const OpenScenarioEngine::v1_3::TrafficDistributions &dis
   for (auto &distribution : distributions)
   {
     std::transform(distribution.begin(), distribution.end(), std::back_inserter(entities),  //
-                   [multiplier = distribution.weight](const Entity &entity) -> WeightedEntity
-                   { return {entity, GetWeight(entity) * multiplier}; });
+                   [weight = distribution.weight](const Entity &entity) -> WeightedEntity
+                   { return {entity, weight}; });
   }
   std::sort(entities.begin(), entities.end(), Less<ToLength>{});
-  weights = Transform(entities, ToAccumulatedWeight{});
+  Transform(entities, weights, ToAccumulatedWeight{});
 
   // Add intervals
   for (const SpawnZones &regions : zones)
   {
     for (const SpawnZone &zone : regions)
     {
-      for (auto obstacle{std::next(zone.obstacles.begin())}; obstacle != zone.obstacles.end(); ++obstacle)
+      if (zone.obstacles.size() > 1)
       {
-        spots.emplace(*std::prev(obstacle), *obstacle);
+        for (auto obstacle{std::next(zone.obstacles.begin())}; obstacle != zone.obstacles.end(); ++obstacle)
+        {
+          spots.emplace(*std::prev(obstacle), *obstacle);
+        }
       }
     }
   }
@@ -52,9 +56,9 @@ Entity SpawnSpace::SampleEntity(StochasticsInterface &rng) const
 {
   const units::length::meter_t max_length{GetMaxIntervalLength()};
   const auto entity_end{FindSpawnableEntityEnd(max_length)};
-  if (entity_end == entities.begin())
+  if (std::distance(entities.begin(), entity_end) <= 1)
   {
-    return nullptr;
+    return entity_end == entities.begin() ? Entity{} : entities.front().handle;
   }
   assert(weights.size() == entities.size() + 1);
   const units::dimensionless::scalar_t max_weight{weights[static_cast<size_t>(std::distance(entities.begin(), entity_end))]};
@@ -64,7 +68,7 @@ Entity SpawnSpace::SampleEntity(StochasticsInterface &rng) const
 
 units::length::meter_t SpawnSpace::GetMaxIntervalLength() const
 {
-  return GetLength(*spots.rbegin());
+  return spots.empty() ? units::length::meter_t{} : GetLength(*spots.rbegin());
 }
 
 std::vector<WeightedEntity>::const_iterator SpawnSpace::FindSpawnableEntityEnd(units::length::meter_t upperBound) const
@@ -100,9 +104,10 @@ std::set<SpawnSpot, Less<ToLength>>::const_iterator SpawnSpace::SampleSpot(units
     return candidates.empty() ? spots.end() : *candidates.begin();
   }
   ToAccumulatedLength accumulator;
-  std::vector<units::length::meter_t> lengths{Transform(candidates,
-                                                        [&accumulator](const auto iterator)
-                                                        { return accumulator(*iterator); })};
+  std::vector<units::length::meter_t> lengths{{}};
+  Transform(candidates, lengths, [&accumulator](const auto iterator) {  //
+    return accumulator(*iterator);
+  });
   const units::length::meter_t sampledLength{rng.GetUniformDistributed(.0, lengths.back().value())};
   return Sample(candidates, lengths, sampledLength);
 }
diff --git a/engine/src/Utils/Spawning/VelocityRange.cpp b/engine/src/Utils/Spawning/VelocityRange.cpp
index a1326dac..d6bbd78c 100644
--- a/engine/src/Utils/Spawning/VelocityRange.cpp
+++ b/engine/src/Utils/Spawning/VelocityRange.cpp
@@ -17,7 +17,19 @@ namespace OpenScenarioEngine::v1_3
 {
 VelocityRange ToSpawnVelocityRange::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IEntityObject &object) const
 {
-  return object.GetVehicle() ? (*this)(object.GetVehicle()) : (*this)(object.GetPedestrian());
+  if (object.IsSetVehicle())
+  {
+    return (*this)(object.GetVehicle());
+  }
+  else if (object.IsSetPedestrian())
+  {
+    return (*this)(object.GetPedestrian());
+  }
+  else if (object.IsSetMiscObject())
+  {
+    return (*this)(object.GetMiscObject());
+  }
+  throw std::runtime_error("ToSpawmVelocityRange: Invalid entity. Is neither a vehicle, pedestrian or misc. object");
 }
 
 VelocityRange ToSpawnVelocityRange::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IVehicle &vehicle) const
@@ -30,6 +42,11 @@ VelocityRange ToSpawnVelocityRange::operator()(const NET_ASAM_OPENSCENARIO::v1_3
   return (*this)(pedestrian.GetProperties());
 }
 
+VelocityRange ToSpawnVelocityRange::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IMiscObject &object) const
+{
+  return (*this)(object.GetProperties());
+}
+
 VelocityRange ToSpawnVelocityRange::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IProperties &properties) const
 {
   const auto &entries{properties.GetProperties()};
diff --git a/engine/src/Utils/Spawning/VelocityRange.h b/engine/src/Utils/Spawning/VelocityRange.h
index e851efc0..180019f2 100644
--- a/engine/src/Utils/Spawning/VelocityRange.h
+++ b/engine/src/Utils/Spawning/VelocityRange.h
@@ -36,9 +36,9 @@ struct ToSpawnVelocityRange
   VelocityRange operator()(const std::shared_ptr<Type> &) const;
 
   VelocityRange operator()(const NET_ASAM_OPENSCENARIO::v1_3::IEntityObject &) const;
-  VelocityRange operator()(const NET_ASAM_OPENSCENARIO::v1_3::IMiscObject &) const;
   VelocityRange operator()(const NET_ASAM_OPENSCENARIO::v1_3::IVehicle &) const;
   VelocityRange operator()(const NET_ASAM_OPENSCENARIO::v1_3::IPedestrian &) const;
+  VelocityRange operator()(const NET_ASAM_OPENSCENARIO::v1_3::IMiscObject &) const;
   VelocityRange operator()(const NET_ASAM_OPENSCENARIO::v1_3::IProperties &) const;
 };
 
diff --git a/engine/src/Utils/Spawning/Weight.cpp b/engine/src/Utils/Spawning/Weight.cpp
index fdb7e4ec..96774fbc 100644
--- a/engine/src/Utils/Spawning/Weight.cpp
+++ b/engine/src/Utils/Spawning/Weight.cpp
@@ -69,6 +69,6 @@ units::dimensionless::scalar_t ToWeight::operator()(const NET_ASAM_OPENSCENARIO:
 
 units::dimensionless::scalar_t ToWeight::operator()(const WeightedEntity& entity) const
 {
-  return ToWeight{}(entity.handle);
+  return entity.weight;
 }
 }  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp b/engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp
index 3212d377..c9a2de01 100644
--- a/engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp
+++ b/engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp
@@ -55,9 +55,4 @@ TEST_F(TrafficAreaActionTestFixture, GivenScenarioWithTrafficAreaAction_WhenInit
   engine.Init();
   engine.SetupDynamicContent();
   engine.Step({});
-
-  // OpenScenarioEngine::v1_3::TrafficAreaAction action;
-
-  // EXPECT_THAT(LOGGER->LastLogLevel(), mantle_api::LogLevel::kError);
-  // EXPECT_THAT(LOGGER->LastLogMessage(), HasSubstr("Method TrafficAreaAction::Step() not implemented yet (returning \"true\" by default)"));
 }
diff --git a/engine/tests/Storyboard/GenericAction/data/Scenarios/scenario_with_traffic_area_action.xosc b/engine/tests/Storyboard/GenericAction/data/Scenarios/scenario_with_traffic_area_action.xosc
index ef8d43c5..191987e1 100644
--- a/engine/tests/Storyboard/GenericAction/data/Scenarios/scenario_with_traffic_area_action.xosc
+++ b/engine/tests/Storyboard/GenericAction/data/Scenarios/scenario_with_traffic_area_action.xosc
@@ -13,7 +13,7 @@
   <Entities>
     <ScenarioObject name="Ego">
       <CatalogReference catalogName="VehicleCatalog" entryName="my_car" />
-      <ObjectController name="Ego">
+      <ObjectController>
         <Controller name="Ego">
           <Properties>
             <Property name="AgentProfile" value="MiddleClassCarAgent" />
-- 
GitLab


From 0250f04e90edbc4934234d85e71bfed4fa0a422b Mon Sep 17 00:00:00 2001
From: Noah Schick <noah.schick@in-tech.com>
Date: Wed, 5 Mar 2025 15:04:42 +0100
Subject: [PATCH 16/26] Support catalog references in TrafficAreaAction

---
 engine/src/Utils/Spawning/Length.cpp          | 53 +++++++++++++++++--
 engine/src/Utils/Spawning/Length.h            |  8 +++
 engine/src/Utils/Spawning/VelocityRange.cpp   | 36 ++++++++++---
 engine/src/Utils/Spawning/VelocityRange.h     |  1 +
 .../data/Scenarios/VehicleCatalog.xosc        |  2 +
 5 files changed, 89 insertions(+), 11 deletions(-)

diff --git a/engine/src/Utils/Spawning/Length.cpp b/engine/src/Utils/Spawning/Length.cpp
index 8643a25f..03706dd3 100644
--- a/engine/src/Utils/Spawning/Length.cpp
+++ b/engine/src/Utils/Spawning/Length.cpp
@@ -10,6 +10,8 @@
 
 #include "Length.h"
 
+#include <openScenarioLib/generated/v1_3/catalog/CatalogHelperV1_3.h>
+
 #include <cassert>
 #include <numeric>
 
@@ -52,16 +54,57 @@ units::length::meter_t ToLength::operator()(const SpawnZones& zones) const
   return std::transform_reduce(zones.begin(), zones.end(), units::length::meter_t{}, std::plus<>{}, *this);
 }
 
+units::length::meter_t ToLength::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IBoundingBox& bounding_box) const
+{
+  return units::length::meter_t{bounding_box.GetDimensions()->GetLength()};
+}
+
+units::length::meter_t ToLength::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IVehicle& vehicle) const
+{
+  return (*this)(vehicle.GetBoundingBox());
+}
+
+units::length::meter_t ToLength::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IPedestrian& pedestrian) const
+{
+  return (*this)(pedestrian.GetBoundingBox());
+}
+
+units::length::meter_t ToLength::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IMiscObject& misc_object) const
+{
+  return (*this)(misc_object.GetBoundingBox());
+}
+
 units::length::meter_t ToLength::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IEntityObject& object) const
 {
-  if (decltype(auto) vehicle{object.GetVehicle()}; vehicle != nullptr)
+  if (object.IsSetCatalogReference())
+  {
+    const auto& catalog_reference{object.GetCatalogReference()->GetRef()};
+    auto& element{const_cast<std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::ICatalogElement>&>(catalog_reference)};
+    if (NET_ASAM_OPENSCENARIO::v1_3::CatalogHelper::IsVehicle(element))
+    {
+      return (*this)(NET_ASAM_OPENSCENARIO::v1_3::CatalogHelper::AsVehicle(element));
+    }
+    if (NET_ASAM_OPENSCENARIO::v1_3::CatalogHelper::IsPedestrian(element))
+    {
+      return (*this)(NET_ASAM_OPENSCENARIO::v1_3::CatalogHelper::AsPedestrian(element));
+    }
+    if (NET_ASAM_OPENSCENARIO::v1_3::CatalogHelper::IsMiscObject(element))
+    {
+      return (*this)(NET_ASAM_OPENSCENARIO::v1_3::CatalogHelper::AsMiscObject(element));
+    }
+  }
+  if (object.IsSetVehicle())
+  {
+    return (*this)(object.GetVehicle());
+  }
+  if (object.IsSetPedestrian())
   {
-    return units::length::meter_t{vehicle->GetBoundingBox()->GetDimensions()->GetLength()};
+    return (*this)(object.GetPedestrian());
   }
-  if (decltype(auto) pedestrian{object.GetPedestrian()}; pedestrian != nullptr)
+  if (object.IsSetMiscObject())
   {
-    return units::length::meter_t{pedestrian->GetBoundingBox()->GetDimensions()->GetLength()};
+    return (*this)(object.GetMiscObject());
   }
-  return units::length::meter_t{std::numeric_limits<double>::quiet_NaN()};
+  throw std::runtime_error("ToLength: Entity is not a catalog reference, vehicle, pedestrian or misc. object.");
 }
 }  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/Length.h b/engine/src/Utils/Spawning/Length.h
index 7c81c36f..e5c9702d 100644
--- a/engine/src/Utils/Spawning/Length.h
+++ b/engine/src/Utils/Spawning/Length.h
@@ -44,6 +44,14 @@ struct ToLength
 
   units::length::meter_t operator()(const mantle_api::EntityProperties&) const;
 
+  units::length::meter_t operator()(const NET_ASAM_OPENSCENARIO::v1_3::IBoundingBox&) const;
+
+  units::length::meter_t operator()(const NET_ASAM_OPENSCENARIO::v1_3::IVehicle&) const;
+
+  units::length::meter_t operator()(const NET_ASAM_OPENSCENARIO::v1_3::IPedestrian&) const;
+
+  units::length::meter_t operator()(const NET_ASAM_OPENSCENARIO::v1_3::IMiscObject&) const;
+
   template <typename Type>
   units::length::meter_t operator()(const Type*) const;
 
diff --git a/engine/src/Utils/Spawning/VelocityRange.cpp b/engine/src/Utils/Spawning/VelocityRange.cpp
index d6bbd78c..d3f03f50 100644
--- a/engine/src/Utils/Spawning/VelocityRange.cpp
+++ b/engine/src/Utils/Spawning/VelocityRange.cpp
@@ -10,6 +10,8 @@
 
 #include "VelocityRange.h"
 
+#include <openScenarioLib/generated/v1_3/catalog/CatalogHelperV1_3.h>
+
 #include <exception>
 #include <string>
 
@@ -17,21 +19,43 @@ namespace OpenScenarioEngine::v1_3
 {
 VelocityRange ToSpawnVelocityRange::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IEntityObject &object) const
 {
+  if (object.IsSetCatalogReference())
+  {
+    return (*this)(object.GetCatalogReference()->GetRef());
+  }
   if (object.IsSetVehicle())
   {
     return (*this)(object.GetVehicle());
   }
-  else if (object.IsSetPedestrian())
+  if (object.IsSetPedestrian())
   {
     return (*this)(object.GetPedestrian());
   }
-  else if (object.IsSetMiscObject())
+  if (object.IsSetMiscObject())
   {
     return (*this)(object.GetMiscObject());
   }
   throw std::runtime_error("ToSpawmVelocityRange: Invalid entity. Is neither a vehicle, pedestrian or misc. object");
 }
 
+VelocityRange ToSpawnVelocityRange::operator()(const std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::ICatalogElement> &input) const
+{
+  auto &element{const_cast<std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::ICatalogElement> &>(input)};
+  if (NET_ASAM_OPENSCENARIO::v1_3::CatalogHelper::IsVehicle(element))
+  {
+    return (*this)(NET_ASAM_OPENSCENARIO::v1_3::CatalogHelper::AsVehicle(element));
+  }
+  if (NET_ASAM_OPENSCENARIO::v1_3::CatalogHelper::IsPedestrian(element))
+  {
+    return (*this)(NET_ASAM_OPENSCENARIO::v1_3::CatalogHelper::AsPedestrian(element));
+  }
+  if (NET_ASAM_OPENSCENARIO::v1_3::CatalogHelper::IsMiscObject(element))
+  {
+    return (*this)(NET_ASAM_OPENSCENARIO::v1_3::CatalogHelper::AsMiscObject(element));
+  }
+  throw std::runtime_error("ToSpawnVelocityRange: Invalid catalog reference. Contains no vehicle, pedestrian or misc. object.");
+}
+
 VelocityRange ToSpawnVelocityRange::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IVehicle &vehicle) const
 {
   return (*this)(vehicle.GetProperties());
@@ -53,18 +77,18 @@ VelocityRange ToSpawnVelocityRange::operator()(const NET_ASAM_OPENSCENARIO::v1_3
   const auto min_match{std::find_if(entries.begin(),
                                     entries.end(),
                                     [](const std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IProperty> &entry)
-                                    { return entry->GetName() == "MinVelocity"; })};
+                                    { return entry->GetName() == "min_spawn_velocity"; })};
   if (min_match == entries.end())
   {
-    throw std::range_error("ToSpawnVelocityRange: 'MinVelocity' not found in properties");
+    throw std::range_error("ToSpawnVelocityRange: 'min_spawn_velocity' not found in properties");
   }
   const auto max_match{std::find_if(entries.begin(),
                                     entries.end(),
                                     [](const std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IProperty> &entry)
-                                    { return entry->GetName() == "MaxVelocity"; })};
+                                    { return entry->GetName() == "max_spawn_velocity"; })};
   if (max_match == entries.end())
   {
-    throw std::range_error("ToSpawnVelocityRange: 'MaxVelocity' not found in properties");
+    throw std::range_error("ToSpawnVelocityRange: 'max_spawn_velocity' not found in properties");
   }
   return {units::velocity::meters_per_second_t{std::stod((*min_match)->GetValue())}, units::velocity::meters_per_second_t{std::stod((*max_match)->GetValue())}};
 }
diff --git a/engine/src/Utils/Spawning/VelocityRange.h b/engine/src/Utils/Spawning/VelocityRange.h
index 180019f2..75d60e15 100644
--- a/engine/src/Utils/Spawning/VelocityRange.h
+++ b/engine/src/Utils/Spawning/VelocityRange.h
@@ -35,6 +35,7 @@ struct ToSpawnVelocityRange
   template <typename Type>
   VelocityRange operator()(const std::shared_ptr<Type> &) const;
 
+  VelocityRange operator()(const std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::ICatalogElement> &) const;
   VelocityRange operator()(const NET_ASAM_OPENSCENARIO::v1_3::IEntityObject &) const;
   VelocityRange operator()(const NET_ASAM_OPENSCENARIO::v1_3::IVehicle &) const;
   VelocityRange operator()(const NET_ASAM_OPENSCENARIO::v1_3::IPedestrian &) const;
diff --git a/engine/tests/Storyboard/GenericAction/data/Scenarios/VehicleCatalog.xosc b/engine/tests/Storyboard/GenericAction/data/Scenarios/VehicleCatalog.xosc
index c1510b1e..1314dc68 100644
--- a/engine/tests/Storyboard/GenericAction/data/Scenarios/VehicleCatalog.xosc
+++ b/engine/tests/Storyboard/GenericAction/data/Scenarios/VehicleCatalog.xosc
@@ -14,6 +14,8 @@
       </Axles>
       <Properties>
         <Property name="custom_property" value="5" />
+        <Property name="min_spawn_velocity" value="20" />
+        <Property name="max_spawn_velocity" value="80" />
       </Properties>
     </Vehicle>
   </Catalog>
-- 
GitLab


From bba1a777d04881a9038d6a7c965cbe3818b6e505 Mon Sep 17 00:00:00 2001
From: Noah Schick <noah.schick@in-tech.com>
Date: Thu, 6 Mar 2025 15:14:43 +0100
Subject: [PATCH 17/26] Add SequentialSpawnSpace

---
 .../GenericAction/TrafficAreaAction.cpp       |  2 +-
 engine/src/Utils/Spawning/Length.h            |  1 +
 engine/src/Utils/Spawning/SpawnSpace.cpp      | 62 ++++++++++++++-----
 engine/src/Utils/Spawning/SpawnSpace.h        | 61 +++++++++++++++---
 engine/src/Utils/Spawning/SpawnSpot.h         |  4 +-
 engine/src/Utils/Spawning/SpawnZone.cpp       | 28 ++++++++-
 engine/src/Utils/Spawning/SpawnZone.h         | 39 ++----------
 engine/src/Utils/Spawning/VelocityRange.cpp   |  5 ++
 engine/src/Utils/Spawning/VelocityRange.h     | 14 ++---
 .../GenericAction/TrafficAreaActionTest.cpp   |  7 ++-
 10 files changed, 150 insertions(+), 73 deletions(-)

diff --git a/engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp b/engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp
index 5a1bfbf5..fe3abb5e 100644
--- a/engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp
+++ b/engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp
@@ -63,7 +63,7 @@ bool TrafficAreaAction::Step()
   }
   EntityCreator spawner{mantle.environment};
   std::vector<SpawnZones> spawnZones{Transform(trafficAreas, ToSpawnZones{})};
-  SpawnSpace spawnSpace{values.trafficDistributions, spawnZones};
+  SequentialSpawnSpace spawnSpace{values.trafficDistributions, spawnZones};
   while (values.numberOfEntities)
   {
     if (!spawnSpace.Spawn(spawner, rng))
diff --git a/engine/src/Utils/Spawning/Length.h b/engine/src/Utils/Spawning/Length.h
index e5c9702d..20650dda 100644
--- a/engine/src/Utils/Spawning/Length.h
+++ b/engine/src/Utils/Spawning/Length.h
@@ -131,6 +131,7 @@ constexpr size_t ToSize::operator()(const std::vector<Type>& container) const
 template <typename Type>
 units::length::meter_t GetLength(const Type& item)
 {
+  std::cout << "Length: " << ToLength{}(item) << '\n';
   return ToLength{}(item);
 }
 
diff --git a/engine/src/Utils/Spawning/SpawnSpace.cpp b/engine/src/Utils/Spawning/SpawnSpace.cpp
index 35640587..95ff4952 100644
--- a/engine/src/Utils/Spawning/SpawnSpace.cpp
+++ b/engine/src/Utils/Spawning/SpawnSpace.cpp
@@ -35,24 +35,51 @@ SpawnSpace::SpawnSpace(const OpenScenarioEngine::v1_3::TrafficDistributions &dis
   }
   std::sort(entities.begin(), entities.end(), Less<ToLength>{});
   Transform(entities, weights, ToAccumulatedWeight{});
+}
 
-  // Add intervals
+SequentialSpawnSpace::SequentialSpawnSpace(const OpenScenarioEngine::v1_3::TrafficDistributions &distributions, std::vector<SpawnZones> &zones)
+    : SpawnSpace{distributions, zones}
+{  // clang-format off
+  std::transform(zones.begin(), zones.end(), std::back_inserter(spots), [](const SpawnZones &zones) {
+    std::vector<SpawnSpot> spots;
+    for(const SpawnZone& zone : zones) {
+      auto next_spots{zone.GetSpawnSpots()};
+      spots.insert(spots.end(), std::make_move_iterator(next_spots.begin()), std::make_move_iterator(next_spots.end()));
+    }
+    return spots;
+  });
+  spots.erase(std::remove_if(spots.begin(), spots.end(), [](const auto& container) { return container.empty(); }), spots.end());
+  // clang-format on
+}
+
+Entity SequentialSpawnSpace::SampleEntity(StochasticsInterface &rng) const
+{
+  if (entities.size() <= 1)
+  {
+    return entities.empty() ? Entity{} : entities.front().handle;
+  }
+  assert(weights.size() == entities.size() + 1);
+  const units::dimensionless::scalar_t sampled_weight{rng.GetUniformDistributed(.0, weights.back().value())};
+  return Sample(entities, weights, sampled_weight).handle;
+}
+
+DistributedSpawnSpace::DistributedSpawnSpace(const OpenScenarioEngine::v1_3::TrafficDistributions &distributions, std::vector<SpawnZones> &zones)
+    : SpawnSpace{distributions, zones}
+{
   for (const SpawnZones &regions : zones)
   {
     for (const SpawnZone &zone : regions)
     {
-      if (zone.obstacles.size() > 1)
+      assert(zone.obstacles.size() > 1);
+      for (auto obstacle{std::next(zone.obstacles.begin())}; obstacle != zone.obstacles.end(); ++obstacle)
       {
-        for (auto obstacle{std::next(zone.obstacles.begin())}; obstacle != zone.obstacles.end(); ++obstacle)
-        {
-          spots.emplace(*std::prev(obstacle), *obstacle);
-        }
+        spots.emplace(*std::prev(obstacle), *obstacle);
       }
     }
   }
 }
 
-Entity SpawnSpace::SampleEntity(StochasticsInterface &rng) const
+Entity DistributedSpawnSpace::SampleEntity(StochasticsInterface &rng) const
 {
   const units::length::meter_t max_length{GetMaxIntervalLength()};
   const auto entity_end{FindSpawnableEntityEnd(max_length)};
@@ -66,23 +93,23 @@ Entity SpawnSpace::SampleEntity(StochasticsInterface &rng) const
   return Sample(entities, weights, sampled_weight).handle;
 }
 
-units::length::meter_t SpawnSpace::GetMaxIntervalLength() const
+units::length::meter_t DistributedSpawnSpace::GetMaxIntervalLength() const
 {
   return spots.empty() ? units::length::meter_t{} : GetLength(*spots.rbegin());
 }
 
-std::vector<WeightedEntity>::const_iterator SpawnSpace::FindSpawnableEntityEnd(units::length::meter_t upperBound) const
+std::vector<WeightedEntity>::const_iterator DistributedSpawnSpace::FindSpawnableEntityEnd(units::length::meter_t threshold) const
 {
-  return std::upper_bound(entities.begin(), entities.end(), upperBound, [](units::length::meter_t upperBound, const WeightedEntity &entity)
-                          { return GetLength(entity) < upperBound; });
+  return std::upper_bound(entities.begin(), entities.end(), threshold, [](units::length::meter_t threshold, const WeightedEntity &entity)
+                          { return threshold < GetLength(entity); });
 }
-std::vector<WeightedEntity>::iterator SpawnSpace::FindSpawnableEntityEnd(units::length::meter_t upperBound)
+std::vector<WeightedEntity>::iterator DistributedSpawnSpace::FindSpawnableEntityEnd(units::length::meter_t threshold)
 {
-  return std::upper_bound(entities.begin(), entities.end(), upperBound, [](units::length::meter_t upperBound, const WeightedEntity &entity)
-                          { return GetLength(entity) < upperBound; });
+  return std::upper_bound(entities.begin(), entities.end(), threshold, [](units::length::meter_t threshold, const WeightedEntity &entity)
+                          { return threshold < GetLength(entity); });
 }
 
-std::set<SpawnSpot, Less<ToLength>>::const_iterator SpawnSpace::SampleSpot(units::length::meter_t minLength, VelocityRange velocity, StochasticsInterface &rng) const
+std::set<SpawnSpot, Less<ToLength>>::const_iterator DistributedSpawnSpace::SampleSpot(units::length::meter_t minLength, VelocityRange velocity, StochasticsInterface &rng) const
 {
   auto spot{spots.lower_bound(minLength)};
   if (spot == spots.end())
@@ -93,8 +120,9 @@ std::set<SpawnSpot, Less<ToLength>>::const_iterator SpawnSpace::SampleSpot(units
   for (; spot != spots.end(); ++spot)
   {
     const bool canKeepUp{spot->velocity.Contains(velocity)};
-    const bool isEnoughRoomAfter2Seconds{GetLength(*spot) - spot->velocity.GetDifference() * units::time::second_t{2.0} > minLength};
-    if (canKeepUp && isEnoughRoomAfter2Seconds)
+    const bool isEnoughRoomNow{GetLength(*spot) > minLength};
+    const bool isEnoughRoomAfter2Seconds{GetLength(*spot) + spot->velocity.GetDifference() * units::time::second_t{2.0} > minLength};
+    if (canKeepUp && isEnoughRoomNow && isEnoughRoomAfter2Seconds)
     {
       candidates.push_back(spot);
     }
diff --git a/engine/src/Utils/Spawning/SpawnSpace.h b/engine/src/Utils/Spawning/SpawnSpace.h
index 9d1404d3..28593c85 100644
--- a/engine/src/Utils/Spawning/SpawnSpace.h
+++ b/engine/src/Utils/Spawning/SpawnSpace.h
@@ -29,13 +29,46 @@
 
 namespace OpenScenarioEngine::v1_3
 {
-/// A SpawnSpace is essentially a self-managing, depleting list of TrafficAreas and weighted entities optimized
+struct SpawnSpace
+{
+  SpawnSpace(const OpenScenarioEngine::v1_3::TrafficDistributions &, std::vector<SpawnZones> &);
+
+  std::vector<WeightedEntity> entities;                 // Entities sorted by ascending length
+  std::vector<units::dimensionless::scalar_t> weights;  // Used to sample an entity
+};
+
+/// Space that spawns entities naively & greedily by design as defined here:
+/// https://eclipse.dev/openpass/content/html/user_guide/sim_user_guide/components/spawner.html#spawner
+/// The algorithm is greedy because it does not restrict what entities it samples from based on the available space. Instead, it
+/// samples a vehicle & velocity, then finds the first area it can be spawned in. If no such area exists, spawning is aborted. The
+/// algorithm is naive because no memoization of area sizes occurs, meaning all areas are tested anew when another entity is being spawned.
+struct SequentialSpawnSpace : SpawnSpace
+{
+  SequentialSpawnSpace(const OpenScenarioEngine::v1_3::TrafficDistributions &, std::vector<SpawnZones> &);
+
+  /// Samples and returns a random entity without considering the remaining space.
+  ///
+  /// \return Entity Random entity out of this space's entities. If it has none, a nullptr is returned.
+  Entity SampleEntity(StochasticsInterface &) const;
+
+  /// Tries to spawn an entity and returns whether spawning was successful.
+  /// If successful, also adjusts the spawn space's intervals.
+  ///
+  /// \tparam EntityCreator Provides CreateEntity(const std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IScenarioObject> &)
+  /// \param StochasticsInterface Used to sample the entity and its velocity
+  template <typename EntityCreator>
+  bool Spawn(EntityCreator &&, StochasticsInterface &);
+
+  std::vector<std::vector<SpawnSpot>> spots;
+};
+
+/// A DistributedSpawnSpace is essentially a self-managing, depleting list of TrafficAreas and weighted entities optimized
 /// for the use-case of spawning entities at the rear end of a TrafficArea (with offsets). It restructures the
 /// data to be sorted by interval and entity length, allowing efficient filtering of remaining spawnable entities
 /// and intervals as the space is filled.
-struct SpawnSpace
+struct DistributedSpawnSpace : SpawnSpace
 {
-  SpawnSpace(const OpenScenarioEngine::v1_3::TrafficDistributions &, std::vector<SpawnZones> &);
+  DistributedSpawnSpace(const OpenScenarioEngine::v1_3::TrafficDistributions &, std::vector<SpawnZones> &);
 
   /// Returns the entity out of the set of entities in this space based on the given sample
   ///
@@ -59,18 +92,28 @@ struct SpawnSpace
   std::vector<WeightedEntity>::iterator FindSpawnableEntityEnd(units::length::meter_t upper_bound);
 
   std::set<SpawnSpot, Less<ToLength>> spots;
-
-  std::vector<WeightedEntity> entities;                 // Entities sorted by ascending length
-  std::vector<units::dimensionless::scalar_t> weights;  // Used to sample an entity
 };
 
 }  // namespace OpenScenarioEngine::v1_3
 
 namespace OpenScenarioEngine::v1_3
 {
+template <typename EntityCreator>
+bool SequentialSpawnSpace::Spawn(EntityCreator &&, StochasticsInterface &rng)
+{
+  Entity entity{SampleEntity(rng)};
+  if (entity == nullptr)
+  {
+    return false;
+  }
+  const auto velocity_range{GetSpawnVelocityRange(entity)};
+  auto spawn_velocity{SampleVelocity(velocity_range, rng)};
+  // TODO
+  return true;
+}
 
 template <typename EntityCreator>
-bool SpawnSpace::Spawn(EntityCreator &&spawner, StochasticsInterface &rng)
+bool DistributedSpawnSpace::Spawn(EntityCreator &&spawner, StochasticsInterface &rng)
 {
   Entity entity{SampleEntity(rng)};
   if (entity == nullptr)
@@ -84,10 +127,12 @@ bool SpawnSpace::Spawn(EntityCreator &&spawner, StochasticsInterface &rng)
     return false;
   }
 
+  // TODO: Sample Velocity
+  //
   // TODO: Find last spawnable spot on the zone of the sampled interval
   // spawner.CreateEntity(*entity, place.zone->GetPosition(distance));
   // TODO: Remove/replace interval
-  // interval->zone->RemoveInterval(*interval->interval, sampler(restricted_velocity.min, restricted_velocity.max));
+  // spot->zone->RemoveInterval(*interval->interval, sampler(restricted_velocity.min, restricted_velocity.max));
   return true;
 }
 
diff --git a/engine/src/Utils/Spawning/SpawnSpot.h b/engine/src/Utils/Spawning/SpawnSpot.h
index 84804855..b476ba56 100644
--- a/engine/src/Utils/Spawning/SpawnSpot.h
+++ b/engine/src/Utils/Spawning/SpawnSpot.h
@@ -21,7 +21,7 @@ namespace OpenScenarioEngine::v1_3
 /// and a pointer to the spot's zone so that the SpawnSpace can look for the further spots within that zone.
 struct SpawnSpot : Interval
 {
-  constexpr SpawnSpot(Interval &&, VelocityRange &&);
+  constexpr SpawnSpot(Interval, VelocityRange);
 
   constexpr SpawnSpot(const Obstacle &before, const Obstacle &after);
 
@@ -31,7 +31,7 @@ struct SpawnSpot : Interval
 
 namespace OpenScenarioEngine::v1_3
 {
-constexpr SpawnSpot::SpawnSpot(Interval &&interval, VelocityRange &&velocity)
+constexpr SpawnSpot::SpawnSpot(Interval interval, VelocityRange velocity)
     : Interval{std::move(interval)}, velocity{std::move(velocity)} {}
 
 constexpr SpawnSpot::SpawnSpot(const Obstacle &before, const Obstacle &after)
diff --git a/engine/src/Utils/Spawning/SpawnZone.cpp b/engine/src/Utils/Spawning/SpawnZone.cpp
index d682f779..cba3f7b8 100644
--- a/engine/src/Utils/Spawning/SpawnZone.cpp
+++ b/engine/src/Utils/Spawning/SpawnZone.cpp
@@ -17,14 +17,33 @@
 
 namespace OpenScenarioEngine::v1_3
 {
-SpawnZone::SpawnZone(Interval&& interval, std::set<Obstacle, Less<ToMin>>&& obstacles)
-    : Interval{std::move(interval)}, obstacles{std::move(obstacles)} {}
+SpawnZone::SpawnZone(Interval interval, std::set<Obstacle, Less<ToMin>> obstacles)
+    : Interval{std::move(interval)}, obstacles{std::move(obstacles)}
+{
+  this->obstacles.emplace_hint(this->obstacles.begin(), Obstacle::GetStartingSentinel(min));
+  this->obstacles.emplace_hint(this->obstacles.end(), Obstacle::GetEndingSentinel(max));
+}
 
 SpawnZone::SpawnZone(const mantle_api::ITrafficAreaStream& stream)
     : SpawnZone{Interval{{}, ToLength{}(stream)}, GetObstacles(stream)}
 {
 }
 
+std::vector<SpawnSpot> SpawnZone::GetSpawnSpots() const
+{
+  std::vector<SpawnSpot> result;
+  assert(obstacles.size() > 1);
+  for (auto obstacle_after{std::next(obstacles.begin())}; obstacle_after != obstacles.end(); ++obstacle_after)
+  {
+    const auto obstacle_before{std::prev(obstacle_after)};
+    if (obstacle_before->max < obstacle_after->min)
+    {
+      result.emplace_back(*obstacle_before, *obstacle_after);
+    }
+  }
+  return result;
+}
+
 SpawnZones::SpawnZones(std::vector<SpawnZone> input)
     : Iterable<std::vector<SpawnZone>>{std::move(input)}
 {
@@ -49,4 +68,9 @@ SpawnZones ToSpawnZones::operator()(const mantle_api::TrafficArea& area) const
 {
   return {area};
 }
+
+std::vector<SpawnSpot> ToSpawnSpots::operator()(const SpawnZone& zone) const
+{
+  return zone.GetSpawnSpots();
+}
 }  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/SpawnZone.h b/engine/src/Utils/Spawning/SpawnZone.h
index 4f8336f9..0ae88e46 100644
--- a/engine/src/Utils/Spawning/SpawnZone.h
+++ b/engine/src/Utils/Spawning/SpawnZone.h
@@ -28,15 +28,14 @@ namespace OpenScenarioEngine::v1_3
 class SpawnZone : public Interval
 {
 public:
-  SpawnZone(Interval&&, std::set<Obstacle, Less<ToMin>>&&);
+  SpawnZone(Interval, std::set<Obstacle, Less<ToMin>>);
 
   SpawnZone(const mantle_api::ITrafficAreaStream&);
 
   const Interval& GetLongestInterval() const;
   Interval& GetLongestInterval();
 
-  template <typename Predicate>
-  std::optional<SpawnSpot> GetLastSpawnSpot(Predicate&&) const;
+  std::vector<SpawnSpot> GetSpawnSpots() const;
 
   std::set<Obstacle, Less<ToMin>> obstacles;
 };
@@ -59,35 +58,9 @@ struct ToSpawnZones
 {
   SpawnZones operator()(const mantle_api::TrafficArea&) const;
 };
-}  // namespace OpenScenarioEngine::v1_3
 
-namespace OpenScenarioEngine::v1_3
-{
-template <typename Predicate>
-std::optional<SpawnSpot> SpawnZone::GetLastSpawnSpot(Predicate&& pred) const
+struct ToSpawnSpots
 {
-  auto obstacle_before{std::find_if(obstacles.rbegin(),
-                                    obstacles.rend(),
-                                    [this](const Obstacle& obstacle)
-                                    { return obstacle.min < max; })};
-  if (obstacle_before == obstacles.rend())
-  {  // There are no obstacles
-    return pred(*this) ? std::optional<SpawnSpot>{*this, {}} : std::nullopt;
-  }
-  auto obstacle_after{obstacle_before++};
-  while (obstacle_before != obstacles.rend())
-  {
-    if (SpawnSpot candidate{*obstacle_before, *obstacle_after}; pred(candidate))
-    {  // Obstacle before and after the spawn spot
-      return std::move(candidate);
-    }
-    ++obstacle_before;
-    ++obstacle_after;
-  }
-  if (SpawnSpot candidate{Obstacle::GetStartingSentinel(min), *obstacle_after}; obstacle_after->min > min && pred(candidate))
-  {  // No obstacle before, but an obstacle after the spawn spot
-    return std::move(candidate);
-  }
-  return std::nullopt;
-}
-}  // namespace OpenScenarioEngine::v1_3
\ No newline at end of file
+  std::vector<SpawnSpot> operator()(const SpawnZone&) const;
+};
+}  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/VelocityRange.cpp b/engine/src/Utils/Spawning/VelocityRange.cpp
index d3f03f50..0190e588 100644
--- a/engine/src/Utils/Spawning/VelocityRange.cpp
+++ b/engine/src/Utils/Spawning/VelocityRange.cpp
@@ -92,4 +92,9 @@ VelocityRange ToSpawnVelocityRange::operator()(const NET_ASAM_OPENSCENARIO::v1_3
   }
   return {units::velocity::meters_per_second_t{std::stod((*min_match)->GetValue())}, units::velocity::meters_per_second_t{std::stod((*max_match)->GetValue())}};
 }
+
+units::velocity::meters_per_second_t SampleVelocity(const VelocityRange &range, StochasticsInterface &rng)
+{
+  return units::velocity::meters_per_second_t{rng.GetUniformDistributed(range.min.value(), range.max.value())};
+}
 }  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/VelocityRange.h b/engine/src/Utils/Spawning/VelocityRange.h
index 75d60e15..348f7d8b 100644
--- a/engine/src/Utils/Spawning/VelocityRange.h
+++ b/engine/src/Utils/Spawning/VelocityRange.h
@@ -9,6 +9,7 @@
  ********************************************************************************/
 #pragma once
 
+#include <Stochastics/StochasticsInterface.h>
 #include <units.h>
 
 #include <memory>
@@ -21,17 +22,16 @@ struct SpawnZone;
 
 struct VelocityRange
 {
-  units::velocity::meters_per_second_t min{std::numeric_limits<double>::lowest()}, max{std::numeric_limits<double>::max()};
-
   constexpr bool Contains(const VelocityRange &) const;
 
   constexpr units::velocity::meters_per_second_t GetDifference() const;
+
+  units::velocity::meters_per_second_t min{std::numeric_limits<double>::lowest()};
+  units::velocity::meters_per_second_t max{std::numeric_limits<double>::max()};
 };
 
 struct ToSpawnVelocityRange
 {
-  constexpr VelocityRange operator()(VelocityRange) const;
-
   template <typename Type>
   VelocityRange operator()(const std::shared_ptr<Type> &) const;
 
@@ -46,6 +46,7 @@ struct ToSpawnVelocityRange
 template <typename Input>
 VelocityRange GetSpawnVelocityRange(Input &&);
 
+units::velocity::meters_per_second_t SampleVelocity(const VelocityRange &, StochasticsInterface &);
 }  // namespace OpenScenarioEngine::v1_3
 
 namespace OpenScenarioEngine::v1_3
@@ -60,11 +61,6 @@ constexpr units::velocity::meters_per_second_t VelocityRange::GetDifference() co
   return max - min;
 }
 
-constexpr VelocityRange ToSpawnVelocityRange::operator()(VelocityRange input) const
-{
-  return input;
-}
-
 template <typename Type>
 VelocityRange ToSpawnVelocityRange::operator()(const std::shared_ptr<Type> &input) const
 {
diff --git a/engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp b/engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp
index c9a2de01..2c98045f 100644
--- a/engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp
+++ b/engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp
@@ -11,7 +11,9 @@
 #include <MantleAPI/Test/test_utils.h>
 #include <Storyboard/GenericAction/TrafficAreaAction.h>
 #include <gtest/gtest.h>
+#include <units.h>
 
+#include <memory>
 #include <string>
 
 #include "OpenScenarioEngine/OpenScenarioEngine.h"
@@ -24,6 +26,7 @@ using testing::Return;
 using testing::ReturnRef;
 using testing::OpenScenarioEngine::v1_3::GetScenariosPath;
 using testing::OpenScenarioEngine::v1_3::OpenScenarioEngineTestBase;
+using namespace units::literals;
 
 class TrafficAreaActionTestFixture : public OpenScenarioEngineTestBase
 {
@@ -35,15 +38,17 @@ protected:
     ON_CALL(controller_, GetUniqueId()).WillByDefault(Return(1234));
     LOGGER = std::make_unique<testing::OpenScenarioEngine::v1_3::TestLogger>();
     auto& mockTrafficAreaService{dynamic_cast<mantle_api::MockTrafficAreaService&>(env_->GetTrafficAreaService())};
+
+    ON_CALL(*mock_traffic_area_stream, GetLength()).WillByDefault(Return(500_m));
     ON_CALL(mockTrafficAreaService, CreateTrafficArea(_)).WillByDefault([this]() -> std::vector<std::shared_ptr<mantle_api::ITrafficAreaStream>>
                                                                         {
-      auto mock_traffic_area_stream{std::make_shared<mantle_api::MockTrafficAreaStream>()};
       traffic_area_stream_vec_.emplace_back(std::move(mock_traffic_area_stream));
       return traffic_area_stream_vec_; });
   }
 
   std::vector<std::shared_ptr<mantle_api::ITrafficAreaStream>> traffic_area_stream_vec_;
   std::shared_ptr<testing::OpenScenarioEngine::v1_3::TestLogger> LOGGER;
+  std::shared_ptr<mantle_api::MockTrafficAreaStream> mock_traffic_area_stream{std::make_shared<mantle_api::MockTrafficAreaStream>()};
 
   std::string controller_name{"TestController"};
 };
-- 
GitLab


From 600cf83bab609e145750e1ec17f5e8ab5906330e Mon Sep 17 00:00:00 2001
From: Noah Schick <noah.schick@in-tech.com>
Date: Fri, 7 Mar 2025 10:48:33 +0100
Subject: [PATCH 18/26] Implement spawning logic

---
 .../GenericAction/TrafficAreaAction.cpp       |  3 +-
 engine/src/Utils/Spawning/Interval.h          | 76 ++++++++++++++++---
 engine/src/Utils/Spawning/Length.h            |  1 -
 engine/src/Utils/Spawning/SpawnSpace.cpp      | 67 ++++++++++++----
 engine/src/Utils/Spawning/SpawnSpace.h        | 57 +++++++++++---
 engine/src/Utils/Spawning/SpawnZone.h         |  4 +
 engine/src/Utils/Spawning/Transform.h         | 28 +++++--
 engine/src/Utils/Spawning/VelocityRange.h     | 21 +++++
 .../GenericAction/TrafficAreaActionTest.cpp   |  2 +
 9 files changed, 214 insertions(+), 45 deletions(-)

diff --git a/engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp b/engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp
index fe3abb5e..b3518912 100644
--- a/engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp
+++ b/engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp
@@ -62,8 +62,7 @@ bool TrafficAreaAction::Step()
     }
   }
   EntityCreator spawner{mantle.environment};
-  std::vector<SpawnZones> spawnZones{Transform(trafficAreas, ToSpawnZones{})};
-  SequentialSpawnSpace spawnSpace{values.trafficDistributions, spawnZones};
+  SequentialSpawnSpace spawnSpace{trafficAreas, values.trafficDistributions};
   while (values.numberOfEntities)
   {
     if (!spawnSpace.Spawn(spawner, rng))
diff --git a/engine/src/Utils/Spawning/Interval.h b/engine/src/Utils/Spawning/Interval.h
index f1378417..1beb8aed 100644
--- a/engine/src/Utils/Spawning/Interval.h
+++ b/engine/src/Utils/Spawning/Interval.h
@@ -20,6 +20,16 @@
 
 namespace OpenScenarioEngine::v1_3
 {
+struct Padding
+{
+  constexpr units::length::meter_t GetLength() const;
+
+  constexpr Padding operator-() const;
+
+  units::length::meter_t rear;
+  units::length::meter_t front;
+};
+
 struct Interval
 {
   constexpr Interval(units::length::meter_t min, units::length::meter_t max)
@@ -30,11 +40,24 @@ struct Interval
   /// \return Difference between min and max
   constexpr units::length::meter_t GetLength() const;
 
-  /// Moves the min and max of this interval by the given offsets
+  /// Returns an interval that matches this interval moved by the given offsets.
+  ///
+  /// \param Padding Rear and front padding to be added to this interval
+  /// \return Modified copy of this interval
+  constexpr Interval OffsetBy(Padding) const;
+
+  /// Returns an interval that matches this interval moved by the given offsets.
+  ///
+  /// \param Interval Min and max offset to be added to this interval's respective limits
+  /// \return Modified copy of this interval
+  constexpr Interval OffsetBy(Interval) const;
+
+  /// Returns an interval that matches this interval moved by the given offsets.
   ///
-  /// \param frontOffset The amount to be added to min
-  /// \param rearOffset The amount to be added to max
-  constexpr void OffsetBy(units::length::meter_t frontOffset, units::length::meter_t rearOffset);
+  /// \param min_offset The amount to be added to min
+  /// \param max_offset The amount to be added to max
+  /// \return Modified copy of this interval
+  constexpr Interval OffsetBy(units::length::meter_t min_offset, units::length::meter_t max_offset) const;
 
   /// Returns the resulting list of intervals if the given interval were cut out of this interval.
   ///
@@ -42,14 +65,21 @@ struct Interval
   /// \return 0, 1 or 2 elements sorted by ascending (min/max) value.
   std::vector<Interval> SplitBy(Interval) const;
 
+  /// Returns the interval representing the intersection of this interval and the given interval.
+  /// If the intervals do not intersect, the inverted negative space between the intervals is returned instead.
+  ///
+  /// \param other Other interval
+  /// \return The intersection interval of this and the given other interval
+  constexpr Interval GetIntersection(const Interval& other) const;
+
   units::length::meter_t min, max;
 };
 
 struct OffsetBy
 {
-  constexpr void operator()(Interval&) const;
+  constexpr Interval operator()(const Interval&) const;
 
-  units::length::meter_t frontOffset, rearOffset;
+  Padding offset;
 };
 
 void OffsetIntervals(std::vector<Interval>& intervals, units::length::meter_t frontOffset, units::length::meter_t rearOffset);
@@ -74,20 +104,44 @@ struct ToMax
 
 namespace OpenScenarioEngine::v1_3
 {
+constexpr units::length::meter_t Padding::GetLength() const
+{
+  return rear + front;
+}
+
+constexpr Padding Padding::operator-() const
+{
+  return {-rear, -front};
+}
+
 constexpr units::length::meter_t Interval::GetLength() const
 {
   return max - min;
 }
 
-constexpr void Interval::OffsetBy(units::length::meter_t frontOffset, units::length::meter_t rearOffset)
+constexpr Interval Interval::GetIntersection(const Interval& other) const
+{
+  return {std::max(min, other.min), std::min(max, other.max)};
+}
+
+constexpr Interval Interval::OffsetBy(Padding padding) const
+{
+  return OffsetBy(padding.rear, padding.front);
+}
+
+constexpr Interval Interval::OffsetBy(Interval other) const
+{
+  return OffsetBy(other.min, other.max);
+}
+
+constexpr Interval Interval::OffsetBy(units::length::meter_t min_offset, units::length::meter_t max_offset) const
 {
-  min += frontOffset;
-  max += rearOffset;
+  return {min + min_offset, max + max_offset};
 }
 
-constexpr void OffsetBy::operator()(Interval& interval) const
+constexpr Interval OffsetBy::operator()(const Interval& interval) const
 {
-  interval.OffsetBy(frontOffset, rearOffset);
+  return interval.OffsetBy(offset);
 }
 
 template <typename Filter = Return<true>>
diff --git a/engine/src/Utils/Spawning/Length.h b/engine/src/Utils/Spawning/Length.h
index 20650dda..e5c9702d 100644
--- a/engine/src/Utils/Spawning/Length.h
+++ b/engine/src/Utils/Spawning/Length.h
@@ -131,7 +131,6 @@ constexpr size_t ToSize::operator()(const std::vector<Type>& container) const
 template <typename Type>
 units::length::meter_t GetLength(const Type& item)
 {
-  std::cout << "Length: " << ToLength{}(item) << '\n';
   return ToLength{}(item);
 }
 
diff --git a/engine/src/Utils/Spawning/SpawnSpace.cpp b/engine/src/Utils/Spawning/SpawnSpace.cpp
index 95ff4952..98ee93e6 100644
--- a/engine/src/Utils/Spawning/SpawnSpace.cpp
+++ b/engine/src/Utils/Spawning/SpawnSpace.cpp
@@ -19,7 +19,7 @@
 
 namespace OpenScenarioEngine::v1_3
 {
-SpawnSpace::SpawnSpace(const OpenScenarioEngine::v1_3::TrafficDistributions &distributions, std::vector<SpawnZones> &zones)
+SpawnSpace::SpawnSpace(const OpenScenarioEngine::v1_3::TrafficDistributions &distributions)
     : weights{{}}
 {
   // Add entities
@@ -37,19 +37,14 @@ SpawnSpace::SpawnSpace(const OpenScenarioEngine::v1_3::TrafficDistributions &dis
   Transform(entities, weights, ToAccumulatedWeight{});
 }
 
-SequentialSpawnSpace::SequentialSpawnSpace(const OpenScenarioEngine::v1_3::TrafficDistributions &distributions, std::vector<SpawnZones> &zones)
-    : SpawnSpace{distributions, zones}
-{  // clang-format off
-  std::transform(zones.begin(), zones.end(), std::back_inserter(spots), [](const SpawnZones &zones) {
-    std::vector<SpawnSpot> spots;
-    for(const SpawnZone& zone : zones) {
-      auto next_spots{zone.GetSpawnSpots()};
-      spots.insert(spots.end(), std::make_move_iterator(next_spots.begin()), std::make_move_iterator(next_spots.end()));
-    }
-    return spots;
-  });
-  spots.erase(std::remove_if(spots.begin(), spots.end(), [](const auto& container) { return container.empty(); }), spots.end());
-  // clang-format on
+SequentialSpawnSpace::SequentialSpawnSpace(const std::vector<mantle_api::TrafficArea> &areas, const OpenScenarioEngine::v1_3::TrafficDistributions &distributions)
+    : SpawnSpace{distributions}, areas{areas}
+{
+  std::vector<SpawnZones> zones{Transform(areas, ToSpawnZones{})};  // clang-format off
+  Transform(zones, spots, [](const SpawnZones &zones) {
+    assert(!zones.empty());
+    return Transform(zones, ToSpawnSpots{});
+  });  // clang-format on
 }
 
 Entity SequentialSpawnSpace::SampleEntity(StochasticsInterface &rng) const
@@ -63,8 +58,44 @@ Entity SequentialSpawnSpace::SampleEntity(StochasticsInterface &rng) const
   return Sample(entities, weights, sampled_weight).handle;
 }
 
+std::tuple<std::vector<std::vector<std::vector<SpawnSpot>>>::iterator,
+           std::vector<std::vector<SpawnSpot>>::iterator,
+           std::vector<SpawnSpot>::iterator,
+           Interval>
+SequentialSpawnSpace::FindSpot(const Padding &padding, units::velocity::meters_per_second_t velocity, units::time::second_t timespan)
+{
+  assert(!spots.empty());
+  for (auto area{spots.begin()}; area != spots.end(); ++area)
+  {
+    for (auto stream{area->begin()}; stream != area->end(); ++stream)
+    {
+      for (auto spot{stream->rbegin()}; spot != stream->rend(); ++spot)
+      {
+        Interval range{spot->OffsetBy(-padding)};
+        if (range.min <= range.max)
+        {
+          range = range.GetIntersection(spot->OffsetBy(spot->velocity * timespan));
+          if (range.min <= range.max)
+          {
+            return {area, stream, std::next(spot).base(), std::move(range)};
+          }
+        }
+      }
+    }
+  }
+  return {
+      spots.end(),
+      spots.back().end(),
+      spots.back().back().end(),
+      Interval{
+          units::length::meter_t{std::numeric_limits<double>::quiet_NaN()},
+          units::length::meter_t{std::numeric_limits<double>::quiet_NaN()}  //
+      }                                                                     //
+  };
+}
+
 DistributedSpawnSpace::DistributedSpawnSpace(const OpenScenarioEngine::v1_3::TrafficDistributions &distributions, std::vector<SpawnZones> &zones)
-    : SpawnSpace{distributions, zones}
+    : SpawnSpace{distributions}
 {
   for (const SpawnZones &regions : zones)
   {
@@ -139,4 +170,10 @@ std::set<SpawnSpot, Less<ToLength>>::const_iterator DistributedSpawnSpace::Sampl
   const units::length::meter_t sampledLength{rng.GetUniformDistributed(.0, lengths.back().value())};
   return Sample(candidates, lengths, sampledLength);
 }
+
+Padding GetLengthPadding(const Entity &entity)
+{  // TODO: Consider the center offset of the entity's bounds
+  const units::length::meter_t half_length{GetLength(entity) * units::dimensionless::scalar_t{.5}};
+  return {half_length, half_length};
+}
 }  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/SpawnSpace.h b/engine/src/Utils/Spawning/SpawnSpace.h
index 28593c85..5315c305 100644
--- a/engine/src/Utils/Spawning/SpawnSpace.h
+++ b/engine/src/Utils/Spawning/SpawnSpace.h
@@ -31,7 +31,7 @@ namespace OpenScenarioEngine::v1_3
 {
 struct SpawnSpace
 {
-  SpawnSpace(const OpenScenarioEngine::v1_3::TrafficDistributions &, std::vector<SpawnZones> &);
+  SpawnSpace(const OpenScenarioEngine::v1_3::TrafficDistributions &);
 
   std::vector<WeightedEntity> entities;                 // Entities sorted by ascending length
   std::vector<units::dimensionless::scalar_t> weights;  // Used to sample an entity
@@ -44,7 +44,7 @@ struct SpawnSpace
 /// algorithm is naive because no memoization of area sizes occurs, meaning all areas are tested anew when another entity is being spawned.
 struct SequentialSpawnSpace : SpawnSpace
 {
-  SequentialSpawnSpace(const OpenScenarioEngine::v1_3::TrafficDistributions &, std::vector<SpawnZones> &);
+  SequentialSpawnSpace(const std::vector<mantle_api::TrafficArea> &, const OpenScenarioEngine::v1_3::TrafficDistributions &);
 
   /// Samples and returns a random entity without considering the remaining space.
   ///
@@ -59,7 +59,15 @@ struct SequentialSpawnSpace : SpawnSpace
   template <typename EntityCreator>
   bool Spawn(EntityCreator &&, StochasticsInterface &);
 
-  std::vector<std::vector<SpawnSpot>> spots;
+  std::tuple<std::vector<std::vector<std::vector<SpawnSpot>>>::iterator,
+             std::vector<std::vector<SpawnSpot>>::iterator,
+             std::vector<SpawnSpot>::iterator,
+             Interval>
+  FindSpot(const Padding &, units::velocity::meters_per_second_t velocity, units::time::second_t timespan);
+
+  // Stream -> vector<SpawnSpot>, thus Area = vector<vector<SpawnSpot>>, thus vector<Area> = vector<vector<vector<SpawnSpot>>>
+  std::vector<std::vector<std::vector<SpawnSpot>>> spots;
+  const std::vector<mantle_api::TrafficArea> &areas;
 };
 
 /// A DistributedSpawnSpace is essentially a self-managing, depleting list of TrafficAreas and weighted entities optimized
@@ -94,13 +102,21 @@ struct DistributedSpawnSpace : SpawnSpace
   std::set<SpawnSpot, Less<ToLength>> spots;
 };
 
+/// Returns the positive distance from the entity's center to its rear and the positive distance from the entity's center to its front.
+Padding GetLengthPadding(const Entity &);
 }  // namespace OpenScenarioEngine::v1_3
 
 namespace OpenScenarioEngine::v1_3
 {
 template <typename EntityCreator>
-bool SequentialSpawnSpace::Spawn(EntityCreator &&, StochasticsInterface &rng)
+bool SequentialSpawnSpace::Spawn(EntityCreator &&spawner, StochasticsInterface &rng)
 {
+  using namespace units::literals;
+
+  if (spots.empty())
+  {
+    return false;
+  }
   Entity entity{SampleEntity(rng)};
   if (entity == nullptr)
   {
@@ -108,8 +124,33 @@ bool SequentialSpawnSpace::Spawn(EntityCreator &&, StochasticsInterface &rng)
   }
   const auto velocity_range{GetSpawnVelocityRange(entity)};
   auto spawn_velocity{SampleVelocity(velocity_range, rng)};
-  // TODO
-  return true;
+  const units::time::second_t minimum_time_to_collision{2_s};
+  const Padding padding{GetLengthPadding(entity)};
+  if (auto [spot_area, spot_stream, spot, interval]{FindSpot(padding, spawn_velocity, minimum_time_to_collision)}; spot_area != spots.end())
+  {
+    const auto area{std::next(areas.begin(), std::distance(spots.begin(), spot_area))};
+    const auto &stream{*std::next(area->begin(), std::distance(spot_area->begin(), spot_stream))};
+    const mantle_api::Pose pose{stream->Convert(mantle_api::ITrafficAreaStream::StreamPose{{interval.max, {}}, {}}).value()};
+
+    // TODO: Spawn entity
+
+    std::vector<OpenScenarioEngine::v1_3::Interval> new_intervals{spot->SplitBy(Interval{interval.max, interval.max}.OffsetBy(-padding.rear, padding.front))};
+    if (new_intervals.empty())
+    {
+      spot_stream->erase(spot);
+    }
+    else
+    {
+      std::vector<SpawnSpot> new_spots{Transform(std::move(new_intervals), [spot](Interval &&interval) -> SpawnSpot {  //
+        return {interval, spot->velocity};
+      })};
+      new_spots.front().velocity.max = spawn_velocity;
+      new_spots.back().velocity.min = spawn_velocity;
+      spot_stream->insert(spot_stream->erase(spot), std::make_move_iterator(new_spots.begin()), std::make_move_iterator(new_spots.end()));
+    }
+    return true;
+  }
+  return false;
 }
 
 template <typename EntityCreator>
@@ -126,9 +167,7 @@ bool DistributedSpawnSpace::Spawn(EntityCreator &&spawner, StochasticsInterface
   {
     return false;
   }
-
-  // TODO: Sample Velocity
-  //
+  auto spawn_velocity{SampleVelocity(velocity_range, rng)};
   // TODO: Find last spawnable spot on the zone of the sampled interval
   // spawner.CreateEntity(*entity, place.zone->GetPosition(distance));
   // TODO: Remove/replace interval
diff --git a/engine/src/Utils/Spawning/SpawnZone.h b/engine/src/Utils/Spawning/SpawnZone.h
index 0ae88e46..f82ebf82 100644
--- a/engine/src/Utils/Spawning/SpawnZone.h
+++ b/engine/src/Utils/Spawning/SpawnZone.h
@@ -25,6 +25,10 @@
 
 namespace OpenScenarioEngine::v1_3
 {
+/// \brief A SpawnZone is a representation of a mantle_api::ITrafficAreaStream with the assumption
+/// that said stream consists of connected lanes. It reduces the stream to its length and a full
+/// list of obstacles within it. The SpawnZone is used to generate SpawnSpots, which is a list of
+/// empty intervals where entities can be spawned.
 class SpawnZone : public Interval
 {
 public:
diff --git a/engine/src/Utils/Spawning/Transform.h b/engine/src/Utils/Spawning/Transform.h
index 072c6733..975b4316 100644
--- a/engine/src/Utils/Spawning/Transform.h
+++ b/engine/src/Utils/Spawning/Transform.h
@@ -17,20 +17,34 @@
 
 namespace OpenScenarioEngine::v1_3
 {
-template <typename Invocable, typename Type>
-std::vector<std::invoke_result_t<Invocable, const Type&>> Transform(const std::vector<Type>& input, Invocable&& invocable)
+template <typename Invocable, typename Container>
+std::vector<std::invoke_result_t<Invocable, typename std::remove_reference_t<Container>::value_type>> Transform(Container&& input, Invocable&& invocable)
 {
-  std::vector<std::invoke_result_t<Invocable, const Type&>> result;
+  std::vector<std::invoke_result_t<Invocable, typename std::remove_reference_t<Container>::value_type>> result;
   result.reserve(input.size());
-  std::transform(input.begin(), input.end(), std::back_inserter(result), std::forward<Invocable>(invocable));
+  if constexpr (std::is_lvalue_reference_v<decltype(input)>)
+  {
+    std::transform(input.begin(), input.end(), std::back_inserter(result), std::forward<Invocable>(invocable));
+  }
+  else
+  {
+    std::transform(std::make_move_iterator(input.begin()), std::make_move_iterator(input.end()), std::back_inserter(result), std::forward<Invocable>(invocable));
+  }
   return result;
 }
 
-template <typename Invocable, typename Type>
-void Transform(const std::vector<Type>& input, std::vector<std::invoke_result_t<Invocable, const Type&>>& output, Invocable&& invocable)
+template <typename Invocable, typename Container>
+void Transform(Container&& input, std::vector<std::invoke_result_t<Invocable, typename std::remove_reference_t<Container>::value_type>>& output, Invocable&& invocable)
 {
   output.reserve(output.size() + input.size());
-  std::transform(input.begin(), input.end(), std::back_inserter(output), std::forward<Invocable>(invocable));
+  if constexpr (std::is_lvalue_reference_v<decltype(input)>)
+  {
+    std::transform(input.begin(), input.end(), std::back_inserter(output), std::forward<Invocable>(invocable));
+  }
+  else
+  {
+    std::transform(std::make_move_iterator(input.begin()), std::make_move_iterator(input.end()), std::back_inserter(output), std::forward<Invocable>(invocable));
+  }
 }
 
 template <typename Type, typename Invocable>
diff --git a/engine/src/Utils/Spawning/VelocityRange.h b/engine/src/Utils/Spawning/VelocityRange.h
index 348f7d8b..a4a9eeb7 100644
--- a/engine/src/Utils/Spawning/VelocityRange.h
+++ b/engine/src/Utils/Spawning/VelocityRange.h
@@ -14,6 +14,7 @@
 
 #include <memory>
 
+#include "Interval.h"
 #include "openScenarioLib/generated/v1_3/api/ApiClassInterfacesV1_3.h"
 
 namespace OpenScenarioEngine::v1_3
@@ -24,12 +25,17 @@ struct VelocityRange
 {
   constexpr bool Contains(const VelocityRange &) const;
 
+  constexpr bool Contains(units::velocity::meters_per_second_t) const;
+
   constexpr units::velocity::meters_per_second_t GetDifference() const;
 
   units::velocity::meters_per_second_t min{std::numeric_limits<double>::lowest()};
   units::velocity::meters_per_second_t max{std::numeric_limits<double>::max()};
 };
 
+constexpr Interval operator*(const VelocityRange &, units::time::second_t);
+constexpr Interval operator*(units::time::second_t, const VelocityRange &);
+
 struct ToSpawnVelocityRange
 {
   template <typename Type>
@@ -56,11 +62,26 @@ constexpr bool VelocityRange::Contains(const VelocityRange &other) const
   return min <= other.min && other.max <= max;
 }
 
+constexpr bool VelocityRange::Contains(units::velocity::meters_per_second_t velocity) const
+{
+  return min <= velocity && velocity <= max;
+}
+
 constexpr units::velocity::meters_per_second_t VelocityRange::GetDifference() const
 {
   return max - min;
 }
 
+constexpr Interval operator*(const VelocityRange &range, units::time::second_t time)
+{
+  return {range.min * time, range.max * time};
+}
+
+constexpr Interval operator*(units::time::second_t time, const VelocityRange &range)
+{
+  return range * time;
+}
+
 template <typename Type>
 VelocityRange ToSpawnVelocityRange::operator()(const std::shared_ptr<Type> &input) const
 {
diff --git a/engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp b/engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp
index 2c98045f..d4fd0dad 100644
--- a/engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp
+++ b/engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp
@@ -15,6 +15,7 @@
 
 #include <memory>
 #include <string>
+#include <utility>
 
 #include "OpenScenarioEngine/OpenScenarioEngine.h"
 #include "TestUtils.h"
@@ -40,6 +41,7 @@ protected:
     auto& mockTrafficAreaService{dynamic_cast<mantle_api::MockTrafficAreaService&>(env_->GetTrafficAreaService())};
 
     ON_CALL(*mock_traffic_area_stream, GetLength()).WillByDefault(Return(500_m));
+    // ON_CALL(*mock_traffic_area_stream, Convert(_)).WillByDefault(Return(std::optional<mantle_api::Pose>{std::in_place, mantle_api::ITrafficAreaStream::StreamPosition{100_m, 0_m}, units::angle::radian_t{}}));
     ON_CALL(mockTrafficAreaService, CreateTrafficArea(_)).WillByDefault([this]() -> std::vector<std::shared_ptr<mantle_api::ITrafficAreaStream>>
                                                                         {
       traffic_area_stream_vec_.emplace_back(std::move(mock_traffic_area_stream));
-- 
GitLab


From c28cda2958e622cd41bd9e9ea9de7138ab5576ae Mon Sep 17 00:00:00 2001
From: Noah Schick <noah.schick@in-tech.com>
Date: Tue, 11 Mar 2025 17:27:19 +0100
Subject: [PATCH 19/26] Expant mocking for TrafficAreaAction

---
 .../ConvertScenarioTrafficDistribution.cpp    |   4 +-
 .../GenericAction/TrafficAreaAction.cpp       |   1 +
 engine/src/Utils/EntityCreator.cpp            | 121 ++++++++++--------
 engine/src/Utils/EntityCreator.h              |  32 +++--
 engine/src/Utils/Spawning/Interval.h          |   2 +-
 engine/src/Utils/Spawning/SpawnSpace.h        |   4 +-
 .../GenericAction/TrafficAreaActionTest.cpp   |  65 ++++++++--
 .../data/Scenarios/VehicleCatalog.xosc        |   4 +-
 8 files changed, 161 insertions(+), 72 deletions(-)

diff --git a/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.cpp b/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.cpp
index bba5d9a4..1fd3d7a3 100644
--- a/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.cpp
+++ b/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.cpp
@@ -19,7 +19,7 @@ struct ToEntities
 {
   std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IEntityObject> operator()(const NET_ASAM_OPENSCENARIO::v1_3::IEntityDistributionEntry& entry) const
   {
-    return entry.GetScenarioObjectTemplate()->GetEntitiyObject(); // https://github.com/RA-Consulting-GmbH/openscenario.api.test/issues/222
+    return entry.GetScenarioObjectTemplate()->GetEntitiyObject();  // https://github.com/RA-Consulting-GmbH/openscenario.api.test/issues/222
   }
 
   std::vector<std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IEntityObject>> operator()(const NET_ASAM_OPENSCENARIO::v1_3::ITrafficDistributionEntry& entry) const
@@ -33,7 +33,7 @@ struct ToTrafficDistribution
   TrafficDistribution operator()(const NET_ASAM_OPENSCENARIO::v1_3::ITrafficDistributionEntry& entry) const
   {
     return {TransformSharedPointers(entry.GetEntityDistribution()->GetEntityDistributionEntry(), [](const NET_ASAM_OPENSCENARIO::v1_3::IEntityDistributionEntry& entry)
-                                    { return entry.GetScenarioObjectTemplate()->GetEntitiyObject(); }), // https://github.com/RA-Consulting-GmbH/openscenario.api.test/issues/222
+                                    { return entry.GetScenarioObjectTemplate()->GetEntitiyObject(); }),  // https://github.com/RA-Consulting-GmbH/openscenario.api.test/issues/222
             entry.GetWeight()};
   }
 };
diff --git a/engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp b/engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp
index b3518912..3c7d47a7 100644
--- a/engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp
+++ b/engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp
@@ -11,6 +11,7 @@
 #include "Storyboard/GenericAction/TrafficAreaAction.h"
 
 #include "Conversion/OscToMantle/ConvertScenarioTrafficArea.h"
+#include "Utils/ControllerCreator.h"
 #include "Utils/EntityCreator.h"
 #include "Utils/Logger.h"
 #include "Utils/Spawning/Length.h"
diff --git a/engine/src/Utils/EntityCreator.cpp b/engine/src/Utils/EntityCreator.cpp
index 0042a031..0176c046 100644
--- a/engine/src/Utils/EntityCreator.cpp
+++ b/engine/src/Utils/EntityCreator.cpp
@@ -63,35 +63,41 @@ std::map<NET_ASAM_OPENSCENARIO::v1_3::MiscObjectCategory::MiscObjectCategoryEnum
 EntityCreator::EntityCreator(std::shared_ptr<mantle_api::IEnvironment> environment)
     : environment_(environment) {}
 
-void EntityCreator::CreateEntity(std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IScenarioObject> scenario_object)
+mantle_api::IEntity& EntityCreator::CreateEntity(const std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IEntityObject>& entity_object, const std::string& name)
 {
-  auto entity_object = scenario_object->GetEntityObject();
-
-  if (!entity_object)
+  if (entity_object->IsSetVehicle())
   {
-    throw std::runtime_error(std::string("entityObject missing in ScenarioObject " + scenario_object->GetName()));
+    return CreateVehicle(entity_object->GetVehicle(), name);
   }
-
-  if (entity_object->GetVehicle() != nullptr)
+  else if (entity_object->IsSetPedestrian())
   {
-    CreateVehicle(entity_object->GetVehicle(), scenario_object->GetName());
+    return CreatePedestrian(entity_object->GetPedestrian(), name);
   }
-  else if (entity_object->GetPedestrian() != nullptr)
+  else if (entity_object->IsSetMiscObject())
   {
-    CreatePedestrian(entity_object->GetPedestrian(), scenario_object->GetName());
+    return CreateMiscObject(entity_object->GetMiscObject(), name);
   }
-  else if (entity_object->GetMiscObject() != nullptr)
+  else if (entity_object->IsSetCatalogReference())
+  {
+    return CreateCatalogReferenceEntity(entity_object->GetCatalogReference(), name);
+  }
+  else
   {
-    CreateMiscObject(entity_object->GetMiscObject(), scenario_object->GetName());
+    throw std::runtime_error("EntityCreator::CreateEntity: Entity has no valid object type (neither vehicle, pedestrian, miscellaneous nor catalog reference)");
   }
-  else if (entity_object->GetCatalogReference() != nullptr)
+}
+
+mantle_api::IEntity& EntityCreator::CreateEntity(const std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IScenarioObject>& scenario_object)
+{
+  if (!scenario_object->IsSetEntityObject())
   {
-    CreateCatalogReferenceEntity(entity_object->GetCatalogReference(), scenario_object->GetName());
+    throw std::runtime_error("EntityObject missing in ScenarioObject " + scenario_object->GetName());
   }
+  return CreateEntity(scenario_object->GetEntityObject(), scenario_object->GetName());
 }
 
-void EntityCreator::CreateVehicle(std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IVehicle> vehicle,
-                                  const std::string& name)
+mantle_api::IVehicle& EntityCreator::CreateVehicle(const std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IVehicle>& vehicle,
+                                                   const std::string& name)
 {
   mantle_api::VehicleProperties properties;
   properties.type = mantle_api::EntityType::kVehicle;
@@ -129,30 +135,46 @@ void EntityCreator::CreateVehicle(std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::I
     properties.is_host = true;
   }
 
-  environment_->GetEntityRepository().Create(name, properties);
+  return environment_->GetEntityRepository().Create(name, properties);
+}
+
+inline bool IsInWheelchair(const NET_ASAM_OPENSCENARIO::v1_3::IPedestrian& pedestrian)
+{
+  return GetEnumValue(pedestrian.GetPedestrianCategory()) == NET_ASAM_OPENSCENARIO::v1_3::PedestrianCategory::PedestrianCategoryEnum::WHEELCHAIR;
 }
 
-void EntityCreator::CreatePedestrian(std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IPedestrian> pedestrian,
-                                     const std::string& name)
+mantle_api::IEntity& EntityCreator::CreatePedestrian(const std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IPedestrian>& pedestrian,
+                                                     const std::string& name)
 {
-  if (pedestrian->GetPedestrianCategory().GetFromLiteral(pedestrian->GetPedestrianCategory().GetLiteral()) ==
-      NET_ASAM_OPENSCENARIO::v1_3::PedestrianCategory::PedestrianCategoryEnum::WHEELCHAIR)
+  if (IsInWheelchair(*pedestrian))
   {
-    mantle_api::VehicleProperties vehicle_properties;
-    vehicle_properties.classification = mantle_api::VehicleClass::kWheelchair;
-    FillEntityPropertiesForPedestrian(pedestrian, vehicle_properties);
-    environment_->GetEntityRepository().Create(name, vehicle_properties);
+    return CreateWheelchairPedestrian(pedestrian, name);
   }
   else
   {
-    mantle_api::PedestrianProperties pedestrian_properties;
-    FillEntityPropertiesForPedestrian(pedestrian, pedestrian_properties);
-    environment_->GetEntityRepository().Create(name, pedestrian_properties);
+    return CreateRegularPedestrian(pedestrian, name);
   }
 }
 
-void EntityCreator::CreateMiscObject(std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IMiscObject> misc_object,
-                                     const std::string& name)
+mantle_api::IVehicle& EntityCreator::CreateWheelchairPedestrian(const std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IPedestrian>& pedestrian,
+                                                                const std::string& name)
+{
+  mantle_api::VehicleProperties vehicle_properties;
+  vehicle_properties.classification = mantle_api::VehicleClass::kWheelchair;
+  FillEntityPropertiesForPedestrian(pedestrian, vehicle_properties);
+  return environment_->GetEntityRepository().Create(name, vehicle_properties);
+}
+
+mantle_api::IPedestrian& EntityCreator::CreateRegularPedestrian(const std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IPedestrian>& pedestrian,
+                                                                const std::string& name)
+{
+  mantle_api::PedestrianProperties pedestrian_properties;
+  FillEntityPropertiesForPedestrian(pedestrian, pedestrian_properties);
+  return environment_->GetEntityRepository().Create(name, pedestrian_properties);
+}
+
+mantle_api::IStaticObject& EntityCreator::CreateMiscObject(const std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IMiscObject>& misc_object,
+                                                           const std::string& name)
 {
   mantle_api::StaticObjectProperties properties;
   properties.type = mantle_api::EntityType::kStatic;
@@ -161,32 +183,31 @@ void EntityCreator::CreateMiscObject(std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3
   FillBoundingBoxProperties(misc_object->GetBoundingBox(), misc_object->GetName(), properties);
   FillGenericProperties(*misc_object, properties);
   SetVerticalOffset(misc_object, properties);
-  environment_->GetEntityRepository().Create(name, properties);
+  return environment_->GetEntityRepository().Create(name, properties);
 }
 
-void EntityCreator::CreateCatalogReferenceEntity(
-    std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::ICatalogReference> catalog_reference,
+mantle_api::IEntity& EntityCreator::CreateCatalogReferenceEntity(
+    const std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::ICatalogReference>& catalog_reference,
     const std::string& name)
 {
-  auto catalog_element = catalog_reference->GetRef();
-  if (catalog_element == nullptr)
+  if (!catalog_reference->IsSetRef())
   {
-    throw std::runtime_error(
-        std::string("CatalogReference " + catalog_reference->GetEntryName() + " without ref."));
+    throw std::runtime_error("CatalogReference " + catalog_reference->GetEntryName() + " without ref.");
   }
-
+  auto catalog_element = catalog_reference->GetRef();
   if (NET_ASAM_OPENSCENARIO::v1_3::CatalogHelper::IsVehicle(catalog_element))
   {
-    CreateVehicle(NET_ASAM_OPENSCENARIO::v1_3::CatalogHelper::AsVehicle(catalog_element), name);
+    return CreateVehicle(NET_ASAM_OPENSCENARIO::v1_3::CatalogHelper::AsVehicle(catalog_element), name);
   }
-  else if (NET_ASAM_OPENSCENARIO::v1_3::CatalogHelper::IsPedestrian(catalog_element))
+  if (NET_ASAM_OPENSCENARIO::v1_3::CatalogHelper::IsPedestrian(catalog_element))
   {
-    CreatePedestrian(NET_ASAM_OPENSCENARIO::v1_3::CatalogHelper::AsPedestrian(catalog_element), name);
+    return CreatePedestrian(NET_ASAM_OPENSCENARIO::v1_3::CatalogHelper::AsPedestrian(catalog_element), name);
   }
-  else if (NET_ASAM_OPENSCENARIO::v1_3::CatalogHelper::IsMiscObject(catalog_element))
+  if (NET_ASAM_OPENSCENARIO::v1_3::CatalogHelper::IsMiscObject(catalog_element))
   {
-    CreateMiscObject(NET_ASAM_OPENSCENARIO::v1_3::CatalogHelper::AsMiscObject(catalog_element), name);
+    return CreateMiscObject(NET_ASAM_OPENSCENARIO::v1_3::CatalogHelper::AsMiscObject(catalog_element), name);
   }
+  throw std::runtime_error("Catalog reference does not describe a valid entity");
 }
 
 void EntityCreator::FillBoundingBoxProperties(std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IBoundingBox> bounding_box,
@@ -195,12 +216,12 @@ void EntityCreator::FillBoundingBoxProperties(std::shared_ptr<NET_ASAM_OPENSCENA
 {
   if (!bounding_box)
   {
-    throw std::runtime_error(std::string("Entity " + name + " without bounding box."));
+    throw std::runtime_error("Entity " + name + " without bounding box.");
   }
   auto dimensions = bounding_box->GetDimensions();
   if (!dimensions)
   {
-    throw std::runtime_error(std::string("Bounding box of entity " + name + " without Dimensions."));
+    throw std::runtime_error("Bounding box of entity " + name + " without Dimensions.");
   }
 
   properties.bounding_box.dimension.length = units::length::meter_t(dimensions->GetLength());
@@ -210,7 +231,7 @@ void EntityCreator::FillBoundingBoxProperties(std::shared_ptr<NET_ASAM_OPENSCENA
   auto center = bounding_box->GetCenter();
   if (!center)
   {
-    throw std::runtime_error(std::string("Bounding box of entity " + name + " without Center."));
+    throw std::runtime_error("Bounding box of entity " + name + " without Center.");
   }
 
   properties.bounding_box.geometric_center.x = units::length::meter_t(center->GetX());
@@ -225,7 +246,7 @@ void EntityCreator::FillAxleProperties(std::shared_ptr<NET_ASAM_OPENSCENARIO::v1
 {
   if (open_scenario_axle == nullptr)
   {
-    throw std::runtime_error(std::string("Entity " + name + " is missing axle information."));
+    throw std::runtime_error("Entity " + name + " is missing axle information.");
   }
 
   axle.bb_center_to_axle_center = {
@@ -255,7 +276,7 @@ mantle_api::VehicleClass EntityCreator::GetVehicleClass(NET_ASAM_OPENSCENARIO::v
   {
     return map_vehicle_class[vehicle_category.GetFromLiteral(vehicle_category.GetLiteral())];
   }
-  throw std::runtime_error(std::string("No vehicle class for vehicle category " + vehicle_category.GetLiteral()));
+  throw std::runtime_error("No vehicle class for vehicle category " + vehicle_category.GetLiteral());
 }
 
 mantle_api::StaticObjectType EntityCreator::GetStaticObjectType(NET_ASAM_OPENSCENARIO::v1_3::MiscObjectCategory misc_object_category)
@@ -265,7 +286,7 @@ mantle_api::StaticObjectType EntityCreator::GetStaticObjectType(NET_ASAM_OPENSCE
   {
     return map_static_object_type[misc_object_category.GetFromLiteral(misc_object_category.GetLiteral())];
   }
-  throw std::runtime_error(std::string("No static object type for misc object category " + misc_object_category.GetLiteral()));
+  throw std::runtime_error("No static object type for misc object category " + misc_object_category.GetLiteral());
 }
 
 mantle_api::EntityType EntityCreator::GetEntityTypeFromPedestrianCategory(
@@ -276,7 +297,7 @@ mantle_api::EntityType EntityCreator::GetEntityTypeFromPedestrianCategory(
   {
     return map_entity_type[pedestrian_category.GetFromLiteral(pedestrian_category.GetLiteral())];
   }
-  throw std::runtime_error(std::string("No entity type for pedestrian category " + pedestrian_category.GetLiteral()));
+  throw std::runtime_error("No entity type for pedestrian category " + pedestrian_category.GetLiteral());
 }
 
 void EntityCreator::SetVerticalOffset(std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IMiscObject> misc_object, mantle_api::StaticObjectProperties& entity_properties)
diff --git a/engine/src/Utils/EntityCreator.h b/engine/src/Utils/EntityCreator.h
index 8dc19ed7..03f976d8 100644
--- a/engine/src/Utils/EntityCreator.h
+++ b/engine/src/Utils/EntityCreator.h
@@ -21,16 +21,27 @@ class EntityCreator
 public:
   explicit EntityCreator(std::shared_ptr<mantle_api::IEnvironment> environment);
 
-  void CreateEntity(std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IScenarioObject> scenario_object);
+  mantle_api::IEntity& CreateEntity(const std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IScenarioObject>& scenario_object);
+
+  mantle_api::IEntity& CreateEntity(const std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IEntityObject>& entity_object, const std::string& name);
 
 private:
-  void CreateVehicle(std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IVehicle> vehicle, const std::string& name);
-  void CreatePedestrian(std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IPedestrian> pedestrian,
-                        const std::string& name);
-  void CreateMiscObject(std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IMiscObject> misc_object,
-                        const std::string& name);
-  void CreateCatalogReferenceEntity(std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::ICatalogReference> catalog_reference,
-                                    const std::string& name);
+  mantle_api::IVehicle& CreateVehicle(const std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IVehicle>& vehicle, const std::string& name);
+
+  mantle_api::IEntity& CreatePedestrian(const std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IPedestrian>& pedestrian,
+                                        const std::string& name);
+
+  mantle_api::IVehicle& CreateWheelchairPedestrian(const std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IPedestrian>& pedestrian,
+                                                   const std::string& name);
+
+  mantle_api::IPedestrian& CreateRegularPedestrian(const std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IPedestrian>& pedestrian,
+                                                   const std::string& name);
+
+  mantle_api::IStaticObject& CreateMiscObject(const std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IMiscObject>& misc_object,
+                                              const std::string& name);
+
+  mantle_api::IEntity& CreateCatalogReferenceEntity(const std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::ICatalogReference>& catalog_reference,
+                                                    const std::string& name);
 
   /// Fills the bounding box properties of an entity
   /// @param[in]  bounding_box The bounding box information to copy from
@@ -86,4 +97,9 @@ private:
   std::shared_ptr<mantle_api::IEnvironment> environment_;
 };
 
+template <typename Category>
+inline auto GetEnumValue(const Category& category)
+{
+  return category.GetFromLiteral(category.GetLiteral());
+}
 }  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/Interval.h b/engine/src/Utils/Spawning/Interval.h
index 1beb8aed..1748e0e1 100644
--- a/engine/src/Utils/Spawning/Interval.h
+++ b/engine/src/Utils/Spawning/Interval.h
@@ -126,7 +126,7 @@ constexpr Interval Interval::GetIntersection(const Interval& other) const
 
 constexpr Interval Interval::OffsetBy(Padding padding) const
 {
-  return OffsetBy(padding.rear, padding.front);
+  return OffsetBy(-padding.rear, padding.front);
 }
 
 constexpr Interval Interval::OffsetBy(Interval other) const
diff --git a/engine/src/Utils/Spawning/SpawnSpace.h b/engine/src/Utils/Spawning/SpawnSpace.h
index 5315c305..9b838397 100644
--- a/engine/src/Utils/Spawning/SpawnSpace.h
+++ b/engine/src/Utils/Spawning/SpawnSpace.h
@@ -132,7 +132,9 @@ bool SequentialSpawnSpace::Spawn(EntityCreator &&spawner, StochasticsInterface &
     const auto &stream{*std::next(area->begin(), std::distance(spot_area->begin(), spot_stream))};
     const mantle_api::Pose pose{stream->Convert(mantle_api::ITrafficAreaStream::StreamPose{{interval.max, {}}, {}}).value()};
 
-    // TODO: Spawn entity
+    mantle_api::IEntity &object{spawner.CreateEntity(entity, "NAME_DISCARDED_BY_TRAFFIC_AREA_ACTION")};
+    object.SetPosition(pose.position);
+    object.SetOrientation(pose.orientation);
 
     std::vector<OpenScenarioEngine::v1_3::Interval> new_intervals{spot->SplitBy(Interval{interval.max, interval.max}.OffsetBy(-padding.rear, padding.front))};
     if (new_intervals.empty())
diff --git a/engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp b/engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp
index d4fd0dad..b78a52d9 100644
--- a/engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp
+++ b/engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp
@@ -29,28 +29,76 @@ using testing::OpenScenarioEngine::v1_3::GetScenariosPath;
 using testing::OpenScenarioEngine::v1_3::OpenScenarioEngineTestBase;
 using namespace units::literals;
 
+struct MockVehicle : mantle_api::IVehicle
+{
+  mantle_api::UniqueId GetUniqueId() const override { return {}; }
+
+  void SetName(const std::string& name) override {}
+  const std::string& GetName() const override
+  {
+    static std::string s;
+    return s;
+  }
+
+  void SetPosition(const mantle_api::Vec3<units::length::meter_t>& xyz) override { this->xyz = xyz; };
+  mantle_api::Vec3<units::length::meter_t> GetPosition() const override { return {}; }
+
+  void SetVelocity(const mantle_api::Vec3<units::velocity::meters_per_second_t>&) override {}
+  mantle_api::Vec3<units::velocity::meters_per_second_t> GetVelocity() const override { return {}; }
+
+  void SetAcceleration(const mantle_api::Vec3<units::acceleration::meters_per_second_squared_t>&) override {}
+  mantle_api::Vec3<units::acceleration::meters_per_second_squared_t> GetAcceleration() const override { return {}; }
+
+  void SetOrientation(const mantle_api::Orientation3<units::angle::radian_t>& orientation) override { this->orientation = orientation; }
+  mantle_api::Orientation3<units::angle::radian_t> GetOrientation() const override { return {}; }
+
+  void SetOrientationRate(const mantle_api::Orientation3<units::angular_velocity::radians_per_second_t>&) override {}
+  mantle_api::Orientation3<units::angular_velocity::radians_per_second_t> GetOrientationRate() const override { return {}; }
+
+  void SetOrientationAcceleration(const mantle_api::Orientation3<units::angular_acceleration::radians_per_second_squared_t>&) override {}
+  mantle_api::Orientation3<units::angular_acceleration::radians_per_second_squared_t> GetOrientationAcceleration() const override { return {}; }
+
+  void SetProperties(std::unique_ptr<mantle_api::EntityProperties>) override {}
+  mantle_api::VehicleProperties* GetProperties() const override { return {}; }
+
+  void SetAssignedLaneIds(const std::vector<std::uint64_t>&) override {}
+  std::vector<std::uint64_t> GetAssignedLaneIds() const override { return {}; }
+
+  void SetVisibility(const mantle_api::EntityVisibilityConfig& visibility) override {}
+  mantle_api::EntityVisibilityConfig GetVisibility() const override { return {}; }
+
+  void SetIndicatorState(mantle_api::IndicatorState state) override {}
+  mantle_api::IndicatorState GetIndicatorState() const override { return {}; }
+
+  mantle_api::Vec3<units::length::meter_t> xyz;
+  mantle_api::Orientation3<units::angle::radian_t> orientation;
+};
+
 class TrafficAreaActionTestFixture : public OpenScenarioEngineTestBase
 {
 protected:
   void SetUp() override
   {
+    auto& stream{dynamic_cast<mantle_api::MockTrafficAreaStream&>(*streams.emplace_back(std::make_shared<mantle_api::MockTrafficAreaStream>()))};
     OpenScenarioEngineTestBase::SetUp();
     ON_CALL(controller_, GetName()).WillByDefault(ReturnRef(controller_name));
     ON_CALL(controller_, GetUniqueId()).WillByDefault(Return(1234));
     LOGGER = std::make_unique<testing::OpenScenarioEngine::v1_3::TestLogger>();
     auto& mockTrafficAreaService{dynamic_cast<mantle_api::MockTrafficAreaService&>(env_->GetTrafficAreaService())};
 
-    ON_CALL(*mock_traffic_area_stream, GetLength()).WillByDefault(Return(500_m));
-    // ON_CALL(*mock_traffic_area_stream, Convert(_)).WillByDefault(Return(std::optional<mantle_api::Pose>{std::in_place, mantle_api::ITrafficAreaStream::StreamPosition{100_m, 0_m}, units::angle::radian_t{}}));
-    ON_CALL(mockTrafficAreaService, CreateTrafficArea(_)).WillByDefault([this]() -> std::vector<std::shared_ptr<mantle_api::ITrafficAreaStream>>
-                                                                        {
-      traffic_area_stream_vec_.emplace_back(std::move(mock_traffic_area_stream));
-      return traffic_area_stream_vec_; });
+    ON_CALL(stream, GetLength()).WillByDefault(Return(500_m));
+    ON_CALL(stream, Convert(testing::A<const mantle_api::ITrafficAreaStream::StreamPose&>())).WillByDefault(Return(mantle_api::Pose{mantle_api::Vec3<units::length::meter_t>{100_m, 0_m, 0_m}, mantle_api::Orientation3<units::angle::radian_t>{}}));  // clang-format off
+    ON_CALL(mockTrafficAreaService, CreateTrafficArea(_)).WillByDefault([this]() -> std::vector<std::shared_ptr<mantle_api::ITrafficAreaStream>> {  // 
+      return streams;
+    });  // clang-format on
+    auto& entity_repository{static_cast<mantle_api::MockEntityRepository&>(env_->GetEntityRepository())};
+    ON_CALL(entity_repository, Create(testing::A<const std::string&>(), testing::A<const mantle_api::VehicleProperties&>())).WillByDefault(ReturnRef(mock_vehicle));
   }
 
-  std::vector<std::shared_ptr<mantle_api::ITrafficAreaStream>> traffic_area_stream_vec_;
+  std::vector<std::shared_ptr<mantle_api::ITrafficAreaStream>> streams;
   std::shared_ptr<testing::OpenScenarioEngine::v1_3::TestLogger> LOGGER;
-  std::shared_ptr<mantle_api::MockTrafficAreaStream> mock_traffic_area_stream{std::make_shared<mantle_api::MockTrafficAreaStream>()};
+
+  MockVehicle mock_vehicle;
 
   std::string controller_name{"TestController"};
 };
@@ -62,4 +110,5 @@ TEST_F(TrafficAreaActionTestFixture, GivenScenarioWithTrafficAreaAction_WhenInit
   engine.Init();
   engine.SetupDynamicContent();
   engine.Step({});
+  EXPECT_EQ(mock_vehicle.xyz.x, 100_m);
 }
diff --git a/engine/tests/Storyboard/GenericAction/data/Scenarios/VehicleCatalog.xosc b/engine/tests/Storyboard/GenericAction/data/Scenarios/VehicleCatalog.xosc
index 1314dc68..bc7c5a51 100644
--- a/engine/tests/Storyboard/GenericAction/data/Scenarios/VehicleCatalog.xosc
+++ b/engine/tests/Storyboard/GenericAction/data/Scenarios/VehicleCatalog.xosc
@@ -14,8 +14,8 @@
       </Axles>
       <Properties>
         <Property name="custom_property" value="5" />
-        <Property name="min_spawn_velocity" value="20" />
-        <Property name="max_spawn_velocity" value="80" />
+        <Property name="min_spawn_velocity" value="2" />
+        <Property name="max_spawn_velocity" value="8" />
       </Properties>
     </Vehicle>
   </Catalog>
-- 
GitLab


From d16d5a85a2280dd302732f697fe377bc508bebf8 Mon Sep 17 00:00:00 2001
From: Noah Schick <noah.schick@in-tech.com>
Date: Wed, 12 Mar 2025 10:21:45 +0100
Subject: [PATCH 20/26] Fix TTC calculation in TrafficAreaAction

---
 engine/src/Utils/Spawning/Interval.cpp        | 10 ++++++
 engine/src/Utils/Spawning/Interval.h          | 18 ++++++++++
 engine/src/Utils/Spawning/SpawnSpace.cpp      | 30 +++++++++++++++-
 engine/src/Utils/Spawning/SpawnSpace.h        | 35 ++++++++++---------
 engine/src/Utils/Spawning/VelocityRange.cpp   |  5 +++
 engine/src/Utils/Spawning/VelocityRange.h     |  3 ++
 .../GenericAction/TrafficAreaActionTest.cpp   | 30 ++++++++++++----
 7 files changed, 108 insertions(+), 23 deletions(-)

diff --git a/engine/src/Utils/Spawning/Interval.cpp b/engine/src/Utils/Spawning/Interval.cpp
index d35e5077..9946ebe0 100644
--- a/engine/src/Utils/Spawning/Interval.cpp
+++ b/engine/src/Utils/Spawning/Interval.cpp
@@ -43,4 +43,14 @@ std::vector<Interval> Interval::SplitBy(Interval input) const
   }
   return {};
 }
+
+std::ostream& operator<<(std::ostream& os, const Padding& padding)
+{
+  return os << '[' << padding.rear.value() << ", " << padding.front.value() << ']';
+}
+
+std::ostream& operator<<(std::ostream& os, const Interval& interval)
+{
+  return os << '[' << interval.min.value() << ", " << interval.max.value() << ']';
+}
 }  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/Interval.h b/engine/src/Utils/Spawning/Interval.h
index 1748e0e1..762a5f83 100644
--- a/engine/src/Utils/Spawning/Interval.h
+++ b/engine/src/Utils/Spawning/Interval.h
@@ -13,6 +13,7 @@
 #include <MantleAPI/Traffic/i_traffic_area_stream.h>
 #include <units.h>
 
+#include <iostream>
 #include <set>
 #include <vector>
 
@@ -30,6 +31,8 @@ struct Padding
   units::length::meter_t front;
 };
 
+std::ostream& operator<<(std::ostream&, const Padding&);
+
 struct Interval
 {
   constexpr Interval(units::length::meter_t min, units::length::meter_t max)
@@ -75,6 +78,11 @@ struct Interval
   units::length::meter_t min, max;
 };
 
+constexpr Interval operator+(const Interval&, units::length::meter_t);
+constexpr Interval operator-(const Interval&, units::length::meter_t);
+
+std::ostream& operator<<(std::ostream&, const Interval&);
+
 struct OffsetBy
 {
   constexpr Interval operator()(const Interval&) const;
@@ -139,6 +147,16 @@ constexpr Interval Interval::OffsetBy(units::length::meter_t min_offset, units::
   return {min + min_offset, max + max_offset};
 }
 
+constexpr Interval operator+(const Interval& interval, units::length::meter_t offset)
+{
+  return {interval.min + offset, interval.max + offset};
+}
+
+constexpr Interval operator-(const Interval& interval, units::length::meter_t offset)
+{
+  return {interval.min - offset, interval.max - offset};
+}
+
 constexpr Interval OffsetBy::operator()(const Interval& interval) const
 {
   return interval.OffsetBy(offset);
diff --git a/engine/src/Utils/Spawning/SpawnSpace.cpp b/engine/src/Utils/Spawning/SpawnSpace.cpp
index 98ee93e6..b8e77eed 100644
--- a/engine/src/Utils/Spawning/SpawnSpace.cpp
+++ b/engine/src/Utils/Spawning/SpawnSpace.cpp
@@ -74,7 +74,7 @@ SequentialSpawnSpace::FindSpot(const Padding &padding, units::velocity::meters_p
         Interval range{spot->OffsetBy(-padding)};
         if (range.min <= range.max)
         {
-          range = range.GetIntersection(spot->OffsetBy(spot->velocity * timespan));
+          range = range.GetIntersection(spot->OffsetBy(spot->velocity * timespan - velocity * timespan));
           if (range.min <= range.max)
           {
             return {area, stream, std::next(spot).base(), std::move(range)};
@@ -176,4 +176,32 @@ Padding GetLengthPadding(const Entity &entity)
   const units::length::meter_t half_length{GetLength(entity) * units::dimensionless::scalar_t{.5}};
   return {half_length, half_length};
 }
+
+void UpdateSpot(std::vector<OpenScenarioEngine::v1_3::SpawnSpot> &stream,
+                std::vector<OpenScenarioEngine::v1_3::SpawnSpot>::iterator spot,
+                const Interval &occupied_space,
+                units::velocity::meters_per_second_t velocity)
+{
+  std::vector<OpenScenarioEngine::v1_3::Interval> new_intervals{spot->SplitBy(occupied_space)};
+  if (new_intervals.empty())
+  {
+    stream.erase(spot);
+  }
+  else
+  {
+    std::vector<SpawnSpot> new_spots{Transform(std::move(new_intervals), [spot](Interval &&interval) -> SpawnSpot {  //
+      return {interval, spot->velocity};
+    })};
+    if (new_spots.front().min < occupied_space.min)
+    {
+      new_spots.front().velocity.max = velocity;
+    }
+    if (new_spots.back().max > occupied_space.max)
+    {
+      new_spots.back().velocity.min = velocity;
+    }
+    stream.insert(stream.erase(spot), std::make_move_iterator(new_spots.begin()), std::make_move_iterator(new_spots.end()));
+  }
+}
+
 }  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/SpawnSpace.h b/engine/src/Utils/Spawning/SpawnSpace.h
index 9b838397..faef6325 100644
--- a/engine/src/Utils/Spawning/SpawnSpace.h
+++ b/engine/src/Utils/Spawning/SpawnSpace.h
@@ -104,6 +104,19 @@ struct DistributedSpawnSpace : SpawnSpace
 
 /// Returns the positive distance from the entity's center to its rear and the positive distance from the entity's center to its front.
 Padding GetLengthPadding(const Entity &);
+
+//! Receives a vector of spawn spots, an iterator to a spot within that vector, an interval contained by that spot and a velocity.
+//! Replaces the spot pointed to by the iterator with the disjunction of the spot's interval and the given interval.
+//! The velocities of the new spots match the given velocity at the end where they touch the given interval.
+//!
+//! @param stream Container of spots in which a specific spot will be erased and possibly others inserted
+//! @param spot Iterator to a spot that will be replaced
+//! @param occupied_space Interval to be cut out of the given spot
+//! @param velocity Velocity of the ends of the new inserted spots at the given interval
+void UpdateSpot(std::vector<OpenScenarioEngine::v1_3::SpawnSpot> &stream,
+                std::vector<OpenScenarioEngine::v1_3::SpawnSpot>::iterator spot,
+                const Interval &occupied_space,
+                units::velocity::meters_per_second_t velocity);
 }  // namespace OpenScenarioEngine::v1_3
 
 namespace OpenScenarioEngine::v1_3
@@ -131,25 +144,15 @@ bool SequentialSpawnSpace::Spawn(EntityCreator &&spawner, StochasticsInterface &
     const auto area{std::next(areas.begin(), std::distance(spots.begin(), spot_area))};
     const auto &stream{*std::next(area->begin(), std::distance(spot_area->begin(), spot_stream))};
     const mantle_api::Pose pose{stream->Convert(mantle_api::ITrafficAreaStream::StreamPose{{interval.max, {}}, {}}).value()};
-
     mantle_api::IEntity &object{spawner.CreateEntity(entity, "NAME_DISCARDED_BY_TRAFFIC_AREA_ACTION")};
     object.SetPosition(pose.position);
     object.SetOrientation(pose.orientation);
-
-    std::vector<OpenScenarioEngine::v1_3::Interval> new_intervals{spot->SplitBy(Interval{interval.max, interval.max}.OffsetBy(-padding.rear, padding.front))};
-    if (new_intervals.empty())
-    {
-      spot_stream->erase(spot);
-    }
-    else
-    {
-      std::vector<SpawnSpot> new_spots{Transform(std::move(new_intervals), [spot](Interval &&interval) -> SpawnSpot {  //
-        return {interval, spot->velocity};
-      })};
-      new_spots.front().velocity.max = spawn_velocity;
-      new_spots.back().velocity.min = spawn_velocity;
-      spot_stream->insert(spot_stream->erase(spot), std::make_move_iterator(new_spots.begin()), std::make_move_iterator(new_spots.end()));
-    }
+    const auto occupied_space{Interval{interval.max, interval.max}.OffsetBy(-padding.rear, padding.front)};
+    // object.SetAssignedLaneIds(areas[static_cast<size_t>(std::distance(spots.begin(), spot_area))]  // clang-format off
+    //                                [static_cast<size_t>(std::distance(spot_area->begin(), spot_stream))]->GetLanesIds(
+    //   occupied_space.min, occupied_space.max
+    // ));  // clang-format on
+    UpdateSpot(*spot_stream, spot, occupied_space, spawn_velocity);
     return true;
   }
   return false;
diff --git a/engine/src/Utils/Spawning/VelocityRange.cpp b/engine/src/Utils/Spawning/VelocityRange.cpp
index 0190e588..7e028a6a 100644
--- a/engine/src/Utils/Spawning/VelocityRange.cpp
+++ b/engine/src/Utils/Spawning/VelocityRange.cpp
@@ -97,4 +97,9 @@ units::velocity::meters_per_second_t SampleVelocity(const VelocityRange &range,
 {
   return units::velocity::meters_per_second_t{rng.GetUniformDistributed(range.min.value(), range.max.value())};
 }
+
+std::ostream &operator<<(std::ostream &os, const VelocityRange &range)
+{
+  return os << '[' << range.min.value() << " - " << range.max.value() << ']';
+}
 }  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/VelocityRange.h b/engine/src/Utils/Spawning/VelocityRange.h
index a4a9eeb7..65c40a55 100644
--- a/engine/src/Utils/Spawning/VelocityRange.h
+++ b/engine/src/Utils/Spawning/VelocityRange.h
@@ -12,6 +12,7 @@
 #include <Stochastics/StochasticsInterface.h>
 #include <units.h>
 
+#include <iostream>
 #include <memory>
 
 #include "Interval.h"
@@ -53,6 +54,8 @@ template <typename Input>
 VelocityRange GetSpawnVelocityRange(Input &&);
 
 units::velocity::meters_per_second_t SampleVelocity(const VelocityRange &, StochasticsInterface &);
+
+std::ostream &operator<<(std::ostream &, const VelocityRange &);
 }  // namespace OpenScenarioEngine::v1_3
 
 namespace OpenScenarioEngine::v1_3
diff --git a/engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp b/engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp
index b78a52d9..594ce5c7 100644
--- a/engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp
+++ b/engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp
@@ -10,12 +10,14 @@
 
 #include <MantleAPI/Test/test_utils.h>
 #include <Storyboard/GenericAction/TrafficAreaAction.h>
+#include <gmock/gmock.h>
 #include <gtest/gtest.h>
 #include <units.h>
 
 #include <memory>
 #include <string>
 #include <utility>
+#include <vector>
 
 #include "OpenScenarioEngine/OpenScenarioEngine.h"
 #include "TestUtils.h"
@@ -23,6 +25,7 @@
 
 using testing::_;
 using testing::HasSubstr;
+using testing::NiceMock;
 using testing::Return;
 using testing::ReturnRef;
 using testing::OpenScenarioEngine::v1_3::GetScenariosPath;
@@ -87,18 +90,22 @@ protected:
     auto& mockTrafficAreaService{dynamic_cast<mantle_api::MockTrafficAreaService&>(env_->GetTrafficAreaService())};
 
     ON_CALL(stream, GetLength()).WillByDefault(Return(500_m));
-    ON_CALL(stream, Convert(testing::A<const mantle_api::ITrafficAreaStream::StreamPose&>())).WillByDefault(Return(mantle_api::Pose{mantle_api::Vec3<units::length::meter_t>{100_m, 0_m, 0_m}, mantle_api::Orientation3<units::angle::radian_t>{}}));  // clang-format off
-    ON_CALL(mockTrafficAreaService, CreateTrafficArea(_)).WillByDefault([this]() -> std::vector<std::shared_ptr<mantle_api::ITrafficAreaStream>> {  // 
+    ON_CALL(stream, Convert(testing::A<const mantle_api::ITrafficAreaStream::StreamPose&>())).WillByDefault([](const mantle_api::ITrafficAreaStream::StreamPose& pose) {  // clang-format off
+      return mantle_api::Pose{mantle_api::Vec3<units::length::meter_t>{pose.s, pose.t, 0_m}, mantle_api::Orientation3<units::angle::radian_t>{}};
+    });
+    ON_CALL(mockTrafficAreaService, CreateTrafficArea(_)).WillByDefault([this]() -> std::vector<std::shared_ptr<mantle_api::ITrafficAreaStream>> {  //
       return streams;
+    });                                                                                                            // clang-format on
+    auto& entity_repository{static_cast<mantle_api::MockEntityRepository&>(env_->GetEntityRepository())};  // clang-format off
+    ON_CALL(entity_repository, Create(testing::A<const std::string&>(), testing::A<const mantle_api::VehicleProperties&>())).WillByDefault([this](const std::string&, const mantle_api::VehicleProperties&) -> NiceMock<MockVehicle>& {
+      return mock_vehicles.emplace_back();
     });  // clang-format on
-    auto& entity_repository{static_cast<mantle_api::MockEntityRepository&>(env_->GetEntityRepository())};
-    ON_CALL(entity_repository, Create(testing::A<const std::string&>(), testing::A<const mantle_api::VehicleProperties&>())).WillByDefault(ReturnRef(mock_vehicle));
   }
 
   std::vector<std::shared_ptr<mantle_api::ITrafficAreaStream>> streams;
   std::shared_ptr<testing::OpenScenarioEngine::v1_3::TestLogger> LOGGER;
 
-  MockVehicle mock_vehicle;
+  std::vector<NiceMock<MockVehicle>> mock_vehicles;
 
   std::string controller_name{"TestController"};
 };
@@ -110,5 +117,16 @@ TEST_F(TrafficAreaActionTestFixture, GivenScenarioWithTrafficAreaAction_WhenInit
   engine.Init();
   engine.SetupDynamicContent();
   engine.Step({});
-  EXPECT_EQ(mock_vehicle.xyz.x, 100_m);
+  const size_t offset{1};  // SetupDynamicContent spawns one entity, which we want to ignore (env_->GetEntityRepository().GetEntities().size() does not work)
+  // The initial area is 500m long. The vehicle is 5_m long.
+  // The last place its center can be placed at is 500 - 5/2 = 497.5
+  EXPECT_EQ(mock_vehicles[0 + offset].xyz.x, 497.5_m);
+  // The prior vehicle starts at 497.5 - 5/2 = 495
+  // The last place the next vehicle's center can be placed at is 495 - 5/2 = 492.5
+  // If its randomly sampled velocity is greater than that of the prior vehicle, it may be placed even further back
+  EXPECT_LE(mock_vehicles[1 + offset].xyz.x, 492.5_m);
+  for (auto vehicle{std::next(mock_vehicles.begin(), offset + 1)}; vehicle < mock_vehicles.end(); ++vehicle)
+  {
+    EXPECT_LE(vehicle->xyz.x, std::prev(vehicle)->xyz.x - 5_m);
+  }
 }
-- 
GitLab


From f2fa931eb7ebef0f254141a67df3bb56c86003cd Mon Sep 17 00:00:00 2001
From: Noah Schick <noah.schick@in-tech.com>
Date: Wed, 19 Mar 2025 17:55:48 +0100
Subject: [PATCH 21/26] Pass ScenarioObjectTemplate through TrafficAreaAction

Doing so makes the list of controllers available when spawning
---
 engine/cmake/generated_files.cmake            |  1 +
 .../ConvertScenarioTrafficDistribution.cpp    |  2 +-
 .../ConvertScenarioTrafficDistribution.h      |  2 +-
 .../GenericAction/TrafficAreaAction.cpp       | 22 +------
 .../GenericAction/TrafficAreaAction.h         | 10 +++
 engine/src/Utils/Spawning/Length.cpp          | 10 +--
 engine/src/Utils/Spawning/Length.h            | 28 +++++++-
 engine/src/Utils/Spawning/SpawnSpace.cpp      | 24 +++----
 engine/src/Utils/Spawning/SpawnSpace.h        | 49 +++++++-------
 engine/src/Utils/Spawning/VelocityRange.cpp   |  5 ++
 engine/src/Utils/Spawning/VelocityRange.h     |  1 +
 engine/src/Utils/Spawning/Weight.cpp          |  7 --
 engine/src/Utils/Spawning/Weight.h            | 13 +++-
 .../{WeightedEntity.h => Weighted.cpp}        | 19 +++---
 engine/src/Utils/Spawning/Weighted.h          | 65 +++++++++++++++++++
 15 files changed, 169 insertions(+), 89 deletions(-)
 rename engine/src/Utils/Spawning/{WeightedEntity.h => Weighted.cpp} (57%)
 create mode 100644 engine/src/Utils/Spawning/Weighted.h

diff --git a/engine/cmake/generated_files.cmake b/engine/cmake/generated_files.cmake
index 1c4c3ab5..4750e194 100644
--- a/engine/cmake/generated_files.cmake
+++ b/engine/cmake/generated_files.cmake
@@ -239,6 +239,7 @@ list(APPEND ${PROJECT_NAME}_SOURCES
     src/Utils/Spawning/SpawnZone.cpp
     src/Utils/Spawning/VelocityRange.cpp
     src/Utils/Spawning/Weight.cpp
+    src/Utils/Spawning/Weighted.cpp
     src/Utils/ConditionEdgeEvaluator.cpp
     src/Utils/ControllerCreator.cpp
     src/Utils/ControllerService.cpp
diff --git a/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.cpp b/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.cpp
index 1fd3d7a3..8f2cdb17 100644
--- a/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.cpp
+++ b/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.cpp
@@ -33,7 +33,7 @@ struct ToTrafficDistribution
   TrafficDistribution operator()(const NET_ASAM_OPENSCENARIO::v1_3::ITrafficDistributionEntry& entry) const
   {
     return {TransformSharedPointers(entry.GetEntityDistribution()->GetEntityDistributionEntry(), [](const NET_ASAM_OPENSCENARIO::v1_3::IEntityDistributionEntry& entry)
-                                    { return entry.GetScenarioObjectTemplate()->GetEntitiyObject(); }),  // https://github.com/RA-Consulting-GmbH/openscenario.api.test/issues/222
+                                    { return entry.GetScenarioObjectTemplate(); }),  // https://github.com/RA-Consulting-GmbH/openscenario.api.test/issues/222
             entry.GetWeight()};
   }
 };
diff --git a/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.h b/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.h
index 5442081e..2149edf1 100644
--- a/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.h
+++ b/engine/src/Conversion/OscToMantle/ConvertScenarioTrafficDistribution.h
@@ -20,7 +20,7 @@
 
 namespace OpenScenarioEngine::v1_3
 {
-struct TrafficDistribution : Iterable<std::vector<std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IEntityObject>>>
+struct TrafficDistribution : Iterable<std::vector<std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IScenarioObjectTemplate>>>
 {
   units::dimensionless::scalar_t weight;
 };
diff --git a/engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp b/engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp
index 3c7d47a7..af2ab558 100644
--- a/engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp
+++ b/engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp
@@ -29,23 +29,6 @@ void TrafficAreaAction::UpdateTrafficAreas()
 
 bool TrafficAreaAction::Step()
 {
-  // Spawn entities in a randomly selected area among this action's defined traffic areas.
-  // Selection is weighted based on the length of each area's remaining spawnable area (SpawnZone).
-
-  // Traffic distributions determine which vehicles can spawn and their likelihood of spawning. The distributions are sorted by
-  // entity length for efficient filtering in case the largest interval grows smaller than some spawnable entities. Similary, a
-  // SpawnZone tracks its largest interval (without sorting, as it would have to be reordered each time an entity is spawned).
-
-  // The general process of the spawn loop must go as follows:
-  // - Filter out the entities of all traffic distributions longer than the largest interval of all areas.
-  //   (can be skipped if the last spawned entity was not on the largest interval)
-  // - Sample the entity to be spawned based on the weights of all entity distributions
-  // - Filter out the intervals of all areas shorter than the entity to be spawned
-  // - Sample the spawn position from the total length of all remaining intervals
-  // - Shrink the sampled interval interval by the front and rear offset from the entity's center to its
-  //   front and rear bounds, scaling the sampled value accordingly.
-  // - Spawn the entity and update the computed area intervals and lengths.
-
   if (values.numberOfEntities == 0)  // No entities left to spawn
   {
     return true;
@@ -88,7 +71,8 @@ yase::NodeStatus TrafficAreaAction::tick()
 
 void TrafficAreaAction::lookupAndRegisterData(yase::Blackboard& blackboard)
 {
-  std::shared_ptr<mantle_api::IEnvironment> environment = blackboard.get<std::shared_ptr<mantle_api::IEnvironment>>("Environment");
+  auto environment = blackboard.get<std::shared_ptr<mantle_api::IEnvironment>>("Environment");
+  std::shared_ptr<IControllerService> controller_service = blackboard.get<std::shared_ptr<IControllerService>>("ControllerService");
 
   impl_ = std::make_unique<OpenScenarioEngine::v1_3::TrafficAreaAction>(
       Values<OpenScenarioEngine::v1_3::TrafficAreaAction>{
@@ -96,7 +80,7 @@ void TrafficAreaAction::lookupAndRegisterData(yase::Blackboard& blackboard)
           action_->GetNumberOfEntities(),
           ConvertScenarioTrafficDistribution(action_->GetTrafficDistribution()),
           ConvertScenarioTrafficArea(action_->GetTrafficArea())},
-      Interfaces<OpenScenarioEngine::v1_3::TrafficAreaAction>{environment});
+      Interfaces<OpenScenarioEngine::v1_3::TrafficAreaAction>{environment, controller_service});
 }
 }  // namespace Node
 }  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Storyboard/GenericAction/TrafficAreaAction.h b/engine/src/Storyboard/GenericAction/TrafficAreaAction.h
index 22cd970d..13633df5 100644
--- a/engine/src/Storyboard/GenericAction/TrafficAreaAction.h
+++ b/engine/src/Storyboard/GenericAction/TrafficAreaAction.h
@@ -24,6 +24,7 @@
 
 #include "Action.h"
 #include "Conversion/OscToMantle/ConvertScenarioTrafficDistribution.h"
+#include "Utils/IControllerService.h"
 
 namespace OpenScenarioEngine::v1_3
 {
@@ -38,6 +39,15 @@ struct Values<TrafficAreaAction>
   std::vector<mantle_api::RoadRange> trafficArea;
 };
 
+template <>
+struct Interfaces<TrafficAreaAction>
+{
+  std::shared_ptr<mantle_api::IEnvironment> environment;
+  std::shared_ptr<IControllerService> controller_service;
+
+  // void CreateController(mantle_api::IEntity& entity);
+};
+
 class TrafficAreaAction : public Action<TrafficAreaAction>
 {
 public:
diff --git a/engine/src/Utils/Spawning/Length.cpp b/engine/src/Utils/Spawning/Length.cpp
index 03706dd3..1d2a41e6 100644
--- a/engine/src/Utils/Spawning/Length.cpp
+++ b/engine/src/Utils/Spawning/Length.cpp
@@ -29,11 +29,6 @@ units::length::meter_t ToLength::operator()(const mantle_api::TrafficArea& area)
   return std::transform_reduce(area.begin(), area.end(), units::length::meter_t{}, std::plus<>{}, *this);
 }
 
-units::length::meter_t ToLength::operator()(const WeightedEntity& entity) const
-{
-  return (*this)(entity.handle);
-}
-
 units::length::meter_t ToLength::operator()(const mantle_api::IEntity& entity) const
 {
   return (*this)(entity.GetProperties());
@@ -107,4 +102,9 @@ units::length::meter_t ToLength::operator()(const NET_ASAM_OPENSCENARIO::v1_3::I
   }
   throw std::runtime_error("ToLength: Entity is not a catalog reference, vehicle, pedestrian or misc. object.");
 }
+
+units::length::meter_t ToLength::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IScenarioObjectTemplate& input) const
+{
+  return (*this)(input.GetEntitiyObject());
+}
 }  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/Length.h b/engine/src/Utils/Spawning/Length.h
index e5c9702d..47d7de9c 100644
--- a/engine/src/Utils/Spawning/Length.h
+++ b/engine/src/Utils/Spawning/Length.h
@@ -22,11 +22,10 @@
 #include "Interval.h"
 #include "SpawnZone.h"
 #include "Transform.h"
+#include "Weighted.h"
 
 namespace OpenScenarioEngine::v1_3
 {
-struct WeightedEntity;
-
 struct ToLength
 {
   constexpr units::length::meter_t operator()(units::length::meter_t) const;
@@ -38,7 +37,8 @@ struct ToLength
   template <typename Type>
   units::length::meter_t operator()(const std::shared_ptr<Type>&) const;
 
-  units::length::meter_t operator()(const WeightedEntity&) const;
+  template <typename Type>
+  units::length::meter_t operator()(const Weighted<Type>&) const;
 
   units::length::meter_t operator()(const mantle_api::IEntity&) const;
 
@@ -62,6 +62,8 @@ struct ToLength
   units::length::meter_t operator()(const SpawnZones&) const;
 
   units::length::meter_t operator()(const NET_ASAM_OPENSCENARIO::v1_3::IEntityObject&) const;
+
+  units::length::meter_t operator()(const NET_ASAM_OPENSCENARIO::v1_3::IScenarioObjectTemplate&) const;
 };
 
 struct ToSize
@@ -85,6 +87,13 @@ struct ToAccumulatedLength
 
   units::length::meter_t totalLength;
 };
+
+/// Returns the positive distance from the entity's center to its rear and the positive distance from the entity's center to its front.
+///
+/// \tparam Type Type for which GetLength(const Type&) is implemented
+/// \return Positive distance from the entity's center to its rear and the positive distance from the entity's center to its front
+template <typename Type>
+Padding GetLengthPadding(const Type&);
 }  // namespace OpenScenarioEngine::v1_3
 
 namespace OpenScenarioEngine::v1_3
@@ -105,6 +114,12 @@ units::length::meter_t ToLength::operator()(const std::shared_ptr<Type>& input)
   return ToLength{}(*input);
 }
 
+template <typename Type>
+units::length::meter_t ToLength::operator()(const Weighted<Type>& input) const
+{
+  return ToLength{}(input.handle);
+}
+
 template <typename Type>
 units::length::meter_t ToLength::operator()(const Type* input) const
 {
@@ -140,4 +155,11 @@ units::length::meter_t ToAccumulatedLength::operator()(const Type& input)
   totalLength += GetLength(input);
   return totalLength;
 }
+
+template <typename Input>
+Padding GetLengthPadding(const Input& input)
+{  // TODO: Consider the center offset of the entity's bounds
+  const units::length::meter_t half_length{GetLength(input) * units::dimensionless::scalar_t{.5}};
+  return {half_length, half_length};
+}
 }  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/SpawnSpace.cpp b/engine/src/Utils/Spawning/SpawnSpace.cpp
index b8e77eed..889f8f44 100644
--- a/engine/src/Utils/Spawning/SpawnSpace.cpp
+++ b/engine/src/Utils/Spawning/SpawnSpace.cpp
@@ -30,7 +30,7 @@ SpawnSpace::SpawnSpace(const OpenScenarioEngine::v1_3::TrafficDistributions &dis
   for (auto &distribution : distributions)
   {
     std::transform(distribution.begin(), distribution.end(), std::back_inserter(entities),  //
-                   [weight = distribution.weight](const Entity &entity) -> WeightedEntity
+                   [weight = distribution.weight](const ObjectTemplate &entity) -> Weighted<ObjectTemplate>
                    { return {entity, weight}; });
   }
   std::sort(entities.begin(), entities.end(), Less<ToLength>{});
@@ -47,11 +47,11 @@ SequentialSpawnSpace::SequentialSpawnSpace(const std::vector<mantle_api::Traffic
   });  // clang-format on
 }
 
-Entity SequentialSpawnSpace::SampleEntity(StochasticsInterface &rng) const
+ObjectTemplate SequentialSpawnSpace::SampleObjectTemplate(StochasticsInterface &rng) const
 {
   if (entities.size() <= 1)
   {
-    return entities.empty() ? Entity{} : entities.front().handle;
+    return entities.empty() ? ObjectTemplate{} : entities.front().handle;
   }
   assert(weights.size() == entities.size() + 1);
   const units::dimensionless::scalar_t sampled_weight{rng.GetUniformDistributed(.0, weights.back().value())};
@@ -110,13 +110,13 @@ DistributedSpawnSpace::DistributedSpawnSpace(const OpenScenarioEngine::v1_3::Tra
   }
 }
 
-Entity DistributedSpawnSpace::SampleEntity(StochasticsInterface &rng) const
+ObjectTemplate DistributedSpawnSpace::SampleObjectTemplate(StochasticsInterface &rng) const
 {
   const units::length::meter_t max_length{GetMaxIntervalLength()};
   const auto entity_end{FindSpawnableEntityEnd(max_length)};
   if (std::distance(entities.begin(), entity_end) <= 1)
   {
-    return entity_end == entities.begin() ? Entity{} : entities.front().handle;
+    return entity_end == entities.begin() ? ObjectTemplate{} : entities.front().handle;
   }
   assert(weights.size() == entities.size() + 1);
   const units::dimensionless::scalar_t max_weight{weights[static_cast<size_t>(std::distance(entities.begin(), entity_end))]};
@@ -129,14 +129,14 @@ units::length::meter_t DistributedSpawnSpace::GetMaxIntervalLength() const
   return spots.empty() ? units::length::meter_t{} : GetLength(*spots.rbegin());
 }
 
-std::vector<WeightedEntity>::const_iterator DistributedSpawnSpace::FindSpawnableEntityEnd(units::length::meter_t threshold) const
+std::vector<Weighted<ObjectTemplate>>::const_iterator DistributedSpawnSpace::FindSpawnableEntityEnd(units::length::meter_t threshold) const
 {
-  return std::upper_bound(entities.begin(), entities.end(), threshold, [](units::length::meter_t threshold, const WeightedEntity &entity)
+  return std::upper_bound(entities.begin(), entities.end(), threshold, [](units::length::meter_t threshold, const Weighted<ObjectTemplate> &entity)
                           { return threshold < GetLength(entity); });
 }
-std::vector<WeightedEntity>::iterator DistributedSpawnSpace::FindSpawnableEntityEnd(units::length::meter_t threshold)
+std::vector<Weighted<ObjectTemplate>>::iterator DistributedSpawnSpace::FindSpawnableEntityEnd(units::length::meter_t threshold)
 {
-  return std::upper_bound(entities.begin(), entities.end(), threshold, [](units::length::meter_t threshold, const WeightedEntity &entity)
+  return std::upper_bound(entities.begin(), entities.end(), threshold, [](units::length::meter_t threshold, const Weighted<ObjectTemplate> &entity)
                           { return threshold < GetLength(entity); });
 }
 
@@ -171,12 +171,6 @@ std::set<SpawnSpot, Less<ToLength>>::const_iterator DistributedSpawnSpace::Sampl
   return Sample(candidates, lengths, sampledLength);
 }
 
-Padding GetLengthPadding(const Entity &entity)
-{  // TODO: Consider the center offset of the entity's bounds
-  const units::length::meter_t half_length{GetLength(entity) * units::dimensionless::scalar_t{.5}};
-  return {half_length, half_length};
-}
-
 void UpdateSpot(std::vector<OpenScenarioEngine::v1_3::SpawnSpot> &stream,
                 std::vector<OpenScenarioEngine::v1_3::SpawnSpot>::iterator spot,
                 const Interval &occupied_space,
diff --git a/engine/src/Utils/Spawning/SpawnSpace.h b/engine/src/Utils/Spawning/SpawnSpace.h
index faef6325..0c485089 100644
--- a/engine/src/Utils/Spawning/SpawnSpace.h
+++ b/engine/src/Utils/Spawning/SpawnSpace.h
@@ -25,7 +25,7 @@
 #include "SpawnSpot.h"
 #include "SpawnZone.h"
 #include "VelocityRange.h"
-#include "WeightedEntity.h"
+#include "Weighted.h"
 
 namespace OpenScenarioEngine::v1_3
 {
@@ -33,7 +33,7 @@ struct SpawnSpace
 {
   SpawnSpace(const OpenScenarioEngine::v1_3::TrafficDistributions &);
 
-  std::vector<WeightedEntity> entities;                 // Entities sorted by ascending length
+  std::vector<Weighted<ObjectTemplate>> entities;       // Object templates (entity + controllers) sorted by ascending length
   std::vector<units::dimensionless::scalar_t> weights;  // Used to sample an entity
 };
 
@@ -48,8 +48,8 @@ struct SequentialSpawnSpace : SpawnSpace
 
   /// Samples and returns a random entity without considering the remaining space.
   ///
-  /// \return Entity Random entity out of this space's entities. If it has none, a nullptr is returned.
-  Entity SampleEntity(StochasticsInterface &) const;
+  /// \return Random entity out of this space's entities. If it has none, a nullptr is returned.
+  ObjectTemplate SampleObjectTemplate(StochasticsInterface &) const;
 
   /// Tries to spawn an entity and returns whether spawning was successful.
   /// If successful, also adjusts the spawn space's intervals.
@@ -80,9 +80,15 @@ struct DistributedSpawnSpace : SpawnSpace
 
   /// Returns the entity out of the set of entities in this space based on the given sample
   ///
-  Entity GetEntity(units::dimensionless::scalar_t sample);
+  /// \param sample Value within the accumulated weights of each entity of this space
+  /// \return Blueprint for spawning an entity alongside controllers for that entity
+  ObjectTemplate GetObjectTemplate(units::dimensionless::scalar_t sample);
 
-  Entity SampleEntity(StochasticsInterface &) const;
+  //! Returns a random object template (entity + controllers) using the given number generator
+  //!
+  //! \param StochasticsInterface Random number generator used to sample a weight
+  //! \return Randomly selected object template
+  ObjectTemplate SampleObjectTemplate(StochasticsInterface &) const;
 
   std::set<SpawnSpot, Less<ToLength>>::const_iterator SampleSpot(units::length::meter_t min_length, VelocityRange, StochasticsInterface &) const;
 
@@ -96,15 +102,12 @@ struct DistributedSpawnSpace : SpawnSpace
 
   units::length::meter_t GetMaxIntervalLength() const;
 
-  std::vector<WeightedEntity>::const_iterator FindSpawnableEntityEnd(units::length::meter_t upper_bound) const;
-  std::vector<WeightedEntity>::iterator FindSpawnableEntityEnd(units::length::meter_t upper_bound);
+  typename std::vector<Weighted<ObjectTemplate>>::const_iterator FindSpawnableEntityEnd(units::length::meter_t upper_bound) const;
+  typename std::vector<Weighted<ObjectTemplate>>::iterator FindSpawnableEntityEnd(units::length::meter_t upper_bound);
 
   std::set<SpawnSpot, Less<ToLength>> spots;
 };
 
-/// Returns the positive distance from the entity's center to its rear and the positive distance from the entity's center to its front.
-Padding GetLengthPadding(const Entity &);
-
 //! Receives a vector of spawn spots, an iterator to a spot within that vector, an interval contained by that spot and a velocity.
 //! Replaces the spot pointed to by the iterator with the disjunction of the spot's interval and the given interval.
 //! The velocities of the new spots match the given velocity at the end where they touch the given interval.
@@ -126,32 +129,30 @@ bool SequentialSpawnSpace::Spawn(EntityCreator &&spawner, StochasticsInterface &
 {
   using namespace units::literals;
 
+  static size_t i{0};
+
   if (spots.empty())
   {
     return false;
   }
-  Entity entity{SampleEntity(rng)};
-  if (entity == nullptr)
+  ObjectTemplate blueprint{SampleObjectTemplate(rng)};
+  if (blueprint == nullptr)
   {
     return false;
   }
-  const auto velocity_range{GetSpawnVelocityRange(entity)};
+  const auto velocity_range{GetSpawnVelocityRange(blueprint)};
   auto spawn_velocity{SampleVelocity(velocity_range, rng)};
   const units::time::second_t minimum_time_to_collision{2_s};
-  const Padding padding{GetLengthPadding(entity)};
+  const Padding padding{GetLengthPadding(blueprint)};
   if (auto [spot_area, spot_stream, spot, interval]{FindSpot(padding, spawn_velocity, minimum_time_to_collision)}; spot_area != spots.end())
   {
     const auto area{std::next(areas.begin(), std::distance(spots.begin(), spot_area))};
     const auto &stream{*std::next(area->begin(), std::distance(spot_area->begin(), spot_stream))};
     const mantle_api::Pose pose{stream->Convert(mantle_api::ITrafficAreaStream::StreamPose{{interval.max, {}}, {}}).value()};
-    mantle_api::IEntity &object{spawner.CreateEntity(entity, "NAME_DISCARDED_BY_TRAFFIC_AREA_ACTION")};
+    mantle_api::IEntity &object{spawner.CreateEntity(GetEntity(blueprint), "Common" + std::to_string(++i))};
     object.SetPosition(pose.position);
     object.SetOrientation(pose.orientation);
     const auto occupied_space{Interval{interval.max, interval.max}.OffsetBy(-padding.rear, padding.front)};
-    // object.SetAssignedLaneIds(areas[static_cast<size_t>(std::distance(spots.begin(), spot_area))]  // clang-format off
-    //                                [static_cast<size_t>(std::distance(spot_area->begin(), spot_stream))]->GetLanesIds(
-    //   occupied_space.min, occupied_space.max
-    // ));  // clang-format on
     UpdateSpot(*spot_stream, spot, occupied_space, spawn_velocity);
     return true;
   }
@@ -161,13 +162,13 @@ bool SequentialSpawnSpace::Spawn(EntityCreator &&spawner, StochasticsInterface &
 template <typename EntityCreator>
 bool DistributedSpawnSpace::Spawn(EntityCreator &&spawner, StochasticsInterface &rng)
 {
-  Entity entity{SampleEntity(rng)};
-  if (entity == nullptr)
+  ObjectTemplate blueprint{SampleObjectTemplate(rng)};
+  if (blueprint == nullptr)
   {
     return false;
   }
-  const auto velocity_range{GetSpawnVelocityRange(entity)};
-  const auto spot{SampleSpot(GetLength(entity), velocity_range, rng)};
+  const auto velocity_range{GetSpawnVelocityRange(blueprint)};
+  const auto spot{SampleSpot(GetLength(blueprint), velocity_range, rng)};
   if (spot == spots.end())
   {
     return false;
diff --git a/engine/src/Utils/Spawning/VelocityRange.cpp b/engine/src/Utils/Spawning/VelocityRange.cpp
index 7e028a6a..5fc6f685 100644
--- a/engine/src/Utils/Spawning/VelocityRange.cpp
+++ b/engine/src/Utils/Spawning/VelocityRange.cpp
@@ -56,6 +56,11 @@ VelocityRange ToSpawnVelocityRange::operator()(const std::shared_ptr<NET_ASAM_OP
   throw std::runtime_error("ToSpawnVelocityRange: Invalid catalog reference. Contains no vehicle, pedestrian or misc. object.");
 }
 
+VelocityRange ToSpawnVelocityRange::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IScenarioObjectTemplate &input) const
+{
+  return (*this)(input.GetEntitiyObject());
+}
+
 VelocityRange ToSpawnVelocityRange::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IVehicle &vehicle) const
 {
   return (*this)(vehicle.GetProperties());
diff --git a/engine/src/Utils/Spawning/VelocityRange.h b/engine/src/Utils/Spawning/VelocityRange.h
index 65c40a55..0ac14b0b 100644
--- a/engine/src/Utils/Spawning/VelocityRange.h
+++ b/engine/src/Utils/Spawning/VelocityRange.h
@@ -43,6 +43,7 @@ struct ToSpawnVelocityRange
   VelocityRange operator()(const std::shared_ptr<Type> &) const;
 
   VelocityRange operator()(const std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::ICatalogElement> &) const;
+  VelocityRange operator()(const NET_ASAM_OPENSCENARIO::v1_3::IScenarioObjectTemplate &) const;
   VelocityRange operator()(const NET_ASAM_OPENSCENARIO::v1_3::IEntityObject &) const;
   VelocityRange operator()(const NET_ASAM_OPENSCENARIO::v1_3::IVehicle &) const;
   VelocityRange operator()(const NET_ASAM_OPENSCENARIO::v1_3::IPedestrian &) const;
diff --git a/engine/src/Utils/Spawning/Weight.cpp b/engine/src/Utils/Spawning/Weight.cpp
index 96774fbc..571157b6 100644
--- a/engine/src/Utils/Spawning/Weight.cpp
+++ b/engine/src/Utils/Spawning/Weight.cpp
@@ -9,8 +9,6 @@
  ********************************************************************************/
 #include "Weight.h"
 
-#include "WeightedEntity.h"
-
 namespace OpenScenarioEngine::v1_3
 {
 units::dimensionless::scalar_t ToWeight::operator()(const NET_ASAM_OPENSCENARIO::v1_3::IScenarioObjectTemplate& object) const
@@ -66,9 +64,4 @@ units::dimensionless::scalar_t ToWeight::operator()(const NET_ASAM_OPENSCENARIO:
   }
   throw std::runtime_error("ToWeight: 'Weight' not found in properties");
 }
-
-units::dimensionless::scalar_t ToWeight::operator()(const WeightedEntity& entity) const
-{
-  return entity.weight;
-}
 }  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/Weight.h b/engine/src/Utils/Spawning/Weight.h
index e5d83854..f2ff9cdd 100644
--- a/engine/src/Utils/Spawning/Weight.h
+++ b/engine/src/Utils/Spawning/Weight.h
@@ -15,10 +15,10 @@
 
 #include <memory>
 
+#include "Weighted.h"
+
 namespace OpenScenarioEngine::v1_3
 {
-struct WeightedEntity;
-
 struct ToWeight
 {
   constexpr units::dimensionless::scalar_t operator()(units::dimensionless::scalar_t) const;
@@ -40,7 +40,8 @@ struct ToWeight
 
   units::dimensionless::scalar_t operator()(const NET_ASAM_OPENSCENARIO::v1_3::IProperties&) const;
 
-  units::dimensionless::scalar_t operator()(const WeightedEntity&) const;
+  template <typename Type>
+  units::dimensionless::scalar_t operator()(const Weighted<Type>&) const;
 };
 
 template <typename Input>
@@ -68,6 +69,12 @@ units::dimensionless::scalar_t ToWeight::operator()(const std::shared_ptr<Type>&
   return ToWeight{}(*input);
 }
 
+template <typename Type>
+units::dimensionless::scalar_t ToWeight::operator()(const Weighted<Type>& input) const
+{
+  return input.weight;
+}
+
 template <typename Input>
 units::dimensionless::scalar_t GetWeight(Input&& input)
 {
diff --git a/engine/src/Utils/Spawning/WeightedEntity.h b/engine/src/Utils/Spawning/Weighted.cpp
similarity index 57%
rename from engine/src/Utils/Spawning/WeightedEntity.h
rename to engine/src/Utils/Spawning/Weighted.cpp
index e17b8183..2702aecb 100644
--- a/engine/src/Utils/Spawning/WeightedEntity.h
+++ b/engine/src/Utils/Spawning/Weighted.cpp
@@ -7,20 +7,17 @@
  *
  * SPDX-License-Identifier: EPL-2.0
  ********************************************************************************/
-#pragma once
-
-#include <openScenarioLib/generated/v1_3/api/ApiClassInterfacesV1_3.h>
-#include <units.h>
-
-#include <memory>
+#include "Weighted.h"
 
 namespace OpenScenarioEngine::v1_3
 {
-using Entity = typename std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IEntityObject>;
+std::vector<std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IObjectController>> GetControllers(const NET_ASAM_OPENSCENARIO::v1_3::IScenarioObjectTemplate& handle)
+{
+  return handle.GetObjectController();
+}
 
-struct WeightedEntity
+std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IEntityObject> GetEntity(const NET_ASAM_OPENSCENARIO::v1_3::IScenarioObjectTemplate& handle)
 {
-  Entity handle;
-  units::dimensionless::scalar_t weight;  // Product of the distribution's weight and the weight of this entity's handle
-};
+  return handle.GetEntitiyObject();
+}
 }  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/Weighted.h b/engine/src/Utils/Spawning/Weighted.h
new file mode 100644
index 00000000..b9288b4d
--- /dev/null
+++ b/engine/src/Utils/Spawning/Weighted.h
@@ -0,0 +1,65 @@
+/********************************************************************************
+ * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+#pragma once
+
+#include <openScenarioLib/generated/v1_3/api/ApiClassInterfacesV1_3.h>
+#include <units.h>
+
+#include <memory>
+
+namespace OpenScenarioEngine::v1_3
+{
+//! \brief Shared point of a scenario object template, which is an entity paired with an arbitrary number of controllers
+using ObjectTemplate = typename std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IScenarioObjectTemplate>;
+
+template <typename Type>
+struct Weighted
+{
+  constexpr operator const Type&() const;
+  constexpr operator Type&();
+
+  Type handle;
+  units::dimensionless::scalar_t weight;
+};
+
+template <typename Type>
+std::vector<std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IObjectController>> GetControllers(const std::shared_ptr<Type>&);
+
+std::vector<std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IObjectController>> GetControllers(const NET_ASAM_OPENSCENARIO::v1_3::IScenarioObjectTemplate&);
+
+template <typename Type>
+std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IEntityObject> GetEntity(const std::shared_ptr<Type>&);
+
+std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IEntityObject> GetEntity(const NET_ASAM_OPENSCENARIO::v1_3::IScenarioObjectTemplate&);
+
+template <typename Type>
+constexpr Weighted<Type>::operator const Type&() const
+{
+  return (handle);
+}
+
+template <typename Type>
+constexpr Weighted<Type>::operator Type&()
+{
+  return (handle);
+}
+
+template <typename Type>
+std::vector<std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IObjectController>> GetControllers(const std::shared_ptr<Type>& input)
+{
+  return GetControllers(*input);
+}
+
+template <typename Type>
+std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IEntityObject> GetEntity(const std::shared_ptr<Type>& input)
+{
+  return GetEntity(*input);
+}
+}  // namespace OpenScenarioEngine::v1_3
-- 
GitLab


From 37dd227d9efbae65cff11d88ce3fe3e94e735d11 Mon Sep 17 00:00:00 2001
From: Noah Schick <noah.schick@in-tech.com>
Date: Fri, 21 Mar 2025 10:43:48 +0100
Subject: [PATCH 22/26] TrafficAreaAction: Add controllers

---
 .../GenericAction/TrafficAreaAction.cpp       | 18 +++++++++--
 .../GenericAction/TrafficAreaAction.h         |  6 ++--
 .../GenericAction/TrafficSwarmAction_impl.cpp | 14 ++------
 engine/src/Utils/ControllerCreator.cpp        |  7 ++--
 engine/src/Utils/ControllerCreator.h          |  8 ++---
 engine/src/Utils/Geometry.h                   | 28 ++++++++++++++++
 engine/src/Utils/Spawning/SpawnSpace.h        | 32 ++++++++-----------
 7 files changed, 69 insertions(+), 44 deletions(-)
 create mode 100644 engine/src/Utils/Geometry.h

diff --git a/engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp b/engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp
index af2ab558..797aa3eb 100644
--- a/engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp
+++ b/engine/src/Storyboard/GenericAction/TrafficAreaAction.cpp
@@ -1,5 +1,5 @@
 /********************************************************************************
- * Copyright (c) 2021-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ * Copyright (c) 2021-2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License 2.0 which is available at
@@ -20,6 +20,14 @@
 
 namespace OpenScenarioEngine::v1_3
 {
+mantle_api::IController& Interfaces<TrafficAreaAction>::operator()(mantle_api::IEntity& entity, const std::vector<std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IObjectController>>& controllers)
+{
+  ControllerRegistrar registrar{entity, entity.GetName(), controllers, *environment, environment->GetControllerRepository(), controller_service};
+  auto& controller{registrar.CreateDefaultController()};
+  registrar.CreateUserDefinedControllers(false);
+  return controller;
+}
+
 void TrafficAreaAction::UpdateTrafficAreas()
 {
   trafficAreas = Transform(values.trafficArea, [this](const mantle_api::RoadRange& range)
@@ -49,7 +57,7 @@ bool TrafficAreaAction::Step()
   SequentialSpawnSpace spawnSpace{trafficAreas, values.trafficDistributions};
   while (values.numberOfEntities)
   {
-    if (!spawnSpace.Spawn(spawner, rng))
+    if (!spawnSpace.Spawn(spawner, mantle, rng))
     {
       return false;
     }
@@ -72,7 +80,11 @@ yase::NodeStatus TrafficAreaAction::tick()
 void TrafficAreaAction::lookupAndRegisterData(yase::Blackboard& blackboard)
 {
   auto environment = blackboard.get<std::shared_ptr<mantle_api::IEnvironment>>("Environment");
-  std::shared_ptr<IControllerService> controller_service = blackboard.get<std::shared_ptr<IControllerService>>("ControllerService");
+  auto controller_service = std::dynamic_pointer_cast<ControllerService>(blackboard.get<std::shared_ptr<IControllerService>>("ControllerService"));
+  if (!controller_service)
+  {
+    throw std::runtime_error("TrafficAreaAction::lookupAndRegisterData: IControllerService not supported");
+  }
 
   impl_ = std::make_unique<OpenScenarioEngine::v1_3::TrafficAreaAction>(
       Values<OpenScenarioEngine::v1_3::TrafficAreaAction>{
diff --git a/engine/src/Storyboard/GenericAction/TrafficAreaAction.h b/engine/src/Storyboard/GenericAction/TrafficAreaAction.h
index 13633df5..ddf6db21 100644
--- a/engine/src/Storyboard/GenericAction/TrafficAreaAction.h
+++ b/engine/src/Storyboard/GenericAction/TrafficAreaAction.h
@@ -24,7 +24,7 @@
 
 #include "Action.h"
 #include "Conversion/OscToMantle/ConvertScenarioTrafficDistribution.h"
-#include "Utils/IControllerService.h"
+#include "Utils/ControllerService.h"
 
 namespace OpenScenarioEngine::v1_3
 {
@@ -43,9 +43,9 @@ template <>
 struct Interfaces<TrafficAreaAction>
 {
   std::shared_ptr<mantle_api::IEnvironment> environment;
-  std::shared_ptr<IControllerService> controller_service;
+  std::shared_ptr<ControllerService> controller_service;
 
-  // void CreateController(mantle_api::IEntity& entity);
+  mantle_api::IController& operator()(mantle_api::IEntity& entity, const std::vector<std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IObjectController>>& controllers);
 };
 
 class TrafficAreaAction : public Action<TrafficAreaAction>
diff --git a/engine/src/Storyboard/GenericAction/TrafficSwarmAction_impl.cpp b/engine/src/Storyboard/GenericAction/TrafficSwarmAction_impl.cpp
index 16deb16a..228cb5a3 100644
--- a/engine/src/Storyboard/GenericAction/TrafficSwarmAction_impl.cpp
+++ b/engine/src/Storyboard/GenericAction/TrafficSwarmAction_impl.cpp
@@ -15,21 +15,11 @@
 #include "MantleAPI/Traffic/i_traffic_swarm_service.h"
 #include "Utils/Ellipse.h"
 #include "Utils/EntityUtils.h"
+#include "Utils/Geometry.h"
 
 namespace detail
 {
 constexpr units::length::meter_t ELLIPSE_DETECTION_TOLERANCE{1.0};
-
-mantle_api::Vec3<units::velocity::meters_per_second_t> GetVelocityVector(
-    units::velocity::meters_per_second_t speed,
-    const mantle_api::Orientation3<units::angle::radian_t>& orientation)
-{
-  auto cos_elevation{units::math::cos(orientation.pitch)};
-  return {speed * units::math::cos(orientation.yaw) * cos_elevation,
-          speed * units::math::sin(orientation.yaw) * cos_elevation,
-          speed * -units::math::sin(orientation.pitch)};
-}
-
 }  // namespace detail
 
 namespace OpenScenarioEngine::v1_3
@@ -238,7 +228,7 @@ void TrafficSwarmAction::SpawnEntities()
     auto& entity{mantle_.environment->GetEntityRepository().Create("traffic-swarm-entity-" + std::to_string(spawned_entities_count_ + 1), vehicle_properties)};
     entity.SetPosition(spawning_pose.position);
     entity.SetOrientation(spawning_pose.orientation);
-    entity.SetVelocity(detail::GetVelocityVector(vehicle_speed, spawning_pose.orientation));
+    entity.SetVelocity(util::Rotate(vehicle_speed, spawning_pose.orientation));
 
     const auto controller_configuration{GetControllerConfiguration()};
     auto external_controller_config{std::make_unique<mantle_api::ExternalControllerConfig>(controller_configuration)};
diff --git a/engine/src/Utils/ControllerCreator.cpp b/engine/src/Utils/ControllerCreator.cpp
index 669a8875..96de78e2 100644
--- a/engine/src/Utils/ControllerCreator.cpp
+++ b/engine/src/Utils/ControllerCreator.cpp
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2021-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ * Copyright (c) 2021-2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
  *
  * This program and the accompanying materials are made
  * available under the terms of the Eclipse Public License 2.0
@@ -162,7 +162,7 @@ void ControllerCreator::CreateControllers(const std::vector<std::shared_ptr<NET_
 ControllerRegistrar::ControllerRegistrar(
     mantle_api::IEntity& entity,
     const std::string& entity_name,
-    std::vector<std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IObjectController>>&& object_controllers,
+    std::vector<std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IObjectController>> object_controllers,
     mantle_api::IEnvironment& environment,
     mantle_api::IControllerRepository& controller_repository,
     std::shared_ptr<ControllerService>& controller_service)
@@ -175,7 +175,7 @@ ControllerRegistrar::ControllerRegistrar(
 {
 }
 
-void ControllerRegistrar::CreateDefaultController()
+mantle_api::IController& ControllerRegistrar::CreateDefaultController()
 {
   Logger::Info("ControllerCreator: Setting up internal controller for entity \"" + entity_name_ + "\"");
 
@@ -186,6 +186,7 @@ void ControllerRegistrar::CreateDefaultController()
   auto& controller = controller_repository_.Create(std::move(default_config));
   controller.ChangeState(mantle_api::IController::LateralState::kActivate, mantle_api::IController::LongitudinalState::kActivate);
   RegisterDefaultController(controller);
+  return controller;
 }
 
 void ControllerRegistrar::CreateUserDefinedControllers(bool control_override)
diff --git a/engine/src/Utils/ControllerCreator.h b/engine/src/Utils/ControllerCreator.h
index abdbff2a..4542cadd 100644
--- a/engine/src/Utils/ControllerCreator.h
+++ b/engine/src/Utils/ControllerCreator.h
@@ -1,5 +1,5 @@
 /********************************************************************************
- * Copyright (c) 2021-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ * Copyright (c) 2021-2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License 2.0 which is available at
@@ -50,12 +50,12 @@ public:
   explicit ControllerRegistrar(
       mantle_api::IEntity& entity,
       const std::string& entity_name,
-      std::vector<std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IObjectController>>&& object_controllers,
+      std::vector<std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IObjectController>> object_controllers,
       mantle_api::IEnvironment& environment,
       mantle_api::IControllerRepository& controller_repository,
       std::shared_ptr<ControllerService>& controller_service);
 
-  void CreateDefaultController();
+  mantle_api::IController& CreateDefaultController();
   void CreateUserDefinedControllers(bool control_override);
 
 private:
@@ -68,7 +68,7 @@ private:
   const std::vector<std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IObjectController>> object_controllers_;
   mantle_api::IEnvironment& environment_;
   mantle_api::IControllerRepository& controller_repository_;
-  std::shared_ptr<ControllerService>& controller_service_;
+  std::shared_ptr<ControllerService> controller_service_;
 };
 
 }  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Geometry.h b/engine/src/Utils/Geometry.h
new file mode 100644
index 00000000..5e3fd697
--- /dev/null
+++ b/engine/src/Utils/Geometry.h
@@ -0,0 +1,28 @@
+/********************************************************************************
+ * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+#pragma once
+
+#include <MantleAPI/Common/orientation.h>
+#include <MantleAPI/Common/vector.h>
+#include <units.h>
+
+namespace util
+{
+template <typename Unit>
+constexpr mantle_api::Vec3<Unit> Rotate(
+    Unit magnitude,
+    const mantle_api::Orientation3<units::angle::radian_t>& orientation)
+{
+  auto cos_elevation{units::math::cos(orientation.pitch)};
+  return {magnitude * units::math::cos(orientation.yaw) * cos_elevation,
+          magnitude * units::math::sin(orientation.yaw) * cos_elevation,
+          magnitude * -units::math::sin(orientation.pitch)};
+}
+}  // namespace util
diff --git a/engine/src/Utils/Spawning/SpawnSpace.h b/engine/src/Utils/Spawning/SpawnSpace.h
index 0c485089..ade5d282 100644
--- a/engine/src/Utils/Spawning/SpawnSpace.h
+++ b/engine/src/Utils/Spawning/SpawnSpace.h
@@ -13,7 +13,6 @@
 #include <Stochastics/StochasticsInterface.h>
 #include <units.h>
 
-#include <cassert>
 #include <memory>
 #include <set>
 #include <utility>
@@ -24,6 +23,7 @@
 #include "Length.h"
 #include "SpawnSpot.h"
 #include "SpawnZone.h"
+#include "Utils/Geometry.h"
 #include "VelocityRange.h"
 #include "Weighted.h"
 
@@ -55,9 +55,11 @@ struct SequentialSpawnSpace : SpawnSpace
   /// If successful, also adjusts the spawn space's intervals.
   ///
   /// \tparam EntityCreator Provides CreateEntity(const std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IScenarioObject> &)
+  /// \tparam AttachControllers Invocable taking (mantle_api::IEntity&, std::vector<std::shared_ptr<NET_ASAM_OPENSCENARIO::v1_3::IObjectController>>)
+  ///                           used to attach controllers to the spawned entity
   /// \param StochasticsInterface Used to sample the entity and its velocity
-  template <typename EntityCreator>
-  bool Spawn(EntityCreator &&, StochasticsInterface &);
+  template <typename EntityCreator, typename AttachControllers>
+  bool Spawn(EntityCreator &&, AttachControllers &&, StochasticsInterface &);
 
   std::tuple<std::vector<std::vector<std::vector<SpawnSpot>>>::iterator,
              std::vector<std::vector<SpawnSpot>>::iterator,
@@ -65,7 +67,7 @@ struct SequentialSpawnSpace : SpawnSpace
              Interval>
   FindSpot(const Padding &, units::velocity::meters_per_second_t velocity, units::time::second_t timespan);
 
-  // Stream -> vector<SpawnSpot>, thus Area = vector<vector<SpawnSpot>>, thus vector<Area> = vector<vector<vector<SpawnSpot>>>
+  // Stream = vector<SpawnSpot>, thus Area = vector<vector<SpawnSpot>>, thus vector<Area> = vector<vector<vector<SpawnSpot>>>
   std::vector<std::vector<std::vector<SpawnSpot>>> spots;
   const std::vector<mantle_api::TrafficArea> &areas;
 };
@@ -124,8 +126,8 @@ void UpdateSpot(std::vector<OpenScenarioEngine::v1_3::SpawnSpot> &stream,
 
 namespace OpenScenarioEngine::v1_3
 {
-template <typename EntityCreator>
-bool SequentialSpawnSpace::Spawn(EntityCreator &&spawner, StochasticsInterface &rng)
+template <typename EntityCreator, typename AttachControllers>
+bool SequentialSpawnSpace::Spawn(EntityCreator &&spawner, AttachControllers &&attach_controllers, StochasticsInterface &rng)
 {
   using namespace units::literals;
 
@@ -149,9 +151,13 @@ bool SequentialSpawnSpace::Spawn(EntityCreator &&spawner, StochasticsInterface &
     const auto area{std::next(areas.begin(), std::distance(spots.begin(), spot_area))};
     const auto &stream{*std::next(area->begin(), std::distance(spot_area->begin(), spot_stream))};
     const mantle_api::Pose pose{stream->Convert(mantle_api::ITrafficAreaStream::StreamPose{{interval.max, {}}, {}}).value()};
-    mantle_api::IEntity &object{spawner.CreateEntity(GetEntity(blueprint), "Common" + std::to_string(++i))};
+    std::string name{"Common" + std::to_string(++i)};
+    mantle_api::IEntity &object{spawner.CreateEntity(GetEntity(blueprint), name)};
+    object.SetName(name);
     object.SetPosition(pose.position);
     object.SetOrientation(pose.orientation);
+    object.SetVelocity(util::Rotate(spawn_velocity, pose.orientation));
+    attach_controllers(object, blueprint->GetObjectController());
     const auto occupied_space{Interval{interval.max, interval.max}.OffsetBy(-padding.rear, padding.front)};
     UpdateSpot(*spot_stream, spot, occupied_space, spawn_velocity);
     return true;
@@ -180,16 +186,4 @@ bool DistributedSpawnSpace::Spawn(EntityCreator &&spawner, StochasticsInterface
   // spot->zone->RemoveInterval(*interval->interval, sampler(restricted_velocity.min, restricted_velocity.max));
   return true;
 }
-
-// std::pair<const SpawnZones &, const SpawnZone &> SpawnSpace::SampleZone(units::dimensionless::scalar_t sample) const
-// {
-//   assert(units::dimensionless::scalar_t{} <= sample && sample < units::dimensionless::scalar_t{1.0});
-//   const units::length::meter_t weight{sample * weights.back()};
-//   auto index{static_cast<size_t>(std::distance(weights.begin(), std::prev(std::upper_bound(std::next(weights.begin()), weights.end(), weight))))};
-//   assert(index > 1);
-//   const SpawnZones &zones{(*this)[index]};
-//   const units::length::meter_t zone_weight{weight - weights[index]};
-//   auto zone_index{static_cast<size_t>(std::distance(zones.lengths.begin(), std::prev(std::upper_bound(std::next(zones.lengths.begin()), zones.lengths.end(), zone_weight))))};
-//   return {zones, zones[zone_index]};
-// }
 }  // namespace OpenScenarioEngine::v1_3
-- 
GitLab


From a4de93c092d359150378f5b471b0f51af59a2840 Mon Sep 17 00:00:00 2001
From: Noah Schick <noah.schick@in-tech.com>
Date: Thu, 27 Mar 2025 09:15:12 +0100
Subject: [PATCH 23/26] Update MockVehicle to satisfy upadted mantle_api

---
 .../tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp   | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp b/engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp
index 594ce5c7..32713524 100644
--- a/engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp
+++ b/engine/tests/Storyboard/GenericAction/TrafficAreaActionTest.cpp
@@ -73,6 +73,9 @@ struct MockVehicle : mantle_api::IVehicle
   void SetIndicatorState(mantle_api::IndicatorState state) override {}
   mantle_api::IndicatorState GetIndicatorState() const override { return {}; }
 
+  void SetSteeringWheelAngle(units::angle::radian_t) override{};
+  units::angle::radian_t GetSteeringWheelAngle() const override { return {}; }
+
   mantle_api::Vec3<units::length::meter_t> xyz;
   mantle_api::Orientation3<units::angle::radian_t> orientation;
 };
-- 
GitLab


From 23b6d3459187042ce25fee67beddd75ef6bbf983 Mon Sep 17 00:00:00 2001
From: Noah Schick <noah.schick@in-tech.com>
Date: Thu, 27 Mar 2025 15:36:31 +0100
Subject: [PATCH 24/26] Bump openscenario_api to v1.4.1

---
 .../Storyboard/GenericAction/data/Scenarios/RoadNetwork.xodr | 0
 utils/ci/conan/conanfile.txt                                 | 2 +-
 utils/ci/conan/recipe/openscenario_api/all/conandata.yml     | 3 +++
 utils/ci/conan/recipe/openscenario_api/all/conanfile.py      | 5 -----
 utils/ci/conan/recipe/openscenario_api/config.yml            | 3 +++
 5 files changed, 7 insertions(+), 6 deletions(-)
 create mode 100644 engine/tests/Storyboard/GenericAction/data/Scenarios/RoadNetwork.xodr

diff --git a/engine/tests/Storyboard/GenericAction/data/Scenarios/RoadNetwork.xodr b/engine/tests/Storyboard/GenericAction/data/Scenarios/RoadNetwork.xodr
new file mode 100644
index 00000000..e69de29b
diff --git a/utils/ci/conan/conanfile.txt b/utils/ci/conan/conanfile.txt
index 7e405f78..f1302c20 100644
--- a/utils/ci/conan/conanfile.txt
+++ b/utils/ci/conan/conanfile.txt
@@ -1,6 +1,6 @@
 [requires]
 mantleapi/v11.0.0@openscenarioengine/testing
-openscenario_api/v1.4.0@openscenarioengine/testing
+openscenario_api/v1.4.1@openscenarioengine/testing
 stochastics/0.11.0@openscenarioengine/testing
 units/2.3.4@openscenarioengine/testing
 yase/d0c0e58d17358044cc9018c74308b45f6097ecfb@openscenarioengine/testing
diff --git a/utils/ci/conan/recipe/openscenario_api/all/conandata.yml b/utils/ci/conan/recipe/openscenario_api/all/conandata.yml
index bc5c5031..7a09463a 100644
--- a/utils/ci/conan/recipe/openscenario_api/all/conandata.yml
+++ b/utils/ci/conan/recipe/openscenario_api/all/conandata.yml
@@ -13,6 +13,9 @@
 ################################################################################
 
 sources:
+  "v1.4.1":
+    url: https://github.com/RA-Consulting-GmbH/openscenario.api.test.git
+    sha256: "a72820b00445714071d0e457afcc2112d6015c94"
   "v1.4.0":
     url: https://github.com/RA-Consulting-GmbH/openscenario.api.test.git
     sha256: "5980e8806216351fc2d749c4bfa8669367531688"
diff --git a/utils/ci/conan/recipe/openscenario_api/all/conanfile.py b/utils/ci/conan/recipe/openscenario_api/all/conanfile.py
index 4e4482b5..258f1a89 100644
--- a/utils/ci/conan/recipe/openscenario_api/all/conanfile.py
+++ b/utils/ci/conan/recipe/openscenario_api/all/conanfile.py
@@ -55,11 +55,6 @@ class OpenScenarioApiConan(ConanFile):
         self._repo_source = os.path.join(self.source_folder, self.name)
         self._artifact_path = os.path.join(self._repo_source, "cpp", "buildArtifact")
         apply_conandata_patches(self)
-        replace_in_file(self, 
-                        os.path.join(self._repo_source, "cpp/openScenarioLib/src/parser/modelgroup/XmlChoiceParser.cpp"), 
-                        "if (currentOccurs < parser->GetMaxOccur())", 
-                        "if (currentOccurs < parser->GetMaxOccur() || parser->GetMaxOccur() == -1)")
-
 
     def build(self):
         if self.settings.os == "Windows":
diff --git a/utils/ci/conan/recipe/openscenario_api/config.yml b/utils/ci/conan/recipe/openscenario_api/config.yml
index 2e807a23..1dbb91dd 100644
--- a/utils/ci/conan/recipe/openscenario_api/config.yml
+++ b/utils/ci/conan/recipe/openscenario_api/config.yml
@@ -21,3 +21,6 @@ versions:
 
   "v1.4.0":
     folder: "all"
+
+  "v1.4.1":
+    folder: "all"
-- 
GitLab


From 8e0f9dc6e76f485036cba284cf9e69b388aa4d13 Mon Sep 17 00:00:00 2001
From: Noah Schick <noah.schick@in-tech.com>
Date: Tue, 1 Apr 2025 17:19:42 +0200
Subject: [PATCH 25/26] Negation workaround for units v.2.3.1 compatibility

---
 engine/src/Utils/Spawning/Interval.h | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/engine/src/Utils/Spawning/Interval.h b/engine/src/Utils/Spawning/Interval.h
index 762a5f83..ca4494d2 100644
--- a/engine/src/Utils/Spawning/Interval.h
+++ b/engine/src/Utils/Spawning/Interval.h
@@ -119,7 +119,7 @@ constexpr units::length::meter_t Padding::GetLength() const
 
 constexpr Padding Padding::operator-() const
 {
-  return {-rear, -front};
+  return {units::length::meter_t{-rear()}, units::length::meter_t{-front()}};
 }
 
 constexpr units::length::meter_t Interval::GetLength() const
@@ -134,7 +134,7 @@ constexpr Interval Interval::GetIntersection(const Interval& other) const
 
 constexpr Interval Interval::OffsetBy(Padding padding) const
 {
-  return OffsetBy(-padding.rear, padding.front);
+  return OffsetBy(-units::length::meter_t{padding.rear()}, padding.front);
 }
 
 constexpr Interval Interval::OffsetBy(Interval other) const
-- 
GitLab


From 6defbf83bde8e0ced3e0b0f8b344175d82554c5b Mon Sep 17 00:00:00 2001
From: Noah Schick <noah.schick@in-tech.com>
Date: Wed, 9 Apr 2025 18:04:10 +0200
Subject: [PATCH 26/26] Adapt to TrafficAreaStream change

---
 .gitignore                             |  3 +-
 engine/cmake/generated_files.cmake     |  1 +
 engine/src/Utils/EntityCreator.cpp     |  1 +
 engine/src/Utils/Spawning/Common.h     | 20 +++++++++++++-
 engine/src/Utils/Spawning/Obstacle.cpp | 17 ++++++++----
 engine/src/Utils/Spawning/Obstacle.h   |  5 ++--
 engine/src/Utils/Spawning/ToEntity.cpp | 28 +++++++++++++++++++
 engine/src/Utils/Spawning/ToEntity.h   | 38 ++++++++++++++++++++++++++
 8 files changed, 103 insertions(+), 10 deletions(-)
 create mode 100644 engine/src/Utils/Spawning/ToEntity.cpp
 create mode 100644 engine/src/Utils/Spawning/ToEntity.h

diff --git a/.gitignore b/.gitignore
index 0ef100c2..a31023a5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,5 +7,6 @@ generator.log
 *.orig
 *.bkp
 .devcontainer
+compile_commands.json
 build
-deps
\ No newline at end of file
+deps
diff --git a/engine/cmake/generated_files.cmake b/engine/cmake/generated_files.cmake
index 4750e194..fe7d9855 100644
--- a/engine/cmake/generated_files.cmake
+++ b/engine/cmake/generated_files.cmake
@@ -237,6 +237,7 @@ list(APPEND ${PROJECT_NAME}_SOURCES
     src/Utils/Spawning/Obstacle.cpp
     src/Utils/Spawning/SpawnSpace.cpp
     src/Utils/Spawning/SpawnZone.cpp
+    src/Utils/Spawning/ToEntity.cpp
     src/Utils/Spawning/VelocityRange.cpp
     src/Utils/Spawning/Weight.cpp
     src/Utils/Spawning/Weighted.cpp
diff --git a/engine/src/Utils/EntityCreator.cpp b/engine/src/Utils/EntityCreator.cpp
index 0176c046..52bf1447 100644
--- a/engine/src/Utils/EntityCreator.cpp
+++ b/engine/src/Utils/EntityCreator.cpp
@@ -13,6 +13,7 @@
 
 #include <MantleAPI/Traffic/entity_properties.h>
 #include <openScenarioLib/generated/v1_3/catalog/CatalogHelperV1_3.h>
+#include <units.h>
 
 #include "Utils/Constants.h"
 
diff --git a/engine/src/Utils/Spawning/Common.h b/engine/src/Utils/Spawning/Common.h
index 4814e145..f424bc8a 100644
--- a/engine/src/Utils/Spawning/Common.h
+++ b/engine/src/Utils/Spawning/Common.h
@@ -23,7 +23,25 @@ namespace OpenScenarioEngine::v1_3
 template <typename T>
 bool is_uninitialized(const std::weak_ptr<T>& pointer)
 {
-  return !pointer.owner_before(std::weak_ptr<T>{}) && !std::weak_ptr<T>{}.owner_before(pointer);
+  return pointer.expired();
+}
+
+template <typename T>
+bool is_uninitialized(const std::shared_ptr<T>& pointer)
+{
+  return pointer == nullptr;
+}
+
+template <typename T>
+bool is_uninitialized(const std::unique_ptr<T>& pointer)
+{
+  return pointer == nullptr;
+}
+
+template <typename T>
+constexpr bool is_uninitialized(const T* pointer)
+{
+  return pointer == nullptr;
 }
 
 /// Functor returning the specified value when invoked.
diff --git a/engine/src/Utils/Spawning/Obstacle.cpp b/engine/src/Utils/Spawning/Obstacle.cpp
index fc2c97bd..84d750da 100644
--- a/engine/src/Utils/Spawning/Obstacle.cpp
+++ b/engine/src/Utils/Spawning/Obstacle.cpp
@@ -10,10 +10,15 @@
 
 #include "Obstacle.h"
 
+#include "ToEntity.h"
+
 namespace OpenScenarioEngine::v1_3
 {
-Obstacle::Obstacle(Interval interval, const std::shared_ptr<mantle_api::IEntity>& entity, units::velocity::meters_per_second_t velocity)
-    : Interval{std::move(interval)}, entity{entity}, velocity{velocity} {}
+template <typename Entity>
+Obstacle::Obstacle(Interval interval, Entity&& entity, units::velocity::meters_per_second_t velocity)
+    : Interval{std::move(interval)}, entity{&to_entity(std::forward<Entity>(entity))}, velocity{velocity}
+{
+}
 
 Obstacle Obstacle::GetStartingSentinel(units::length::meter_t value)
 {
@@ -25,7 +30,7 @@ Obstacle Obstacle::GetEndingSentinel(units::length::meter_t value)
   return {{value, value}, nullptr, units::velocity::meters_per_second_t{std::numeric_limits<double>::max()}};
 }
 
-Obstacle ToObstacle(const std::shared_ptr<mantle_api::IEntity>& entity, const mantle_api::ITrafficAreaStream& stream)
+Obstacle ToObstacle(const mantle_api::IEntity* entity, const mantle_api::ITrafficAreaStream& stream)
 {
   const auto halfLength{entity->GetProperties()->bounding_box.dimension.length / units::dimensionless::scalar_t{2.0}};
   const auto offset{entity->GetProperties()->bounding_box.geometric_center.x};
@@ -42,12 +47,12 @@ std::set<Obstacle, Less<ToMin>> GetObstacles(const mantle_api::ITrafficAreaStrea
   const units::length::meter_t max{stream.GetLength()};
   while (min < max)
   {
-    std::weak_ptr<mantle_api::IEntity> pointer{stream.GetEntity(mantle_api::ITrafficAreaStream::SearchDirection::kDownstream, min, max - min)};
-    if (is_uninitialized(pointer))
+    const mantle_api::IEntity* entity{stream.GetEntity(mantle_api::ITrafficAreaStream::SearchDirection::kDownstream, min, max - min)};
+    if (is_uninitialized(entity))
     {
       break;
     }
-    min = result.emplace(ToObstacle(pointer.lock(), stream)).first->max;
+    min = result.emplace(ToObstacle(entity, stream)).first->max;
   }
   return result;
 }
diff --git a/engine/src/Utils/Spawning/Obstacle.h b/engine/src/Utils/Spawning/Obstacle.h
index 79dae2d1..df661464 100644
--- a/engine/src/Utils/Spawning/Obstacle.h
+++ b/engine/src/Utils/Spawning/Obstacle.h
@@ -18,13 +18,14 @@ namespace OpenScenarioEngine::v1_3
 {
 struct Obstacle : Interval
 {
-  Obstacle(Interval, const std::shared_ptr<mantle_api::IEntity>&, units::velocity::meters_per_second_t);
+  template <typename Entity>
+  Obstacle(Interval, Entity&&, units::velocity::meters_per_second_t);
 
   static Obstacle GetStartingSentinel(units::length::meter_t);
 
   static Obstacle GetEndingSentinel(units::length::meter_t);
 
-  std::shared_ptr<mantle_api::IEntity> entity;
+  const mantle_api::IEntity* entity;
 
   units::velocity::meters_per_second_t velocity;
 };
diff --git a/engine/src/Utils/Spawning/ToEntity.cpp b/engine/src/Utils/Spawning/ToEntity.cpp
new file mode 100644
index 00000000..6b4a6349
--- /dev/null
+++ b/engine/src/Utils/Spawning/ToEntity.cpp
@@ -0,0 +1,28 @@
+/********************************************************************************
+ * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+#include "ToEntity.h"
+
+namespace OpenScenarioEngine::v1_3
+{
+const mantle_api::IEntity& ToEntity::operator()(const std::unique_ptr<mantle_api::IEntity>& input) const
+{
+  return (*this)(*input);
+}
+
+const mantle_api::IEntity& ToEntity::operator()(const std::shared_ptr<mantle_api::IEntity>& input) const
+{
+  return (*this)(*input);
+}
+
+const mantle_api::IEntity& ToEntity::operator()(const std::weak_ptr<mantle_api::IEntity>& input) const
+{
+  return (*this)(input.lock());
+}
+}  // namespace OpenScenarioEngine::v1_3
diff --git a/engine/src/Utils/Spawning/ToEntity.h b/engine/src/Utils/Spawning/ToEntity.h
new file mode 100644
index 00000000..e6def0ba
--- /dev/null
+++ b/engine/src/Utils/Spawning/ToEntity.h
@@ -0,0 +1,38 @@
+/********************************************************************************
+ * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+#include <MantleAPI/Traffic/i_entity.h>
+
+#include <memory>
+
+namespace OpenScenarioEngine::v1_3
+{
+struct ToEntity
+{
+  constexpr const mantle_api::IEntity& operator()(const mantle_api::IEntity&) const;
+  constexpr const mantle_api::IEntity& operator()(const mantle_api::IEntity*) const;
+  const mantle_api::IEntity& operator()(const std::unique_ptr<mantle_api::IEntity>&) const;
+  const mantle_api::IEntity& operator()(const std::shared_ptr<mantle_api::IEntity>&) const;
+  const mantle_api::IEntity& operator()(const std::weak_ptr<mantle_api::IEntity>&) const;
+};
+const ToEntity to_entity;  // NOLINT(readability-identifier-naming)
+}  // namespace OpenScenarioEngine::v1_3
+
+namespace OpenScenarioEngine::v1_3
+{
+constexpr const mantle_api::IEntity& ToEntity::operator()(const mantle_api::IEntity& input) const
+{
+  return input;
+}
+
+constexpr const mantle_api::IEntity& ToEntity::operator()(const mantle_api::IEntity* input) const
+{
+  return *input;
+}
+}  // namespace OpenScenarioEngine::v1_3
-- 
GitLab