1 /*
2 SPDX-FileCopyrightText: 2020 Volker Krause <vkrause@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6
7 #include "platform.h"
8
9 #include <osm/element.h>
10 #include <osm/geomath.h>
11 #include <osm/pathutil.h>
12
13 #include <QRegularExpression>
14
15 #include <limits>
16
17 using namespace KOSMIndoorMap;
18
19 namespace KOSMIndoorMap {
20 class PlatformSectionPrivate : public QSharedData
21 {
22 public:
23 QString name;
24 OSM::Element position;
25 };
26 }
27
PlatformSection()28 PlatformSection::PlatformSection()
29 : d(new PlatformSectionPrivate)
30 {
31 }
32
33 PlatformSection::PlatformSection(const PlatformSection&) = default;
34 PlatformSection::PlatformSection(PlatformSection&&) = default;
35 PlatformSection::~PlatformSection() = default;
36 PlatformSection& PlatformSection::operator=(const PlatformSection&) = default;
37 PlatformSection& PlatformSection::operator=(PlatformSection&&) = default;
38
name() const39 QString PlatformSection::name() const
40 {
41 return d->name;
42 }
43
setName(const QString & name)44 void PlatformSection::setName(const QString &name)
45 {
46 d.detach();
47 d->name = name;
48 }
49
position() const50 OSM::Element PlatformSection::position() const
51 {
52 return d->position;
53 }
54
setPosition(const OSM::Element & position)55 void PlatformSection::setPosition(const OSM::Element &position)
56 {
57 d.detach();
58 d->position = position;
59 }
60
isValid() const61 bool PlatformSection::isValid() const
62 {
63 return !d->name.isEmpty() && d->position;
64 }
65
66
67 namespace KOSMIndoorMap {
68 class PlatformPrivate : public QSharedData
69 {
70 public:
71 QString m_name;
72 OSM::Element m_stopPoint;
73 OSM::Element m_edge;
74 OSM::Element m_area;
75 std::vector<OSM::Element> m_track;
76 Platform::Mode m_mode = Platform::Rail; // TODO should eventually be "Unknown"
77 int m_level = std::numeric_limits<int>::min(); // INT_MIN indicates not set, needed for merging
78 std::vector<PlatformSection> m_sections;
79 QString m_ifopt;
80 QStringList m_lines;
81
82 static void appendSection(std::vector<PlatformSection> §ions, const Platform &p, PlatformSection &&sec, std::vector<const OSM::Node*> &edgePath, const OSM::DataSet &dataSet);
83 static double maxSectionDistance(const Platform &p, const std::vector<PlatformSection> §ions, const OSM::DataSet &dataSet);
84 };
85 }
86
Platform()87 Platform::Platform()
88 : d(new PlatformPrivate)
89 {
90 }
91
92 Platform::Platform(const Platform&) = default;
93 Platform::Platform(Platform&&) = default;
94 Platform::~Platform() = default;
95 Platform& Platform::operator=(const Platform&) = default;
96 Platform& Platform::operator=(Platform&&) = default;
97
isValid() const98 bool Platform::isValid() const
99 {
100 return !d->m_name.isEmpty() && position().isValid() && d->m_mode != Unknown;
101 }
102
name() const103 QString Platform::name() const
104 {
105 return d->m_name;
106 }
107
setName(const QString & name)108 void Platform::setName(const QString &name)
109 {
110 d.detach();
111 d->m_name = name;
112 }
113
level() const114 int Platform::level() const
115 {
116 return hasLevel() ? d->m_level : 0;
117 }
118
hasLevel() const119 bool Platform::hasLevel() const
120 {
121 return d->m_level != std::numeric_limits<int>::min();
122 }
123
setLevel(int level)124 void Platform::setLevel(int level)
125 {
126 d.detach();
127 d->m_level = level;
128 }
129
position() const130 OSM::Coordinate Platform::position() const
131 {
132 return OSM::coalesce(d->m_stopPoint, d->m_area).center();
133 }
134
stopPoint() const135 OSM::Element Platform::stopPoint() const
136 {
137 return d->m_stopPoint;
138 }
139
setStopPoint(OSM::Element stop)140 void Platform::setStopPoint(OSM::Element stop)
141 {
142 d.detach();
143 d->m_stopPoint = stop;
144 }
145
edge() const146 OSM::Element Platform::edge() const
147 {
148 return OSM::coalesce(d->m_edge, d->m_stopPoint);
149 }
150
setEdge(OSM::Element edge)151 void Platform::setEdge(OSM::Element edge)
152 {
153 d.detach();
154 d->m_edge = edge;
155 }
156
area() const157 OSM::Element Platform::area() const
158 {
159 return OSM::coalesce(d->m_area, d->m_edge, d->m_stopPoint);
160 }
161
setArea(OSM::Element area)162 void Platform::setArea(OSM::Element area)
163 {
164 d.detach();
165 d->m_area = area;
166 }
167
track() const168 const std::vector<OSM::Element>& Platform::track() const
169 {
170 return d->m_track;
171 }
172
setTrack(std::vector<OSM::Element> && track)173 void Platform::setTrack(std::vector<OSM::Element> &&track)
174 {
175 d.detach();
176 d->m_track = std::move(track);
177 }
178
takeTrack()179 std::vector<OSM::Element>&& Platform::takeTrack()
180 {
181 d.detach();
182 return std::move(d->m_track);
183 }
184
sections() const185 const std::vector<PlatformSection>& Platform::sections() const
186 {
187 return d->m_sections;
188 }
189
setSections(std::vector<PlatformSection> && sections)190 void Platform::setSections(std::vector<PlatformSection> &§ions)
191 {
192 d.detach();
193 d->m_sections = std::move(sections);
194 }
195
takeSections()196 std::vector<PlatformSection>&& Platform::takeSections()
197 {
198 d.detach();
199 return std::move(d->m_sections);
200 }
201
mode() const202 Platform::Mode Platform::mode() const
203 {
204 return d->m_mode;
205 }
206
setMode(Platform::Mode mode)207 void Platform::setMode(Platform::Mode mode)
208 {
209 d.detach();
210 d->m_mode = mode;
211 }
212
ifopt() const213 QString Platform::ifopt() const
214 {
215 return d->m_ifopt;
216 }
217
setIfopt(const QString & ifopt)218 void Platform::setIfopt(const QString &ifopt)
219 {
220 d.detach();
221 d->m_ifopt = ifopt;
222 }
223
lines() const224 QStringList Platform::lines() const
225 {
226 return d->m_lines;
227 }
228
setLines(QStringList && lines)229 void Platform::setLines(QStringList &&lines)
230 {
231 d.detach();
232 d->m_lines = std::move(lines);
233 }
234
takeLines()235 QStringList&& Platform::takeLines()
236 {
237 d.detach();
238 return std::move(d->m_lines);
239 }
240
conflictIfPresent(OSM::Element lhs,OSM::Element rhs)241 static bool conflictIfPresent(OSM::Element lhs, OSM::Element rhs)
242 {
243 return lhs && rhs && lhs != rhs;
244 }
245
equalIfPresent(OSM::Element lhs,OSM::Element rhs)246 static bool equalIfPresent(OSM::Element lhs, OSM::Element rhs)
247 {
248 return lhs && rhs && lhs == rhs;
249 }
250
isSubPath(const std::vector<const OSM::Node * > & path,const OSM::Way & way)251 static bool isSubPath(const std::vector<const OSM::Node*> &path, const OSM::Way &way)
252 {
253 return std::all_of(way.nodes.begin(), way.nodes.end(), [&path](OSM::Id node) {
254 return std::find_if(path.begin(), path.end(), [node](auto n) { return n->id == node; }) != path.end();
255 });
256 }
257
258 static constexpr const auto MAX_TRACK_TO_EDGE_DISTANCE = 4.5; // meters
259 static constexpr const auto MAX_SECTION_TO_EDGE_DISTANCE = 5.0;
260
maxSectionDistance(const std::vector<const OSM::Node * > & path,const std::vector<PlatformSection> & sections)261 static double maxSectionDistance(const std::vector<const OSM::Node*> &path, const std::vector<PlatformSection> §ions)
262 {
263 auto dist = std::numeric_limits<double>::lowest();
264 for (const auto §ion : sections) {
265 dist = std::max(dist, OSM::distance(path, section.position().center()));
266 }
267 return dist;
268 }
269
maxSectionDistance(const Platform & p,const std::vector<PlatformSection> & sections,const OSM::DataSet & dataSet)270 double PlatformPrivate::maxSectionDistance(const Platform &p, const std::vector<PlatformSection> §ions, const OSM::DataSet &dataSet)
271 {
272 if (auto elem = OSM::coalesce(p.d->m_edge, p.d->m_area)) {
273 return ::maxSectionDistance(elem.outerPath(dataSet), sections);
274 }
275 if (!p.d->m_track.empty()) {
276 std::vector<const OSM::Node*> path;
277 OSM::assemblePath(dataSet, p.d->m_track, path);
278 return ::maxSectionDistance(path, sections);
279 }
280 return std::numeric_limits<double>::lowest();
281 }
282
outerWay(OSM::Element & multiPolygon,const OSM::DataSet & dataSet)283 static const OSM::Way* outerWay(OSM::Element &multiPolygon, const OSM::DataSet &dataSet)
284 {
285 // ### this assumes multi-polygons are structured in the way the Marble generator normalizes them!
286 for (const auto &mem : multiPolygon.relation()->members) {
287 if (mem.type() == OSM::Type::Way && (std::strcmp(mem.role().name(), "outer") == 0)) {
288 return dataSet.way(mem.id);
289 }
290 }
291 return nullptr;
292 }
293
isConnectedGeometry(OSM::Element lhs,OSM::Element rhs,const OSM::DataSet & dataSet)294 static bool isConnectedGeometry(OSM::Element lhs, OSM::Element rhs, const OSM::DataSet &dataSet)
295 {
296 if (lhs == rhs) { return false; }
297 const OSM::Way *lway = nullptr;
298 const OSM::Way *rway = nullptr;
299
300 switch (lhs.type()) {
301 case OSM::Type::Null:
302 case OSM::Type::Node:
303 return false;
304 case OSM::Type::Way:
305 lway = lhs.way();
306 break;
307 case OSM::Type::Relation:
308 lway = outerWay(lhs, dataSet);
309 break;
310
311 }
312 switch (rhs.type()) {
313 case OSM::Type::Null:
314 case OSM::Type::Node:
315 return false;
316 case OSM::Type::Way:
317 rway = rhs.way();
318 break;
319 case OSM::Type::Relation:
320 rway = outerWay(rhs, dataSet);
321 break;
322 }
323 if (!lway || !rway) {
324 return false;
325 }
326
327 if (!lway->isClosed() && !rway->isClosed()) {
328 return lway->nodes.front() == rway->nodes.front()
329 || lway->nodes.back() == rway->nodes.front()
330 || lway->nodes.front() == rway->nodes.back()
331 || lway->nodes.back() == rway->nodes.back();
332 }
333 if (lway->isClosed() && lway->nodes.size() > 2 && rway->isClosed() && rway->nodes.size() > 2) {
334 // ### this assumes multi-polygons are structured in the way the Marble generator normalizes them!
335 bool found = false;
336 for (std::size_t i = 0; i < lway->nodes.size() && !found; ++i) {
337 const auto n1 = lway->nodes[i];
338 const auto n2 = lway->nodes[(i + 1) % lway->nodes.size()];
339 for (std::size_t j = 0; j < rway->nodes.size() && !found; ++j) {
340 found = ((n1 == rway->nodes[j]) && (n2 == rway->nodes[(j + 1) % rway->nodes.size()]))
341 || ((n2 == rway->nodes[j]) && (n1 == rway->nodes[(j + 1) % rway->nodes.size()]));
342 }
343 }
344 return found;
345 }
346
347 return false;
348 }
349
isConnectedWay(const std::vector<OSM::Element> & lhs,const std::vector<OSM::Element> & rhs,const OSM::DataSet & dataSet)350 static bool isConnectedWay(const std::vector<OSM::Element> &lhs, const std::vector<OSM::Element> &rhs, const OSM::DataSet &dataSet)
351 {
352 return std::any_of(lhs.begin(), lhs.end(), [&](auto lway) {
353 return std::any_of(rhs.begin(), rhs.end(), [&](auto rway) {
354 return isConnectedGeometry(lway, rway, dataSet);
355 });
356 });
357 }
358
isOverlappingWay(const std::vector<OSM::Element> & lhs,const std::vector<OSM::Element> & rhs)359 static bool isOverlappingWay(const std::vector<OSM::Element> &lhs, const std::vector<OSM::Element> &rhs)
360 {
361 return std::any_of(lhs.begin(), lhs.end(), [&](auto lway) {
362 return std::any_of(rhs.begin(), rhs.end(), [&](auto rway) {
363 return lway == rway;
364 });
365 });
366 }
367
isSame(const Platform & lhs,const Platform & rhs,const OSM::DataSet & dataSet)368 bool Platform::isSame(const Platform &lhs, const Platform &rhs, const OSM::DataSet &dataSet)
369 {
370 if (!lhs.ifopt().isEmpty() && !rhs.ifopt().isEmpty()) {
371 return lhs.ifopt() == rhs.ifopt();
372 }
373
374 const auto isConnectedEdge = isConnectedGeometry(lhs.d->m_edge, rhs.d->m_edge, dataSet);
375 const auto isConnectedTrack = isConnectedWay(lhs.d->m_track, rhs.d->m_track, dataSet);
376 const auto isOverlappingTrack = isOverlappingWay(lhs.d->m_track, rhs.d->m_track);
377 const auto isConnectedArea = isConnectedGeometry(lhs.d->m_area, rhs.d->m_area, dataSet);
378
379 if ((conflictIfPresent(lhs.d->m_stopPoint, rhs.d->m_stopPoint) && lhs.d->m_track != rhs.d->m_track && !isConnectedTrack)
380 || (conflictIfPresent(lhs.d->m_edge, rhs.d->m_edge) && !isConnectedEdge)
381 || (conflictIfPresent(lhs.d->m_area, rhs.d->m_area) && !isConnectedArea)
382 || (!lhs.d->m_track.empty() && !rhs.d->m_track.empty() && !isOverlappingTrack && !isConnectedTrack)
383 || (lhs.hasLevel() && rhs.hasLevel() && lhs.level() != rhs.level())
384 || (lhs.d->m_mode != Unknown && rhs.d->m_mode != Unknown && lhs.d->m_mode != rhs.d->m_mode))
385 {
386 return false;
387 }
388
389 // we can accept conflicting names if one of them is likely a station name instead of a platform name
390 if (!lhs.d->m_name.isEmpty() && !rhs.d->m_name.isEmpty() && lhs.d->m_name != rhs.d->m_name) {
391 if (isPlausibleName(lhs.name()) && isPlausibleName(rhs.name())) {
392 return false;
393 }
394 }
395
396 // edge has to be part of area, but on its own that doesn't mean equallity
397 if (!isConnectedArea && !isConnectedEdge) {
398 if ((lhs.d->m_area && rhs.d->m_edge.type() == OSM::Type::Way && !isSubPath(lhs.d->m_area.outerPath(dataSet), *rhs.d->m_edge.way()))
399 || (rhs.d->m_area && lhs.d->m_edge.type() == OSM::Type::Way && !isSubPath(rhs.d->m_area.outerPath(dataSet), *lhs.d->m_edge.way()))) {
400 return false;
401 }
402 }
403
404 // matching edge, point or track is good enough, matching area however isn't
405 if (equalIfPresent(lhs.d->m_stopPoint, rhs.d->m_stopPoint)
406 || equalIfPresent(lhs.d->m_edge, rhs.d->m_edge) || isConnectedEdge
407 || isOverlappingTrack)
408 {
409 return true;
410 }
411
412 if (!isConnectedEdge) {
413 // track/stop and area/edge elements do not share nodes, so those we need to match by spatial distance
414 if (lhs.d->m_edge && rhs.d->m_stopPoint) {
415 return OSM::distance(lhs.d->m_edge.outerPath(dataSet), rhs.position()) < MAX_TRACK_TO_EDGE_DISTANCE;
416 }
417 if (rhs.d->m_edge && lhs.d->m_stopPoint) {
418 return OSM::distance(rhs.d->m_edge.outerPath(dataSet), lhs.position()) < MAX_TRACK_TO_EDGE_DISTANCE;
419 }
420 }
421
422 if (!isConnectedArea) {
423 if (lhs.d->m_area && rhs.d->m_stopPoint) {
424 return OSM::distance(lhs.d->m_area.outerPath(dataSet), rhs.position()) < MAX_TRACK_TO_EDGE_DISTANCE;
425 }
426 if (rhs.d->m_area && lhs.d->m_stopPoint) {
427 return OSM::distance(rhs.d->m_area.outerPath(dataSet), lhs.position()) < MAX_TRACK_TO_EDGE_DISTANCE;
428 }
429 }
430
431 // free-floating sections: edge, area or track is within a reasonable distance
432 if (!lhs.d->m_name.isEmpty() && lhs.d->m_name == rhs.d->m_name && !isConnectedArea && !isConnectedEdge) {
433 auto d = PlatformPrivate::maxSectionDistance(lhs, rhs.sections(), dataSet);
434 if (d >= 0.0) {
435 return d < MAX_SECTION_TO_EDGE_DISTANCE;
436 }
437 d = PlatformPrivate::maxSectionDistance(rhs, lhs.sections(), dataSet);
438 if (d >= 0.0) {
439 return d < MAX_SECTION_TO_EDGE_DISTANCE;
440 }
441 }
442
443 return isConnectedArea || isConnectedEdge || isConnectedTrack;
444 }
445
compareSection(const PlatformSection & lhs,const PlatformSection & rhs)446 static bool compareSection(const PlatformSection &lhs, const PlatformSection &rhs)
447 {
448 if (lhs.name() == rhs.name()) {
449 return lhs.position() < rhs.position();
450 }
451 return lhs.name() < rhs.name();
452 }
453
appendSection(std::vector<PlatformSection> & sections,const Platform & p,PlatformSection && sec,std::vector<const OSM::Node * > & edgePath,const OSM::DataSet & dataSet)454 void PlatformPrivate::appendSection(std::vector<PlatformSection> §ions, const Platform &p, PlatformSection &&sec, std::vector<const OSM::Node*> &edgePath, const OSM::DataSet &dataSet)
455 {
456 if (sections.empty() || sections.back().name() != sec.name()) {
457 sections.push_back(std::move(sec));
458 return;
459 }
460
461 // check which one is closer
462 if (edgePath.empty()) {
463 if (p.d->m_edge) {
464 edgePath = p.d->m_edge.outerPath(dataSet);
465 } else if (!p.d->m_track.empty()) {
466 OSM::assemblePath(dataSet, p.d->m_track, edgePath);
467 }
468 }
469 const auto dist1 = OSM::distance(edgePath, sections.back().position().center());
470 const auto dist2 = OSM::distance(edgePath, sec.position().center());
471 if (dist2 < dist1) {
472 sections.back() = std::move(sec);
473 }
474 }
475
mergeWays(const std::vector<OSM::Element> & lhs,const std::vector<OSM::Element> & rhs)476 static std::vector<OSM::Element> mergeWays(const std::vector<OSM::Element> &lhs, const std::vector<OSM::Element> &rhs)
477 {
478 std::vector<OSM::Element> w = lhs;
479 for (auto e : rhs) {
480 if (std::find(w.begin(), w.end(), e) == w.end()) {
481 w.push_back(e);
482 }
483 }
484 return w;
485 }
486
merge(const Platform & lhs,const Platform & rhs,const OSM::DataSet & dataSet)487 Platform Platform::merge(const Platform &lhs, const Platform &rhs, const OSM::DataSet &dataSet)
488 {
489 Platform p;
490 p.d->m_name = preferredName(lhs.name(), rhs.name());
491 p.d->m_stopPoint = OSM::coalesce(lhs.d->m_stopPoint, rhs.d->m_stopPoint);
492 p.d->m_edge = OSM::coalesce(lhs.d->m_edge, rhs.d->m_edge);
493 p.d->m_area = OSM::coalesce(lhs.d->m_area, rhs.d->m_area);
494 p.d->m_track = mergeWays(lhs.d->m_track, rhs.d->m_track);
495 p.d->m_level = lhs.hasLevel() ? lhs.d->m_level : rhs.d->m_level;
496 p.d->m_ifopt = lhs.ifopt().isEmpty() ? rhs.ifopt() : lhs.ifopt();
497
498 // TODO
499 p.d->m_mode = std::max(lhs.d->m_mode, rhs.d->m_mode);
500 p.d->m_lines = lhs.d->m_lines.isEmpty() ? std::move(rhs.d->m_lines) : std::move(lhs.d->m_lines);
501
502 std::vector<const OSM::Node*> edgePath;
503 std::vector<PlatformSection> sections;
504 auto lsec = lhs.sections();
505 auto rsec = rhs.sections();
506 std::sort(lsec.begin(), lsec.end(), compareSection);
507 std::sort(rsec.begin(), rsec.end(), compareSection);
508 for (auto lit = lsec.begin(), rit = rsec.begin(); lit != lsec.end() || rit != rsec.end();) {
509 if (rit == rsec.end()) {
510 PlatformPrivate::appendSection(sections, p, std::move(*lit++), edgePath, dataSet);
511 continue;
512 }
513 if (lit == lsec.end()) {
514 PlatformPrivate::appendSection(sections, p, std::move(*rit++), edgePath, dataSet);
515 continue;
516 }
517 if (compareSection(*lit, *rit)) {
518 PlatformPrivate::appendSection(sections, p, std::move(*lit++), edgePath, dataSet);
519 continue;
520 }
521 if (compareSection(*rit, *lit)) {
522 PlatformPrivate::appendSection(sections, p, std::move(*rit++), edgePath, dataSet);
523 continue;
524 }
525
526 // both are equal
527 if ((*lit).position() == (*rit).position()) {
528 PlatformPrivate::appendSection(sections, p, std::move(*lit++), edgePath, dataSet);
529 ++rit;
530 continue;
531 }
532
533 // both are equal but differ in distance: will be handled in appendSection in the next iteration
534 PlatformPrivate::appendSection(sections, p, std::move(*lit++), edgePath, dataSet);
535 }
536 p.setSections(std::move(sections));
537
538 return p;
539 }
540
isPlausibleName(const QString & name)541 bool Platform::isPlausibleName(const QString &name)
542 {
543 static QRegularExpression exp(QStringLiteral("^(\\d{1,3}[a-z]?|[A-Z])$"));
544 return exp.match(name).hasMatch();
545 }
546
preferredName(const QString & lhs,const QString & rhs)547 QString Platform::preferredName(const QString &lhs, const QString &rhs)
548 {
549 if (lhs.isEmpty()) {
550 return rhs;
551 }
552 if (rhs.isEmpty()) {
553 return lhs;
554 }
555
556 if (isPlausibleName(lhs)) {
557 return lhs;
558 }
559 if (isPlausibleName(rhs)) {
560 return rhs;
561 }
562
563 return lhs.size() <= rhs.size() ? lhs: rhs;
564 }
565