KOSMIndoorMap

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

KDE's Doxygen guidelines are available online.