From b8b910d32751f407fcfa77fc2bafbf543615a29f Mon Sep 17 00:00:00 2001 From: Grufoony Date: Mon, 6 Jul 2026 14:54:43 +0200 Subject: [PATCH 1/2] Replace RoadType with new mobility class --- src/dsf/mobility/Road.hpp | 42 ++------ src/dsf/mobility/RoadNetwork.cpp | 67 +++++++++---- test/mobility/Test_graph.cpp | 164 ++++++++++++++++++++++++------- 3 files changed, 188 insertions(+), 85 deletions(-) diff --git a/src/dsf/mobility/Road.hpp b/src/dsf/mobility/Road.hpp index f4d81ce1..6630f53b 100644 --- a/src/dsf/mobility/Road.hpp +++ b/src/dsf/mobility/Road.hpp @@ -2,6 +2,7 @@ #include "../base/Edge.hpp" +#include #include #include #include @@ -9,14 +10,6 @@ #include namespace dsf::mobility { - enum class RoadType : std::uint8_t { - HIGHWAY = 0, - PRIMARY = 1, - SECONDARY = 2, - TERTIARY = 3, - RESIDENTIAL = 4, - UNKNOWN = 255, - }; enum class RoadStatus : std::uint8_t { OPEN = 0, CLOSED = 1, @@ -34,7 +27,7 @@ namespace dsf::mobility { std::string m_name; bool m_hasPriority = false; std::set m_forbiddenTurns; // Stores the forbidden turns (road ids) - RoadType m_roadType = RoadType::UNKNOWN; + std::uint8_t m_mobilityClass = 0u; RoadStatus m_roadStatus = RoadStatus::OPEN; public: @@ -86,8 +79,10 @@ namespace dsf::mobility { /// @param forbiddenTurns The set of forbidden turns void setForbiddenTurns(std::set const& forbiddenTurns); /// @brief Set the road type - /// @param roadType The road type - inline void setRoadType(RoadType const roadType) { m_roadType = roadType; } + /// @param mobilityClass The road type + inline void setMobilityClass(std::uint8_t const mobilityClass) { + m_mobilityClass = mobilityClass; + } /// @brief Set the road status /// @param status The road status inline void setStatus(RoadStatus const status) { m_roadStatus = status; } @@ -128,27 +123,10 @@ namespace dsf::mobility { /// @details The forbidden turns are the road ids that are not allowed to be used by the agents /// when they are on the road. inline auto const& forbiddenTurns() const noexcept { return m_forbiddenTurns; } - /// @brief Get the road type - /// @return RoadType The road type - inline auto roadType() const noexcept { return m_roadType; } - /// @brief Get the string representation of the road type - /// @return std::string The string representation of the road type - constexpr std::string_view strRoadType() const { - switch (m_roadType) { - case RoadType::HIGHWAY: - return "highway"; - case RoadType::PRIMARY: - return "primary"; - case RoadType::SECONDARY: - return "secondary"; - case RoadType::TERTIARY: - return "tertiary"; - case RoadType::RESIDENTIAL: - return "residential"; - default: - return "unknown"; - } - }; + /// @brief Get the road mobility class + /// @return std::uint8_t The mobility class of the road, which is a number between 0 and 255 + inline auto mobilityClass() const noexcept { return m_mobilityClass; } + /// @brief Get the road status /// @return RoadStatus The road status inline auto roadStatus() const noexcept { return m_roadStatus; } diff --git a/src/dsf/mobility/RoadNetwork.cpp b/src/dsf/mobility/RoadNetwork.cpp index 7c4b5787..a9b26c2b 100644 --- a/src/dsf/mobility/RoadNetwork.cpp +++ b/src/dsf/mobility/RoadNetwork.cpp @@ -25,6 +25,7 @@ static constexpr auto EDGE_DEFAULT_ATTRIBUTES = "status", "coilcode", "priority", + "mobility_class", "geometry"}); namespace dsf::mobility { @@ -71,6 +72,8 @@ namespace dsf::mobility { bool const bHasForbiddenTurns = (std::find(colNames.begin(), colNames.end(), "forbidden_turns") != colNames.end()); + bool const bHasMobilityClass = + (std::find(colNames.begin(), colNames.end(), "mobility_class") != colNames.end()); for (auto& row : reader) { auto const sourceId = row["source"].get(); @@ -118,21 +121,21 @@ namespace dsf::mobility { name, polyline)); - if (!strType.empty()) { + if (!strType.empty() && !bHasMobilityClass) { std::transform( strType.begin(), strType.end(), strType.begin(), [](unsigned char c) { return std::tolower(c); }); if (strType.find("motorway") != std::string::npos) { - edge(streetId).setRoadType(RoadType::HIGHWAY); + edge(streetId).setMobilityClass(128u); } else if (strType.find("primary") != std::string::npos) { - edge(streetId).setRoadType(RoadType::PRIMARY); + edge(streetId).setMobilityClass(64u); } else if (strType.find("secondary") != std::string::npos) { - edge(streetId).setRoadType(RoadType::SECONDARY); + edge(streetId).setMobilityClass(32u); } else if (strType.find("tertiary") != std::string::npos) { - edge(streetId).setRoadType(RoadType::TERTIARY); + edge(streetId).setMobilityClass(16u); } else if (strType.find("residential") != std::string::npos) { - edge(streetId).setRoadType(RoadType::RESIDENTIAL); + edge(streetId).setMobilityClass(0u); } } @@ -194,6 +197,17 @@ namespace dsf::mobility { edge(streetId).setStatus(RoadStatus::OPEN); } } + // Handle mobility_class field if present + if (bHasMobilityClass) { + try { + auto mobilityClassValue = row["mobility_class"].get(); + edge(streetId).setMobilityClass(mobilityClassValue); + } catch (...) { + spdlog::warn("Invalid mobility_class for edge {}. Using default (0).", + streetId); + edge(streetId).setMobilityClass(0u); + } + } // Parse forbidden_turns field if present if (bHasForbiddenTurns) { // Expect a string of the form [edgeId1, edgeId2, ...] @@ -408,21 +422,34 @@ namespace dsf::mobility { edge_lanes, name, geometry)); - if (!strType.empty()) { + auto const mobilityClassResult = edge_properties.at_key("mobility_class"); + if (!mobilityClassResult.error()) { + if (mobilityClassResult.is_uint64()) { + edge(edge_id).setMobilityClass( + static_cast(mobilityClassResult.get_uint64())); + } else if (mobilityClassResult.is_int64()) { + edge(edge_id).setMobilityClass( + static_cast(mobilityClassResult.get_int64())); + } else { + spdlog::warn("Invalid mobility_class for edge {}, adding default (0u)", + edge_id); + edge(edge_id).setMobilityClass(0u); + } + } else if (!strType.empty()) { std::transform( strType.begin(), strType.end(), strType.begin(), [](unsigned char c) { return std::tolower(c); }); if (strType.find("motorway") != std::string::npos) { - edge(edge_id).setRoadType(RoadType::HIGHWAY); + edge(edge_id).setMobilityClass(128u); } else if (strType.find("primary") != std::string::npos) { - edge(edge_id).setRoadType(RoadType::PRIMARY); + edge(edge_id).setMobilityClass(64u); } else if (strType.find("secondary") != std::string::npos) { - edge(edge_id).setRoadType(RoadType::SECONDARY); + edge(edge_id).setMobilityClass(32u); } else if (strType.find("tertiary") != std::string::npos) { - edge(edge_id).setRoadType(RoadType::TERTIARY); + edge(edge_id).setMobilityClass(16u); } else if (strType.find("residential") != std::string::npos) { - edge(edge_id).setRoadType(RoadType::RESIDENTIAL); + edge(edge_id).setMobilityClass(0u); } } // Check if there is coilcode property @@ -923,17 +950,17 @@ namespace dsf::mobility { [this, &nAssigned, &nNotAssigned](auto const& pair) { auto const& pNode{pair.second}; auto const& inNeighbours{pNode->ingoingEdges()}; - // NOTE: std::multimap iterates keys in ascending order of RoadType. - // RoadType is defined so that more important roads (e.g., HIGHWAY = 0, - // PRIMARY = 1, SECONDARY = 2, ...) have smaller enum values. The logic + // NOTE: std::multimap iterates keys in descending order of std::uint8_t. + // std::uint8_t is defined so that more important roads (e.g., HIGHWAY = 128u, + // PRIMARY = 64u, SECONDARY = 32u, ...) have bigger values. The logic // below relies on this ordering to consider higher-priority road types // first when selecting streets to mark as priority roads. - std::multimap types; + std::multimap> types; for (auto const& edgeId : inNeighbours) { auto* pStreet{&this->edge(edgeId)}; - auto const roadType = pStreet->roadType(); - if (roadType != RoadType::UNKNOWN) { - types.emplace(roadType, pStreet->id()); + auto const mobilityClass = pStreet->mobilityClass(); + if (mobilityClass != 0u) { + types.emplace(mobilityClass, pStreet->id()); } } if (types.size() < 2) { @@ -1368,7 +1395,7 @@ namespace dsf::mobility { edgeRow.emplace_back(std::format("{}", pStreet->length())); edgeRow.emplace_back(std::format("{}", pStreet->maxSpeed() * 3.6)); edgeRow.emplace_back(std::format("{}", pStreet->nLanes())); - edgeRow.emplace_back(pStreet->strRoadType()); + edgeRow.emplace_back(std::format("{}", pStreet->mobilityClass())); edgeRow.emplace_back(std::format("{}", pStreet->capacity())); edgeRow.emplace_back(std::format("{}", pStreet->roadStatus())); edgeRow.emplace_back(pStreet->name()); diff --git a/test/mobility/Test_graph.cpp b/test/mobility/Test_graph.cpp index 427aa83b..ef5e20c2 100644 --- a/test/mobility/Test_graph.cpp +++ b/test/mobility/Test_graph.cpp @@ -198,20 +198,20 @@ TEST_CASE("RoadNetwork") { int nHighways = 0, nPrimary = 0, nSecondary = 0, nTertiary = 0, nResidential = 0; for (auto const& [_, pEdge] : graph.edges()) { - switch (pEdge->roadType()) { - case RoadType::HIGHWAY: + switch (pEdge->mobilityClass()) { + case 128u: ++nHighways; break; - case RoadType::PRIMARY: + case 64u: ++nPrimary; break; - case RoadType::SECONDARY: + case 32u: ++nSecondary; break; - case RoadType::TERTIARY: + case 16u: ++nTertiary; break; - case RoadType::RESIDENTIAL: + case 8u: ++nResidential; break; default: @@ -343,6 +343,104 @@ TEST_CASE("RoadNetwork") { std::filesystem::remove(tmpGeoJsonPath); } } + WHEN("We import edges from CSV with mobility_class field") { + auto const tmpCsvPath = + std::filesystem::temp_directory_path() / "dsf_mobility_class_edges_test.csv"; + { + std::ofstream tmpCsv(tmpCsvPath); + REQUIRE(tmpCsv.is_open()); + tmpCsv << "id;source;target;oneway;length;geometry;travel_time;maxspeed;nlanes;" + "type;name;mobility_class\n"; + // type says "residential" but mobility_class explicitly overrides it to 128 + tmpCsv << "300;1;2;False;100.0;LINESTRING (8.0 45.0, 8.1 45.1);12.0;30.0;1;" + "residential;mobility_class_override;128\n"; + // no override needed here, just a plain explicit value + tmpCsv << "301;2;3;False;120.0;LINESTRING (8.1 45.1, 8.2 45.2);14.4;30.0;1;" + "residential;mobility_class_plain;32\n"; + // invalid value should fall back to default (0) + tmpCsv << "302;3;4;False;140.0;LINESTRING (8.2 45.2, 8.3 45.3);16.8;30.0;1;" + "residential;mobility_class_invalid;not_a_number\n"; + } + + RoadNetwork graphWithMobilityClass; + graphWithMobilityClass.importEdges(tmpCsvPath.string()); + + THEN("mobility_class is correctly imported from CSV") { + REQUIRE_NOTHROW(graphWithMobilityClass.edge(static_cast(300))); + REQUIRE_NOTHROW(graphWithMobilityClass.edge(static_cast(301))); + REQUIRE_NOTHROW(graphWithMobilityClass.edge(static_cast(302))); + CHECK_EQ(graphWithMobilityClass.edge(static_cast(300)).mobilityClass(), + 128u); + CHECK_EQ(graphWithMobilityClass.edge(static_cast(301)).mobilityClass(), + 32u); + CHECK_EQ(graphWithMobilityClass.edge(static_cast(302)).mobilityClass(), 0u); + std::filesystem::remove(tmpCsvPath); + } + } + + WHEN("We import edges from GeoJSON with mobility_class field") { + auto const tmpGeoJsonPath = std::filesystem::temp_directory_path() / + "dsf_mobility_class_edges_test.geojson"; + { + std::ofstream tmpGeoJson(tmpGeoJsonPath); + REQUIRE(tmpGeoJson.is_open()); + tmpGeoJson << R"({ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "id": 400, + "source": 20, + "target": 21, + "length": 50.0, + "maxspeed": 30.0, + "nlanes": 1, + "type": "residential", + "name": "geo_mobility_class_override", + "mobility_class": 64 + }, + "geometry": { + "type": "LineString", + "coordinates": [[8.0, 45.0], [8.1, 45.1]] + } + }, + { + "type": "Feature", + "properties": { + "id": 401, + "source": 21, + "target": 22, + "length": 60.0, + "maxspeed": 30.0, + "nlanes": 1, + "type": "motorway", + "name": "geo_mobility_class_from_type" + }, + "geometry": { + "type": "LineString", + "coordinates": [[8.1, 45.1], [8.2, 45.2]] + } + } + ] +})"; + } + + RoadNetwork graphWithMobilityClass; + graphWithMobilityClass.importEdges(tmpGeoJsonPath.string()); + + THEN("mobility_class is correctly imported from GeoJSON") { + REQUIRE_NOTHROW(graphWithMobilityClass.edge(static_cast(400))); + REQUIRE_NOTHROW(graphWithMobilityClass.edge(static_cast(401))); + // explicit mobility_class (64) overrides the "residential" type default + CHECK_EQ(graphWithMobilityClass.edge(static_cast(400)).mobilityClass(), + 64u); + // no mobility_class given -> falls back to type-based inference ("motorway" -> 128) + CHECK_EQ(graphWithMobilityClass.edge(static_cast(401)).mobilityClass(), + 128u); + std::filesystem::remove(tmpGeoJsonPath); + } + } } SUBCASE("street") { /// GIVEN: a graph @@ -687,15 +785,15 @@ TEST_CASE("RoadNetwork") { // Node 1 is the intersection // Edge 1: 0 -> 1 (HIGHWAY) Street s1(1, std::make_pair(0, 1), 100., 30., 1); - s1.setRoadType(RoadType::HIGHWAY); + s1.setMobilityClass(128u); // HIGHWAY // Edge 2: 2 -> 1 (HIGHWAY) Street s2(2, std::make_pair(2, 1), 100., 30., 1); - s2.setRoadType(RoadType::HIGHWAY); + s2.setMobilityClass(128u); // HIGHWAY // Edge 3: 3 -> 1 (SECONDARY) Street s3(3, std::make_pair(3, 1), 100., 30., 1); - s3.setRoadType(RoadType::SECONDARY); + s3.setMobilityClass(32u); // SECONDARY graph.addStreets(s1, s2, s3); @@ -712,7 +810,7 @@ TEST_CASE("RoadNetwork") { GIVEN("A node with only one incoming edge") { RoadNetwork graph{}; Street s1(1, std::make_pair(0, 1), 100., 30., 1); - s1.setRoadType(RoadType::HIGHWAY); + s1.setMobilityClass(128u); // HIGHWAY graph.addStreets(s1); WHEN("We auto assign road priorities") { @@ -724,11 +822,11 @@ TEST_CASE("RoadNetwork") { GIVEN("A node with 3 incoming edges of the same road type") { RoadNetwork graph{}; Street s1(1, std::make_pair(0, 1), 100., 30., 1); - s1.setRoadType(RoadType::PRIMARY); + s1.setMobilityClass(64u); // PRIMARY Street s2(2, std::make_pair(2, 1), 100., 30., 1); - s2.setRoadType(RoadType::PRIMARY); + s2.setMobilityClass(64u); // PRIMARY Street s3(3, std::make_pair(3, 1), 100., 30., 1); - s3.setRoadType(RoadType::PRIMARY); + s3.setMobilityClass(64u); // PRIMARY graph.addStreets(s1, s2, s3); WHEN("We auto assign road priorities") { @@ -762,16 +860,16 @@ TEST_CASE("RoadNetwork") { RoadNetwork graph{}; // 3 HIGHWAY roads (won't match count == 2) Street s1(1, std::make_pair(0, 1), 100., 30., 1); - s1.setRoadType(RoadType::HIGHWAY); + s1.setMobilityClass(128u); // HIGHWAY Street s2(2, std::make_pair(2, 1), 100., 30., 1); - s2.setRoadType(RoadType::HIGHWAY); + s2.setMobilityClass(128u); // HIGHWAY Street s3(3, std::make_pair(3, 1), 100., 30., 1); - s3.setRoadType(RoadType::HIGHWAY); + s3.setMobilityClass(128u); // HIGHWAY // 2 SECONDARY roads (will match count == 2) Street s4(4, std::make_pair(4, 1), 100., 30., 1); - s4.setRoadType(RoadType::SECONDARY); + s4.setMobilityClass(32u); // SECONDARY Street s5(5, std::make_pair(5, 1), 100., 30., 1); - s5.setRoadType(RoadType::SECONDARY); + s5.setMobilityClass(32u); // SECONDARY graph.addStreets(s1, s2, s3, s4, s5); WHEN("We auto assign road priorities") { @@ -789,11 +887,11 @@ TEST_CASE("RoadNetwork") { GIVEN("A node with exactly 2 PRIMARY and 1 TERTIARY road") { RoadNetwork graph{}; Street s1(1, std::make_pair(0, 1), 100., 30., 1); - s1.setRoadType(RoadType::PRIMARY); + s1.setMobilityClass(64u); // PRIMARY Street s2(2, std::make_pair(2, 1), 100., 30., 1); - s2.setRoadType(RoadType::PRIMARY); + s2.setMobilityClass(64u); // PRIMARY Street s3(3, std::make_pair(3, 1), 100., 30., 1); - s3.setRoadType(RoadType::TERTIARY); + s3.setMobilityClass(16u); // TERTIARY graph.addStreets(s1, s2, s3); WHEN("We auto assign road priorities") { @@ -809,13 +907,13 @@ TEST_CASE("RoadNetwork") { GIVEN("A node with 1 HIGHWAY, 1 PRIMARY, and 2 RESIDENTIAL roads") { RoadNetwork graph{}; Street s1(1, std::make_pair(0, 1), 100., 30., 1); - s1.setRoadType(RoadType::HIGHWAY); + s1.setMobilityClass(128u); // HIGHWAY Street s2(2, std::make_pair(2, 1), 100., 30., 1); - s2.setRoadType(RoadType::PRIMARY); + s2.setMobilityClass(64u); // PRIMARY Street s3(3, std::make_pair(3, 1), 100., 30., 1); - s3.setRoadType(RoadType::RESIDENTIAL); + s3.setMobilityClass(8u); // RESIDENTIAL Street s4(4, std::make_pair(4, 1), 100., 30., 1); - s4.setRoadType(RoadType::RESIDENTIAL); + s4.setMobilityClass(8u); // RESIDENTIAL graph.addStreets(s1, s2, s3, s4); WHEN("We auto assign road priorities") { @@ -833,18 +931,18 @@ TEST_CASE("RoadNetwork") { RoadNetwork graph{}; // Node 1: 2 HIGHWAY + 1 SECONDARY Street s1(1, std::make_pair(0, 1), 100., 30., 1); - s1.setRoadType(RoadType::HIGHWAY); + s1.setMobilityClass(128u); // HIGHWAY Street s2(2, std::make_pair(2, 1), 100., 30., 1); - s2.setRoadType(RoadType::HIGHWAY); + s2.setMobilityClass(128u); // HIGHWAY Street s3(3, std::make_pair(3, 1), 100., 30., 1); - s3.setRoadType(RoadType::SECONDARY); + s3.setMobilityClass(32u); // SECONDARY // Node 5: 2 TERTIARY + 1 RESIDENTIAL Street s4(4, std::make_pair(1, 5), 100., 30., 1); - s4.setRoadType(RoadType::TERTIARY); + s4.setMobilityClass(16u); // TERTIARY Street s5(5, std::make_pair(4, 5), 100., 30., 1); - s5.setRoadType(RoadType::TERTIARY); + s5.setMobilityClass(16u); // TERTIARY Street s6(6, std::make_pair(6, 5), 100., 30., 1); - s6.setRoadType(RoadType::RESIDENTIAL); + s6.setMobilityClass(8u); // RESIDENTIAL graph.addStreets(s1, s2, s3, s4, s5, s6); WHEN("We auto assign road priorities") { @@ -875,7 +973,7 @@ TEST_CASE("RoadNetwork") { "Exported Street", dsf::geometry::PolyLine{ {dsf::geometry::Point(8.0, 45.0), dsf::geometry::Point(8.1, 45.1)}}}; - s.setRoadType(RoadType::PRIMARY); + s.setMobilityClass(64u); // PRIMARY s.setPriority(); graph.addStreet(std::move(s)); graph.addCoil(42, "coil_42"); @@ -898,7 +996,7 @@ TEST_CASE("RoadNetwork") { CHECK(edgesCsv.find("id,source,target,length,maxspeed,nlanes,type,capacity,status," "name,priority,coilcode,geometry") != std::string::npos); CHECK(edgesCsv.find("Exported Street") != std::string::npos); - CHECK(edgesCsv.find("primary") != std::string::npos); + CHECK(edgesCsv.find("64") != std::string::npos); // PRIMARY CHECK(edgesCsv.find("OPEN") != std::string::npos); CHECK(edgesCsv.find("coil_42") != std::string::npos); CHECK(edgesCsv.find("LINESTRING (") != std::string::npos); From 54bb3019fb696dbf265239f71a14608d44e99933 Mon Sep 17 00:00:00 2001 From: Grufoony Date: Mon, 6 Jul 2026 14:59:43 +0200 Subject: [PATCH 2/2] Bump version --- CITATION.cff | 4 ++-- src/dsf/dsf.hpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CITATION.cff b/CITATION.cff index 123f2f1e..f2d17b15 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -12,5 +12,5 @@ authors: repository-code: 'https://github.com/Grufoony/DynamicalSystemFramework' url: 'https://grufoony.github.io/DynamicalSystemFramework/' license: AGPL-3.0-only -version: 6.3.3 -date-released: '2026-07-01' +version: 6.4.0 +date-released: '2026-07-06' diff --git a/src/dsf/dsf.hpp b/src/dsf/dsf.hpp index 8dc7b06d..2c0ca5fc 100644 --- a/src/dsf/dsf.hpp +++ b/src/dsf/dsf.hpp @@ -8,8 +8,8 @@ #include static constexpr uint8_t DSF_VERSION_MAJOR = 6; -static constexpr uint8_t DSF_VERSION_MINOR = 3; -static constexpr uint8_t DSF_VERSION_PATCH = 3; +static constexpr uint8_t DSF_VERSION_MINOR = 4; +static constexpr uint8_t DSF_VERSION_PATCH = 0; static auto const DSF_VERSION = std::format("{}.{}.{}", DSF_VERSION_MAJOR, DSF_VERSION_MINOR, DSF_VERSION_PATCH);