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