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