KOSMIndoorMap

platformfinder.cpp
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
11using namespace KOSMIndoorMap;
12
13PlatformFinder::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
21PlatformFinder::~PlatformFinder() = default;
22
23static 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
40std::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 if (e.tagValue(m_tagKeys.railway) == "platform_marker") {
67 Platform p;
68 p.setLevel(levelForPlatform((*it).first, e));
69 PlatformSection section;
70 section.setName(QString::fromUtf8(e.tagValue("ref")));
71 section.setPosition(e);
72 p.setSections({section});
73 m_floatingSections.push_back(std::move(p));
74 }
75
76 if (e.type() == OSM::Type::Node) {
77 continue;
78 }
79 const auto railway = e.tagValue(m_tagKeys.railway);
80 if (railway == "platform") {
81 QRegularExpression splitExp(QStringLiteral("[;,/\\+]"));;
82 const auto names = QString::fromUtf8(e.tagValue("local_ref", "ref")).split(splitExp);
83 const auto ifopts = e.tagValue("ref:IFOPT").split(';');
84 for (auto i = 0; i < names.size(); ++i) {
85 Platform platform;
86 platform.setArea(e);
87 platform.setName(names[i]);
88 platform.setLevel(levelForPlatform((*it).first, e));
89 platform.setMode(modeForElement(e));
90 platform.setSections(sectionsForPath(e.outerPath(m_data.dataSet()), names[i]));
91 if (ifopts.size() == names.size()) {
92 platform.setIfopt(QString::fromUtf8(ifopts[i]));
93 }
94 // we delay merging of platforms, as those without track names would
95 // otherwise cobble together two distinct edges when merged to early
96 m_platformAreas.push_back(std::move(platform));
97 }
98 }
99 else if (railway == "platform_edge" && e.type() == OSM::Type::Way) {
100 Platform platform;
101 platform.setEdge(e);
102 platform.setName(QString::fromUtf8(e.tagValue("local_ref", "ref")));
103 platform.setLevel(levelForPlatform((*it).first, e));
104 platform.setSections(sectionsForPath(e.outerPath(m_data.dataSet()), platform.name()));
105 platform.setIfopt(QString::fromUtf8(e.tagValue("ref:IFOPT")));
106 addPlatform(std::move(platform));
107 }
108 else if (!railway.isEmpty() && e.type() == OSM::Type::Way && railway != "disused") {
109 OSM::for_each_node(m_data.dataSet(), *e.way(), [&](const auto &node) {
110 if (!OSM::contains(m_data.boundingBox(), node.coordinate)) {
111 return;
112 }
113 if (OSM::tagValue(node, m_tagKeys.railway) == "buffer_stop") {
114 return;
115 }
116
117 const auto pt = OSM::tagValue(node, m_tagKeys.public_transport);
118 if (pt == "stop_point" || pt == "stop_position") {
119 Platform platform;
120 platform.setStopPoint(OSM::Element(&node));
121 platform.setTrack({e});
122 platform.setLevel(levelForPlatform((*it).first, e));
123 platform.setName(Platform::preferredName(QString::fromUtf8(platform.stopPoint().tagValue("local_ref", "ref", "name")), nameFromTrack(e)));
124 platform.setMode(modeForElement(OSM::Element(&node)));
125 platform.setIfopt(QString::fromUtf8(platform.stopPoint().tagValue("ref:IFOPT")));
126 if (platform.mode() == Platform::Unknown) {
127 platform.setMode(modeForElement(e));
128 }
129 platform.setSections(sectionsForPath(e.outerPath(m_data.dataSet()), platform.name()));
130
131 addPlatform(std::move(platform));
132 }
133 });
134 }
135 }
136 }
137
138 OSM::for_each(m_data.dataSet(), [this](OSM::Element e) {
139 const auto route = e.tagValue(m_tagKeys.route);
140 if (route.isEmpty() || route == "tracks") {
141 return;
142 }
143 scanRoute(e, e);
144 }, OSM::IncludeRelations);
145
146 mergePlatformAreas();
147 for (auto &p : m_floatingSections) {
148 addPlatform(std::move(p));
149 }
150 m_floatingSections.clear();
151
152 finalizeResult();
153 return std::move(m_platforms);
154}
155
156void PlatformFinder::resolveTagKeys()
157{
158 m_tagKeys.level = m_data.dataSet().tagKey("level");
159 m_tagKeys.platform_ref = m_data.dataSet().tagKey("platform_ref");
160 m_tagKeys.platform_colon_ref = m_data.dataSet().tagKey("platform:ref");
161 m_tagKeys.public_transport = m_data.dataSet().tagKey("public_transport");
162 m_tagKeys.railway = m_data.dataSet().tagKey("railway");
163 m_tagKeys.railway_platform_section = m_data.dataSet().tagKey("railway:platform:section");
164 m_tagKeys.route = m_data.dataSet().tagKey("route");
165 m_tagKeys.disused = m_data.dataSet().tagKey("disused");
166}
167
168void PlatformFinder::scanRoute(OSM::Element e, OSM::Element route)
169{
170 switch (e.type()) {
171 case OSM::Type::Null:
172 return;
173 case OSM::Type::Node:
174 scanRoute(*e.node(), route);
175 break;
176 case OSM::Type::Way:
177 OSM::for_each_node(m_data.dataSet(), *e.way(), [this, route](const OSM::Node &node) {
178 scanRoute(node, route);
179 });
180 break;
181 case OSM::Type::Relation:
182 OSM::for_each_member(m_data.dataSet(), *e.relation(), [this, route](OSM::Element e) {
183 scanRoute(e, route);
184 });
185 break;
186 }
187}
188
189void PlatformFinder::scanRoute(const OSM::Node& node, OSM::Element route)
190{
191 const auto pt = OSM::tagValue(node, m_tagKeys.public_transport);
192 if (pt.isEmpty()) {
193 return;
194 }
195
196 for (auto &p : m_platforms) {
197 if (p.stopPoint().id() == node.id) {
198 const auto l = QString::fromUtf8(route.tagValue("ref")).split(QLatin1Char(';'));
199 for (const auto &lineName : l) {
200 if (lineName.isEmpty()) {
201 continue;
202 }
203 auto lines = p.takeLines();
204 const auto it = std::lower_bound(lines.begin(), lines.end(), lineName, m_collator);
205 if (it == lines.end() || (*it) != lineName) {
206 lines.insert(it, lineName);
207 }
208 p.setLines(std::move(lines));
209 }
210 break;
211 }
212 }
213}
214
215std::vector<PlatformSection> PlatformFinder::sectionsForPath(const std::vector<const OSM::Node*> &path, const QString &platformName) const
216{
217 std::vector<PlatformSection> sections;
218 if (path.empty()) {
219 return sections;
220 }
221
222 // skip the last node for closed paths
223 for (auto it = path.begin(); it != (path.front()->id == path.back()->id ? std::prev(path.end()) : path.end()); ++it) {
224 const auto n = (*it);
225 const auto platformRef = OSM::tagValue(*n, m_tagKeys.platform_ref);
226 if (!platformRef.isEmpty() && platformRef != platformName.toUtf8()) {
227 continue; // belongs to a different track on the same platform area
228 }
229 const auto pt = OSM::tagValue(*n, m_tagKeys.public_transport);
230 if (pt == "platform_section_sign") {
231 PlatformSection sec;
232 sec.setPosition(OSM::Element(n));
233 sec.setName(QString::fromUtf8(sec.position().tagValue("platform_section_sign_value", "local_ref", "ref", "platform_section_sign")));
234 sections.push_back(std::move(sec));
235 continue;
236 }
237 const auto railway_platform_section = OSM::tagValue(*n, m_tagKeys.railway_platform_section);
238 if (!railway_platform_section.isEmpty()) {
239 PlatformSection sec;
240 sec.setPosition(OSM::Element(n));
241 sec.setName(QString::fromUtf8(railway_platform_section));
242 sections.push_back(std::move(sec));
243 continue;
244 }
245 }
246 return sections;
247}
248
249struct {
250 const char* name;
251 Platform::Mode mode;
252} static constexpr const mode_map[] = {
253 { "rail", Platform::Rail },
254 { "light_rail", Platform::Rail }, // TODO consumer code can't handle LightRail yet
255 { "subway", Platform::Subway },
256 { "tram", Platform::Tram },
257 { "monorail", Platform::Tram }, // TODO consumer code can't handle Monorail yet
258 { "bus", Platform::Bus },
259};
260
261Platform::Mode PlatformFinder::modeForElement(OSM::Element elem) const
262{
263 const auto railway = elem.tagValue(m_tagKeys.railway);
264 for (const auto &mode : mode_map) {
265 const auto modeTag = elem.tagValue(mode.name);
266 if (railway == mode.name || (!modeTag.isEmpty() && modeTag != "no")) {
267 return mode.mode;
268 }
269 }
270
271 // TODO this should eventually return Unknown
272 return Platform::Rail;
273}
274
275int PlatformFinder::levelForPlatform(const MapLevel &ml, OSM::Element e) const
276{
277 if (ml.numericLevel() != 0) {
278 return qRound(ml.numericLevel() / 10.0) * 10;
279 }
280 return e.tagValue(m_tagKeys.level).isEmpty() ? std::numeric_limits<int>::min() : 0;
281}
282
283void PlatformFinder::addPlatform(Platform &&platform)
284{
285 for (Platform &p : m_platforms) {
286 if (Platform::isSame(p, platform, m_data.dataSet())) {
287 p = Platform::merge(p, platform, m_data.dataSet());
288 return;
289 }
290 }
291
292 m_platforms.push_back(std::move(platform));
293}
294
295void PlatformFinder::mergePlatformAreas()
296{
297 // due to split areas we can end up with multplie entries for the same platform that only merge in the right order
298 // so retry until we no longer find anything matching
299 std::size_t prevCount = 0;
300
301 while (prevCount != m_platformAreas.size() && !m_platformAreas.empty()) {
302 prevCount = m_platformAreas.size();
303 for (auto it = m_platformAreas.begin(); it != m_platformAreas.end();) {
304 bool found = false;
305 for (Platform &p : m_platforms) {
306 if (Platform::isSame(p, (*it), m_data.dataSet())) {
307 p = Platform::merge(p, (*it), m_data.dataSet());
308 found = true;
309 }
310 }
311 if (found) {
312 it = m_platformAreas.erase(it);
313 } else {
314 ++it;
315 }
316 }
317
318 if (prevCount == m_platformAreas.size()) {
319 m_platforms.push_back(m_platformAreas.back());
320 m_platformAreas.erase(std::prev(m_platformAreas.end()));
321 }
322 }
323}
324
325void PlatformFinder::finalizeResult()
326{
327 if (m_platforms.empty()) {
328 return;
329 }
330
331 // integrating the platform elements can have made other platforms mergable that previously weren't,
332 // the same can happen in case of a very fine-granular track split
333 // so do another merge pass over everything we have found so far
334 for (auto it = m_platforms.begin(); it != std::prev(m_platforms.end()) && it != m_platforms.end(); ++it) {
335 for (auto it2 = std::next(it); it2 != m_platforms.end();) {
336 if (Platform::isSame(*it, *it2, m_data.dataSet())) {
337 (*it) = Platform::merge(*it, *it2, m_data.dataSet());
338 it2 = m_platforms.erase(it2);
339 } else {
340 ++it2;
341 }
342 }
343 }
344
345 // remove things that are still incomplete at this point
346 m_platforms.erase(std::remove_if(m_platforms.begin(), m_platforms.end(), [](const auto &p) {
347 return !p.isValid() || p.mode() == Platform::Bus; // ### Bus isn't properly supported yet
348 }), m_platforms.end());
349
350 // filter and sort sections on each platform
351 for (auto &p : m_platforms) {
352 auto sections = p.takeSections();
353 sections.erase(std::remove_if(sections.begin(), sections.end(), [](const auto &s) { return !s.isValid(); }), sections.end());
354 std::sort(sections.begin(), sections.end(), [this](const auto &lhs, const auto &rhs) {
355 return m_collator.compare(lhs.name(), rhs.name()) < 0;
356 });
357 p.setSections(std::move(sections));
358 }
359
360 // sort platforms by mode/name
361 std::sort(m_platforms.begin(), m_platforms.end(), [this](const auto &lhs, const auto &rhs) {
362 if (lhs.mode() == rhs.mode()) {
363 if (lhs.name() == rhs.name()) {
364 return lhs.stopPoint().id() < rhs.stopPoint().id();
365 }
366 return m_collator.compare(lhs.name(), rhs.name()) < 0;
367 }
368 return lhs.mode() < rhs.mode();
369 });
370}
Raw OSM map data, separated by levels.
Definition mapdata.h:60
A floor level.
Definition mapdata.h:28
A railway platform section.
Definition platform.h:30
OSM::Element position() const
Platform section position.
Definition platform.cpp:50
A railway platform/track.
Definition platform.h:56
static Platform merge(const Platform &lhs, const Platform &rhs, const OSM::DataSet &dataSet)
Merge two platform objects.
Definition platform.cpp:487
static QString preferredName(const QString &lhs, const QString &rhs)
Returns the preferred platform name given two possible alternatives.
Definition platform.cpp:547
static bool isSame(const Platform &lhs, const Platform &rhs, const OSM::DataSet &dataSet)
Checks if two instances refer to the same platform.
Definition platform.cpp:368
Mode
Transportation mode served by a platform.
Definition platform.h:109
A reference to any of OSM::Node/OSM::Way/OSM::Relation.
Definition element.h:24
An OSM node.
Definition datatypes.h:204
QString path(const QString &relativePath)
OSM-based multi-floor indoor maps for buildings.
QByteArray tagValue(const Elem &elem, TagKey key)
Returns the tag value for key of elem.
Definition datatypes.h:420
bool isEmpty() const const
QChar & back()
iterator begin()
iterator end()
QString fromUtf8(QByteArrayView str)
QChar & front()
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
QByteArray toUtf8() const const
CaseInsensitive
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 18 2024 12:17:55 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.