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