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> &sections, 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> &sections, 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> &&sections)
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> &sections)
262 {
263     auto dist = std::numeric_limits<double>::lowest();
264     for (const auto &section : 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> &sections, 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> &sections, 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