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