summaryrefslogtreecommitdiffstats
path: root/server/src/datex2.cppm
diff options
context:
space:
mode:
Diffstat (limited to 'server/src/datex2.cppm')
-rw-r--r--server/src/datex2.cppm540
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 @@
1module; 1module;
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
18namespace routemon::datex2 { 18namespace routemon::datex2 {
19 19
20 export struct situation; 20export 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 { 22export 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> { 30export 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 { 39export 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_; 45auto 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 { 53export 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: 273public:
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