diff options
| author | Rutger Broekhoff | 2026-08-29 12:01:42 +0200 |
|---|---|---|
| committer | Rutger Broekhoff | 2026-08-29 12:01:42 +0200 |
| commit | 52b3cd46c76fd4be18aeb8422a08a073484b9fad (patch) | |
| tree | b4f23957c096e6bce5369bb94a4e8e23de71761a /server/src/datex2.cpp | |
| parent | 35878b76049b9c3e752480e9f298b7e2da6fdf1f (diff) | |
| download | routemon-52b3cd46c76fd4be18aeb8422a08a073484b9fad.tar.gz routemon-52b3cd46c76fd4be18aeb8422a08a073484b9fad.zip | |
More module implementation partition units
Diffstat (limited to 'server/src/datex2.cpp')
| -rw-r--r-- | server/src/datex2.cpp | 377 |
1 files changed, 377 insertions, 0 deletions
diff --git a/server/src/datex2.cpp b/server/src/datex2.cpp new file mode 100644 index 0000000..4c8000b --- /dev/null +++ b/server/src/datex2.cpp | |||
| @@ -0,0 +1,377 @@ | |||
| 1 | module; | ||
| 2 | |||
| 3 | #include <boost/geometry/algorithms/is_empty.hpp> | ||
| 4 | #include <boost/geometry/srs/epsg.hpp> | ||
| 5 | #include <boost/geometry/srs/transformation.hpp> | ||
| 6 | |||
| 7 | #include <pugixml.hpp> | ||
| 8 | |||
| 9 | module routemon:datex2$impl; | ||
| 10 | |||
| 11 | import :datex2; | ||
| 12 | |||
| 13 | using namespace std::literals::string_view_literals; | ||
| 14 | |||
| 15 | namespace routemon::datex2 { | ||
| 16 | |||
| 17 | auto parse_timestamp(char const* in) -> std::optional<time::timestamp> | ||
| 18 | { | ||
| 19 | auto res = time::timestamp{}; | ||
| 20 | auto is = std::istringstream{in}; | ||
| 21 | is >> std::chrono::parse("%Y-%m-%dT%H:%M:%SZ", res); | ||
| 22 | return is.fail() ? std::nullopt : std::make_optional(res); | ||
| 23 | } | ||
| 24 | |||
| 25 | auto loader::add_location_from_xml( | ||
| 26 | road_closure& rc, pugi::xml_node const& loc_xml) -> void | ||
| 27 | { | ||
| 28 | auto loc_xml_type = std::string_view{loc_xml.attribute("xsi:type").value()}; | ||
| 29 | if (loc_xml_type == "loc:ItineraryByIndexedLocations") | ||
| 30 | { | ||
| 31 | for (auto const loc_cont_xml : | ||
| 32 | loc_xml.children("loc:locationContainedInItinerary")) | ||
| 33 | { | ||
| 34 | add_location_from_xml(rc, loc_cont_xml.child("loc:location")); | ||
| 35 | } | ||
| 36 | } | ||
| 37 | else if (loc_xml_type == "loc:LinearLocation" | ||
| 38 | || loc_xml_type == "loc:SingleRoadLinearLocation") | ||
| 39 | { | ||
| 40 | auto const& loc_gml_xml = loc_xml.child("loc:gmlLineString"); | ||
| 41 | if (!loc_gml_xml) | ||
| 42 | return; | ||
| 43 | |||
| 44 | auto const srs_name = | ||
| 45 | std::string_view{loc_gml_xml.attribute("srsName").value()}; | ||
| 46 | if (srs_name != "WGS 84"sv) | ||
| 47 | { | ||
| 48 | warnings_.insert( | ||
| 49 | std::format("don't now how to handle the CRS {}", srs_name)); | ||
| 50 | return; | ||
| 51 | } | ||
| 52 | auto const pos_list_str = | ||
| 53 | std::string_view{loc_gml_xml.child_value("loc:posList")}; | ||
| 54 | // lat1 long1 lat2 long2 ... lat(n-1) long(n-1) latn longn | ||
| 55 | |||
| 56 | auto ls = std::make_shared<geo::linestring>(); | ||
| 57 | |||
| 58 | auto lat_set = false; | ||
| 59 | auto lat = 0.0; | ||
| 60 | for (auto const lat_or_long_str : std::views::split(pos_list_str, " "sv)) | ||
| 61 | { | ||
| 62 | auto mlat_or_long = util::parse_double(std::string_view{lat_or_long_str}); | ||
| 63 | if (!mlat_or_long) | ||
| 64 | { | ||
| 65 | warnings_.insert( | ||
| 66 | std::format( | ||
| 67 | "failed to parse coordinate {:?}", | ||
| 68 | std::string_view{lat_or_long_str})); | ||
| 69 | return; | ||
| 70 | } | ||
| 71 | |||
| 72 | if (!lat_set) | ||
| 73 | { | ||
| 74 | lat = *mlat_or_long; | ||
| 75 | lat_set = true; | ||
| 76 | } | ||
| 77 | else | ||
| 78 | { | ||
| 79 | bgeo::append(*ls, geo::point{*mlat_or_long, lat}); | ||
| 80 | lat = 0; | ||
| 81 | lat_set = false; | ||
| 82 | } | ||
| 83 | } | ||
| 84 | |||
| 85 | if (bgeo::is_empty(*ls)) | ||
| 86 | { | ||
| 87 | warnings_.emplace("empty line string in data set"); | ||
| 88 | return; | ||
| 89 | } | ||
| 90 | |||
| 91 | rc.relevant_line_strings.push_back(ls); | ||
| 92 | } | ||
| 93 | else if (loc_xml_type == "loc:PointLocation") | ||
| 94 | { | ||
| 95 | auto const& coords_xml = | ||
| 96 | loc_xml.child("loc:pointByCoordinates").child("loc:pointCoordinates"); | ||
| 97 | if (!coords_xml) | ||
| 98 | return; | ||
| 99 | |||
| 100 | auto mlat = util::parse_double(coords_xml.child_value("loc:latitude")); | ||
| 101 | auto mlon = util::parse_double(coords_xml.child_value("loc:longitude")); | ||
| 102 | if (!mlat || !mlon) | ||
| 103 | { | ||
| 104 | warnings_.emplace("failed to parse PointLocation coordinates"); | ||
| 105 | return; | ||
| 106 | } | ||
| 107 | |||
| 108 | // Vaag genoeg zegt NDW dat het hier om WGS 84 gaat: | ||
| 109 | // https://docs.ndw.nu/en/dataformaten/datex2-v3/elementen/locationreferencing/pointCoordinates/ | ||
| 110 | // maar heeft het UML-model van DATEX II v3 het over ETRS 89: | ||
| 111 | // https://docs.datex2.eu/_static/data/v3.7/umlmodel/html/EARoot/EA3/EA3/EA5/EA676.htm | ||
| 112 | |||
| 113 | auto const coords_etrs89 = geo::point{*mlon, *mlat}; | ||
| 114 | auto coords_wgs84 = geo::point{}; | ||
| 115 | etrs89_to_wgs84_.forward(coords_etrs89, coords_wgs84); | ||
| 116 | |||
| 117 | rc.relevant_points.push_back(coords_wgs84); | ||
| 118 | } | ||
| 119 | else | ||
| 120 | { | ||
| 121 | warnings_.insert( | ||
| 122 | std::format( | ||
| 123 | "don't know how to hande location of type {}, ignoring", | ||
| 124 | loc_xml.attribute("xsi:type").value())); | ||
| 125 | return; | ||
| 126 | } | ||
| 127 | } | ||
| 128 | |||
| 129 | auto loader::handle_road_or_carriageway_or_lane_management( | ||
| 130 | pugi::xml_node const& record_xml, std::weak_ptr<situation> parent) | ||
| 131 | -> std::optional<std::shared_ptr<road_closure>> | ||
| 132 | { | ||
| 133 | auto const type = std::string_view{ | ||
| 134 | record_xml.child("sit:roadOrCarriagewayOrLaneManagementType").child_value() | ||
| 135 | }; | ||
| 136 | if (type != "carriagewayClosures" && type != "roadClosed") | ||
| 137 | // TODO: checken of er nog andere types fietsers de doorgang zouden | ||
| 138 | // kunnen blokkeren? | ||
| 139 | return std::nullopt; | ||
| 140 | |||
| 141 | auto const& restricted_vehicle_types_xml = | ||
| 142 | record_xml.child("sit:forVehiclesWithCharacteristicsOf"); | ||
| 143 | bool likely_restriction_for_bikes = restricted_vehicle_types_xml.empty(); | ||
| 144 | for (auto const vehicle_type_xml : | ||
| 145 | restricted_vehicle_types_xml.children("com:vehicleType")) | ||
| 146 | { | ||
| 147 | auto vehicle_type = std::string_view{vehicle_type_xml.child_value()}; | ||
| 148 | if (vehicle_type == "anyVehicle" || vehicle_type == "bicycle" | ||
| 149 | || vehicle_type == "unknown" || vehicle_type == "other") | ||
| 150 | { | ||
| 151 | likely_restriction_for_bikes = true; | ||
| 152 | } | ||
| 153 | } | ||
| 154 | if (!likely_restriction_for_bikes) | ||
| 155 | return std::nullopt; | ||
| 156 | |||
| 157 | //---- Check if within the defined validity period | ||
| 158 | |||
| 159 | auto validity = std::optional<time::period_seq>{}; | ||
| 160 | auto const& validity_xml = record_xml.child("sit:validity"); | ||
| 161 | if (validity_xml | ||
| 162 | && validity_xml.child_value("com:validityStatus") | ||
| 163 | == "definedByValidityTimeSpec"sv) | ||
| 164 | { | ||
| 165 | auto const& validity_spec_xml = | ||
| 166 | validity_xml.child("com:validityTimeSpecification"); | ||
| 167 | |||
| 168 | auto valid_periods = std::vector<time::period>{}; | ||
| 169 | auto exception_periods = std::vector<time::period>{}; | ||
| 170 | |||
| 171 | // TODO: com:overallEndTime may be missing (according to the DATEX | ||
| 172 | // II v3 data model) | ||
| 173 | auto const overall_start_time = | ||
| 174 | parse_timestamp(validity_spec_xml.child_value("com:overallStartTime")); | ||
| 175 | auto const overall_end_time = | ||
| 176 | parse_timestamp(validity_spec_xml.child_value("com:overallEndTime")); | ||
| 177 | if (overall_start_time && overall_end_time | ||
| 178 | && *overall_start_time < *overall_end_time) | ||
| 179 | { | ||
| 180 | valid_periods.emplace_back(*overall_start_time, *overall_end_time); | ||
| 181 | |||
| 182 | for (auto const valid_period_xml : | ||
| 183 | validity_xml.children("com:validPeriod")) | ||
| 184 | { | ||
| 185 | auto const start_of_period = | ||
| 186 | parse_timestamp(valid_period_xml.child_value("com:startOfPeriod")); | ||
| 187 | auto const end_of_period = | ||
| 188 | parse_timestamp(valid_period_xml.child_value("com:endOfPeriod")); | ||
| 189 | if (start_of_period && end_of_period | ||
| 190 | && *start_of_period < *end_of_period) | ||
| 191 | { | ||
| 192 | valid_periods.emplace_back(*start_of_period, *end_of_period); | ||
| 193 | } | ||
| 194 | } | ||
| 195 | for (auto const exception_period_xml : | ||
| 196 | validity_xml.children("com:exceptionPeriod")) | ||
| 197 | { | ||
| 198 | auto const start_of_period = parse_timestamp( | ||
| 199 | exception_period_xml.child_value("com:startOfPeriod")); | ||
| 200 | auto const end_of_period = parse_timestamp( | ||
| 201 | exception_period_xml.child_value("com:endOfPeriod")); | ||
| 202 | if (start_of_period && end_of_period | ||
| 203 | && *start_of_period < *end_of_period) | ||
| 204 | { | ||
| 205 | exception_periods.emplace_back(*start_of_period, *end_of_period); | ||
| 206 | } | ||
| 207 | } | ||
| 208 | |||
| 209 | validity = | ||
| 210 | time::period_seq{valid_periods.begin(), valid_periods.end()}.except( | ||
| 211 | time::period_seq{ | ||
| 212 | exception_periods.begin(), exception_periods.end() | ||
| 213 | }); | ||
| 214 | } | ||
| 215 | else | ||
| 216 | { | ||
| 217 | warnings_.insert( | ||
| 218 | std::format( | ||
| 219 | "invalid overall start / end time (start time: {}, end " | ||
| 220 | "time: {})", | ||
| 221 | validity_spec_xml.child_value("com:overallStartTime"), | ||
| 222 | validity_spec_xml.child_value("com:overallEndTime"))); | ||
| 223 | return std::nullopt; | ||
| 224 | } | ||
| 225 | } | ||
| 226 | |||
| 227 | //---- Try to extract the location info | ||
| 228 | |||
| 229 | auto rc = std::make_shared<road_closure>(std::move(parent), validity); | ||
| 230 | add_location_from_xml(*rc, record_xml.child("sit:locationReference")); | ||
| 231 | return rc; | ||
| 232 | } | ||
| 233 | |||
| 234 | auto loader::load_situation_publication(std::string const& filename) | ||
| 235 | -> situation_publication | ||
| 236 | { | ||
| 237 | auto doc = pugi::xml_document{}; | ||
| 238 | if (auto result = doc.load_file(filename.c_str()); !result) | ||
| 239 | { | ||
| 240 | throw std::runtime_error{result.description()}; | ||
| 241 | } | ||
| 242 | auto payload_xml = doc.child("mc:messageContainer").child("mc:payload"); | ||
| 243 | auto mpublication_time = | ||
| 244 | parse_timestamp(payload_xml.child_value("com:publicationTime")); | ||
| 245 | if (!mpublication_time) | ||
| 246 | throw std::runtime_error{ | ||
| 247 | "provided publication does not name publication time" | ||
| 248 | }; | ||
| 249 | |||
| 250 | auto situations = std::vector<std::shared_ptr<situation>>{}; | ||
| 251 | for (auto const sit_xml : payload_xml.children("sit:situation")) | ||
| 252 | { | ||
| 253 | auto id = std::string_view{sit_xml.attribute("id").value()}; | ||
| 254 | |||
| 255 | auto const sit = std::make_shared<situation>(std::string{id}); | ||
| 256 | situations.push_back(sit); | ||
| 257 | |||
| 258 | auto const& header_info_xml = sit_xml.child("sit:headerInformation"); | ||
| 259 | if (header_info_xml.child_value("com:informationStatus") != "real"sv) | ||
| 260 | continue; | ||
| 261 | |||
| 262 | for (auto const record_xml : sit_xml.children("sit:situationRecord")) | ||
| 263 | { | ||
| 264 | auto const record_type = | ||
| 265 | std::string_view{record_xml.attribute("xsi:type").value()}; | ||
| 266 | auto const primary_record_types = std::unordered_set<std::string_view>{ | ||
| 267 | "sit:Roadworks", | ||
| 268 | /* { */ "sit:MaintenanceWorks", | ||
| 269 | /* | */ "sit:ConstructionWorks", | ||
| 270 | /* } */ | ||
| 271 | "sit:Obstruction", | ||
| 272 | /* { */ "sit:EnvironmentalObstruction", | ||
| 273 | /* | */ "sit:GeneralObstruction", | ||
| 274 | /* | */ "sit:InfrastructureDamageObstruction", | ||
| 275 | /* } */ | ||
| 276 | "sit:Activity", | ||
| 277 | /* { */ "sit:PublicEvent", | ||
| 278 | /* } */ | ||
| 279 | }; | ||
| 280 | |||
| 281 | if (record_type == "sit:RoadOrCarriagewayOrLaneManagement") | ||
| 282 | { | ||
| 283 | if (auto rc = | ||
| 284 | handle_road_or_carriageway_or_lane_management(record_xml, sit)) | ||
| 285 | { | ||
| 286 | sit->road_closures.push_back(*rc); | ||
| 287 | } | ||
| 288 | } | ||
| 289 | else if (primary_record_types.contains(record_type)) | ||
| 290 | { | ||
| 291 | for (auto const comment_xml : | ||
| 292 | record_xml.children("sit:generalPublicComment")) | ||
| 293 | { | ||
| 294 | // if | ||
| 295 | // (comment_xml.child_value("sit:commentType") | ||
| 296 | // == "internalNote"sv) { | ||
| 297 | auto candidate = std::optional< | ||
| 298 | std::pair<std::string_view, std::string_view> | ||
| 299 | >{}; // (text, language) | ||
| 300 | for (auto const comment_value_xml : comment_xml.child("sit:comment") | ||
| 301 | .child("com:values") | ||
| 302 | .children("com:value")) | ||
| 303 | { | ||
| 304 | if (!candidate | ||
| 305 | || comment_value_xml.attribute("lang").value() == "nl"sv | ||
| 306 | || (candidate->second != "nl"sv | ||
| 307 | && comment_value_xml.attribute("lang").value() == "nl"sv)) | ||
| 308 | { | ||
| 309 | candidate = std::make_pair( | ||
| 310 | comment_value_xml.child_value(), | ||
| 311 | comment_value_xml.attribute("lang").value()); | ||
| 312 | } | ||
| 313 | } | ||
| 314 | if (candidate) | ||
| 315 | { | ||
| 316 | auto already_present = false; | ||
| 317 | for (auto const& comment : sit->comments) | ||
| 318 | already_present = already_present || comment == candidate->first; | ||
| 319 | if (!already_present) | ||
| 320 | { | ||
| 321 | sit->comments.emplace_back(candidate->first); | ||
| 322 | } | ||
| 323 | } | ||
| 324 | // } | ||
| 325 | } | ||
| 326 | |||
| 327 | if (auto const location_ref_xml = | ||
| 328 | record_xml.child("sit:locationReference")) | ||
| 329 | { | ||
| 330 | if (location_ref_xml.attribute("xsi:type").value() | ||
| 331 | == "loc:PointLocation"sv) | ||
| 332 | { | ||
| 333 | if (auto const coords_xml = | ||
| 334 | location_ref_xml.child("loc:pointByCoordinates") | ||
| 335 | .child("loc:pointCoordinates")) | ||
| 336 | { | ||
| 337 | auto const mlat = | ||
| 338 | util::parse_double(coords_xml.child_value("loc:latitude")); | ||
| 339 | auto const mlon = | ||
| 340 | util::parse_double(coords_xml.child_value("loc:longitude")); | ||
| 341 | if (mlat && mlon) | ||
| 342 | { | ||
| 343 | // Vaag genoeg zegt NDW dat het hier om WGS | ||
| 344 | // 84 gaat: | ||
| 345 | // https://docs.ndw.nu/en/dataformaten/datex2-v3/elementen/locationreferencing/pointCoordinates/ | ||
| 346 | // maar heeft het UML-model van DATEX II v3 | ||
| 347 | // het over ETRS 89: | ||
| 348 | // https://docs.datex2.eu/_static/data/v3.7/umlmodel/html/EARoot/EA3/EA3/EA5/EA676.htm | ||
| 349 | |||
| 350 | auto const coords_etrs89 = geo::point{*mlon, *mlat}; | ||
| 351 | auto coords_wgs84 = geo::point{}; | ||
| 352 | etrs89_to_wgs84_.forward(coords_etrs89, coords_wgs84); | ||
| 353 | |||
| 354 | if (!sit->location) | ||
| 355 | { | ||
| 356 | sit->location = coords_wgs84; | ||
| 357 | } | ||
| 358 | } | ||
| 359 | } | ||
| 360 | } | ||
| 361 | } | ||
| 362 | } | ||
| 363 | } | ||
| 364 | } | ||
| 365 | |||
| 366 | return { | ||
| 367 | .publication_time = *mpublication_time, | ||
| 368 | .situations = situations, | ||
| 369 | }; | ||
| 370 | } | ||
| 371 | |||
| 372 | auto loader::warnings() const -> std::multiset<std::string> const& | ||
| 373 | { | ||
| 374 | return warnings_; | ||
| 375 | } | ||
| 376 | |||
| 377 | } // namespace routemon::datex2 | ||