1 /*
2     SPDX-FileCopyrightText: 2020 Volker Krause <vkrause@kde.org>
3 
4     SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "platformfinder_p.h"
8 
9 #include <QRegularExpression>
10 
11 using namespace KOSMIndoorMap;
12 
PlatformFinder()13 PlatformFinder::PlatformFinder()
14 {
15     m_collator.setLocale(QLocale());
16     m_collator.setNumericMode(true);
17     m_collator.setIgnorePunctuation(true);
18     m_collator.setCaseSensitivity(Qt::CaseInsensitive);
19 }
20 
21 PlatformFinder::~PlatformFinder() = default;
22 
nameFromTrack(OSM::Element track)23 static QString nameFromTrack(OSM::Element track)
24 {
25     auto name = QString::fromUtf8(track.tagValue("railway:track_ref"));
26     if (!name.isEmpty()) {
27         return name;
28     }
29 
30     name = QString::fromUtf8(track.tagValue("name"));
31     for (const char *n : { "platform", "voie", "gleis" }) {
32         if (name.contains(QLatin1String(n), Qt::CaseInsensitive)) {
33             return name.remove(QLatin1String(n), Qt::CaseInsensitive).trimmed();
34         }
35     }
36 
37     return {};
38 }
39 
find(const MapData & data)40 std::vector<Platform> PlatformFinder::find(const MapData &data)
41 {
42     m_data = data;
43     resolveTagKeys();
44 
45     for (auto it = m_data.levelMap().begin(); it != m_data.levelMap().end(); ++it) {
46         for (const auto &e : (*it).second) {
47             if (!e.hasTags() || e.tagValue(m_tagKeys.disused) == "yes") {
48                 continue;
49             }
50 
51             // non-standard free-floating section signs
52             const auto platformRef = e.tagValue(m_tagKeys.platform_colon_ref);
53             if (!platformRef.isEmpty() && e.tagValue("pole") == "landmark_sign") {
54                 const auto names = QString::fromUtf8(platformRef).split(QLatin1Char(';'));
55                 for (const auto &name : names) {
56                     Platform p;
57                     p.setLevel(levelForPlatform((*it).first, e));
58                     p.setName(name);
59                     PlatformSection section;
60                     section.setName(QString::fromUtf8(e.tagValue("local_ref", "ref")));
61                     section.setPosition(e);
62                     p.setSections({section});
63                     m_floatingSections.push_back(std::move(p)); // can't merge this reliably until we have the full area geometry
64                 }
65             }
66 
67             if (e.type() == OSM::Type::Node) {
68                 continue;
69             }
70             const auto railway = e.tagValue(m_tagKeys.railway);
71             if (railway == "platform") {
72                 QRegularExpression splitExp(QStringLiteral("[;,/\\+]"));;
73                 const auto names = QString::fromUtf8(e.tagValue("local_ref", "ref")).split(splitExp);
74                 const auto ifopts = e.tagValue("ref:IFOPT").split(';');
75                 for (auto i = 0; i < names.size(); ++i) {
76                     Platform platform;
77                     platform.setArea(e);
78                     platform.setName(names[i]);
79                     platform.setLevel(levelForPlatform((*it).first, e));
80                     platform.setMode(modeForElement(e));
81                     platform.setSections(sectionsForPath(e.outerPath(m_data.dataSet()), names[i]));
82                     if (ifopts.size() == names.size()) {
83                         platform.setIfopt(QString::fromUtf8(ifopts[i]));
84                     }
85                     // we delay merging of platforms, as those without track names would
86                     // otherwise cobble together two distinct edges when merged to early
87                     m_platformAreas.push_back(std::move(platform));
88                 }
89             }
90             else if (railway == "platform_edge" && e.type() == OSM::Type::Way) {
91                 Platform platform;
92                 platform.setEdge(e);
93                 platform.setName(QString::fromUtf8(e.tagValue("local_ref", "ref")));
94                 platform.setLevel(levelForPlatform((*it).first, e));
95                 platform.setSections(sectionsForPath(e.outerPath(m_data.dataSet()), platform.name()));
96                 platform.setIfopt(QString::fromUtf8(e.tagValue("ref:IFOPT")));
97                 addPlatform(std::move(platform));
98             }
99             else if (!railway.isEmpty() && e.type() == OSM::Type::Way && railway != "disused") {
100                 OSM::for_each_node(m_data.dataSet(), *e.way(), [&](const auto &node) {
101                     if (!OSM::contains(m_data.boundingBox(), node.coordinate)) {
102                         return;
103                     }
104                     if (OSM::tagValue(node, m_tagKeys.railway) == "buffer_stop") {
105                         return;
106                     }
107 
108                     const auto pt = OSM::tagValue(node, m_tagKeys.public_transport);
109                     if (pt == "stop_point" || pt == "stop_position") {
110                         Platform platform;
111                         platform.setStopPoint(OSM::Element(&node));
112                         platform.setTrack({e});
113                         platform.setLevel(levelForPlatform((*it).first, e));
114                         platform.setName(Platform::preferredName(QString::fromUtf8(platform.stopPoint().tagValue("local_ref", "ref", "name")), nameFromTrack(e)));
115                         platform.setMode(modeForElement(OSM::Element(&node)));
116                         platform.setIfopt(QString::fromUtf8(platform.stopPoint().tagValue("ref:IFOPT")));
117                         if (platform.mode() == Platform::Unknown) {
118                             platform.setMode(modeForElement(e));
119                         }
120                         platform.setSections(sectionsForPath(e.outerPath(m_data.dataSet()), platform.name()));
121 
122                         addPlatform(std::move(platform));
123                     }
124                 });
125             }
126         }
127     }
128 
129     OSM::for_each(m_data.dataSet(), [this](OSM::Element e) {
130         const auto route = e.tagValue(m_tagKeys.route);
131         if (route.isEmpty() || route == "tracks") {
132             return;
133         }
134         scanRoute(e, e);
135     }, OSM::IncludeRelations);
136 
137     mergePlatformAreas();
138     for (auto &p : m_floatingSections) {
139         addPlatform(std::move(p));
140     }
141     m_floatingSections.clear();
142 
143     finalizeResult();
144     return std::move(m_platforms);
145 }
146 
resolveTagKeys()147 void PlatformFinder::resolveTagKeys()
148 {
149     m_tagKeys.level = m_data.dataSet().tagKey("level");
150     m_tagKeys.platform_ref = m_data.dataSet().tagKey("platform_ref");
151     m_tagKeys.platform_colon_ref = m_data.dataSet().tagKey("platform:ref");
152     m_tagKeys.public_transport = m_data.dataSet().tagKey("public_transport");
153     m_tagKeys.railway = m_data.dataSet().tagKey("railway");
154     m_tagKeys.railway_platform_section = m_data.dataSet().tagKey("railway:platform:section");
155     m_tagKeys.route = m_data.dataSet().tagKey("route");
156     m_tagKeys.disused = m_data.dataSet().tagKey("disused");
157 }
158 
scanRoute(OSM::Element e,OSM::Element route)159 void PlatformFinder::scanRoute(OSM::Element e, OSM::Element route)
160 {
161     switch (e.type()) {
162         case OSM::Type::Null:
163             return;
164         case OSM::Type::Node:
165             scanRoute(*e.node(), route);
166             break;
167         case OSM::Type::Way:
168             OSM::for_each_node(m_data.dataSet(), *e.way(), [this, route](const OSM::Node &node) {
169                 scanRoute(node, route);
170             });
171             break;
172         case OSM::Type::Relation:
173             OSM::for_each_member(m_data.dataSet(), *e.relation(), [this, route](OSM::Element e) {
174                 scanRoute(e, route);
175             });
176             break;
177     }
178 }
179 
scanRoute(const OSM::Node & node,OSM::Element route)180 void PlatformFinder::scanRoute(const OSM::Node& node, OSM::Element route)
181 {
182     const auto pt = OSM::tagValue(node, m_tagKeys.public_transport);
183     if (pt.isEmpty()) {
184         return;
185     }
186 
187     for (auto &p : m_platforms) {
188         if (p.stopPoint().id() == node.id) {
189             const auto l = QString::fromUtf8(route.tagValue("ref")).split(QLatin1Char(';'));
190             for (const auto &lineName : l) {
191                 if (lineName.isEmpty()) {
192                     continue;
193                 }
194                 auto lines = p.takeLines();
195                 const auto it = std::lower_bound(lines.begin(), lines.end(), lineName, m_collator);
196                 if (it == lines.end() || (*it) != lineName) {
197                     lines.insert(it, lineName);
198                 }
199                 p.setLines(std::move(lines));
200             }
201             break;
202         }
203     }
204 }
205 
sectionsForPath(const std::vector<const OSM::Node * > & path,const QString & platformName) const206 std::vector<PlatformSection> PlatformFinder::sectionsForPath(const std::vector<const OSM::Node*> &path, const QString &platformName) const
207 {
208     std::vector<PlatformSection> sections;
209     if (path.empty()) {
210         return sections;
211     }
212 
213     // skip the last node for closed paths
214     for (auto it = path.begin(); it != (path.front()->id == path.back()->id ? std::prev(path.end()) : path.end()); ++it) {
215         const auto n = (*it);
216         const auto platformRef = OSM::tagValue(*n, m_tagKeys.platform_ref);
217         if (!platformRef.isEmpty() && platformRef != platformName.toUtf8()) {
218             continue; // belongs to a different track on the same platform area
219         }
220         const auto pt = OSM::tagValue(*n, m_tagKeys.public_transport);
221         if (pt == "platform_section_sign") {
222             PlatformSection sec;
223             sec.setPosition(OSM::Element(n));
224             sec.setName(QString::fromUtf8(sec.position().tagValue("platform_section_sign_value", "local_ref", "ref")));
225             sections.push_back(std::move(sec));
226             continue;
227         }
228         const auto railway_platform_section = OSM::tagValue(*n, m_tagKeys.railway_platform_section);
229         if (!railway_platform_section.isEmpty()) {
230             PlatformSection sec;
231             sec.setPosition(OSM::Element(n));
232             sec.setName(QString::fromUtf8(railway_platform_section));
233             sections.push_back(std::move(sec));
234             continue;
235         }
236     }
237     return sections;
238 }
239 
240 struct {
241     const char* name;
242     Platform::Mode mode;
243 } static constexpr const mode_map[] = {
244     { "rail", Platform::Rail },
245     { "light_rail", Platform::Rail }, // TODO consumer code can't handle LightRail yet
246     { "subway", Platform::Subway },
247     { "tram", Platform::Tram },
248     { "bus", Platform::Bus },
249 };
250 
modeForElement(OSM::Element elem) const251 Platform::Mode PlatformFinder::modeForElement(OSM::Element elem) const
252 {
253     const auto railway = elem.tagValue(m_tagKeys.railway);
254     for (const auto &mode : mode_map) {
255         const auto modeTag = elem.tagValue(mode.name);
256         if (railway == mode.name || (!modeTag.isEmpty() && modeTag != "no")) {
257             return mode.mode;
258         }
259     }
260 
261     // TODO this should eventually return Unknown
262     return Platform::Rail;
263 }
264 
levelForPlatform(const MapLevel & ml,OSM::Element e) const265 int PlatformFinder::levelForPlatform(const MapLevel &ml, OSM::Element e) const
266 {
267     if (ml.numericLevel() != 0) {
268         return qRound(ml.numericLevel() / 10.0) * 10;
269     }
270     return e.tagValue(m_tagKeys.level).isEmpty() ? std::numeric_limits<int>::min() : 0;
271 }
272 
addPlatform(Platform && platform)273 void PlatformFinder::addPlatform(Platform &&platform)
274 {
275     for (Platform &p : m_platforms) {
276         if (Platform::isSame(p, platform, m_data.dataSet())) {
277             p = Platform::merge(p, platform, m_data.dataSet());
278             return;
279         }
280     }
281 
282     m_platforms.push_back(std::move(platform));
283 }
284 
mergePlatformAreas()285 void PlatformFinder::mergePlatformAreas()
286 {
287     // due to split areas we can end up with multplie entries for the same platform that only merge in the right order
288     // so retry until we no longer find anything matching
289     std::size_t prevCount = 0;
290 
291     while (prevCount != m_platformAreas.size() && !m_platformAreas.empty()) {
292         prevCount = m_platformAreas.size();
293         for (auto it = m_platformAreas.begin(); it != m_platformAreas.end();) {
294             bool found = false;
295             for (Platform &p : m_platforms) {
296                 if (Platform::isSame(p, (*it), m_data.dataSet())) {
297                     p = Platform::merge(p, (*it), m_data.dataSet());
298                     found = true;
299                 }
300             }
301             if (found) {
302                 it = m_platformAreas.erase(it);
303             } else {
304                 ++it;
305             }
306         }
307 
308         if (prevCount == m_platformAreas.size()) {
309             m_platforms.push_back(m_platformAreas.back());
310             m_platformAreas.erase(std::prev(m_platformAreas.end()));
311         }
312     }
313 }
314 
finalizeResult()315 void PlatformFinder::finalizeResult()
316 {
317     if (m_platforms.empty()) {
318         return;
319     }
320 
321     // integrating the platform elements can have made other platforms mergable that previously weren't,
322     // the same can happen in case of a very fine-granular track split
323     // so do another merge pass over everything we have found so far
324     for (auto it = m_platforms.begin(); it != std::prev(m_platforms.end()) && it != m_platforms.end(); ++it) {
325         for (auto it2 = std::next(it); it2 != m_platforms.end();) {
326             if (Platform::isSame(*it, *it2, m_data.dataSet())) {
327                 (*it) = Platform::merge(*it, *it2, m_data.dataSet());
328                 it2 = m_platforms.erase(it2);
329             } else {
330                 ++it2;
331             }
332         }
333     }
334 
335     // remove things that are still incomplete at this point
336     m_platforms.erase(std::remove_if(m_platforms.begin(), m_platforms.end(), [](const auto &p) {
337         return !p.isValid() || p.mode() == Platform::Bus; // ### Bus isn't properly supported yet
338     }), m_platforms.end());
339 
340     // filter and sort sections on each platform
341     for (auto &p : m_platforms) {
342         auto sections = p.takeSections();
343         sections.erase(std::remove_if(sections.begin(), sections.end(), [](const auto &s) { return !s.isValid(); }), sections.end());
344         std::sort(sections.begin(), sections.end(), [this](const auto &lhs, const auto &rhs) {
345             return m_collator.compare(lhs.name(), rhs.name()) < 0;
346         });
347         p.setSections(std::move(sections));
348     }
349 
350     // sort platforms by mode/name
351     std::sort(m_platforms.begin(), m_platforms.end(), [this](const auto &lhs, const auto &rhs) {
352         if (lhs.mode() == rhs.mode()) {
353             if (lhs.name() == rhs.name()) {
354                 return lhs.stopPoint().id() < rhs.stopPoint().id();
355             }
356             return m_collator.compare(lhs.name(), rhs.name()) < 0;
357         }
358         return lhs.mode() < rhs.mode();
359     });
360 }
361