File indexing completed on 2024-11-24 04:15:37
0001 /* 0002 SPDX-FileCopyrightText: 2020 Volker Krause <vkrause@kde.org> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "platformmodel.h" 0008 #include "platformfinder_p.h" 0009 0010 #include <QPointF> 0011 #include <QRegularExpression> 0012 0013 #include <limits> 0014 0015 using namespace KOSMIndoorMap; 0016 0017 static constexpr auto TOP_PARENT = std::numeric_limits<quintptr>::max(); 0018 0019 PlatformModel::PlatformModel(QObject* parent) : 0020 QAbstractItemModel(parent) 0021 { 0022 m_matchTimer.setSingleShot(true); 0023 m_matchTimer.setInterval(0); 0024 connect(&m_matchTimer, &QTimer::timeout, this, &PlatformModel::matchPlatforms); 0025 0026 connect(this, &PlatformModel::mapDataChanged, &m_matchTimer, qOverload<>(&QTimer::start)); 0027 connect(this, &PlatformModel::arrivalPlatformChanged, &m_matchTimer, qOverload<>(&QTimer::start)); 0028 connect(this, &PlatformModel::departurePlatformChanged, &m_matchTimer, qOverload<>(&QTimer::start)); 0029 } 0030 0031 PlatformModel::~PlatformModel() = default; 0032 0033 MapData PlatformModel::mapData() const 0034 { 0035 return m_data; 0036 } 0037 0038 void PlatformModel::setMapData(const MapData &data) 0039 { 0040 if (m_data == data) { 0041 return; 0042 } 0043 0044 beginResetModel(); 0045 m_platforms.clear(); 0046 m_platformLabels.clear(); 0047 m_sectionsLabels.clear(); 0048 m_arrivalPlatformRow = -1; 0049 m_departurePlatformRow = -1; 0050 0051 m_data = data; 0052 if (!m_data.isEmpty()) { 0053 PlatformFinder finder; 0054 m_platforms = finder.find(m_data); 0055 0056 m_tagKeys.arrival = m_data.dataSet().makeTagKey("mx:arrival"); 0057 m_tagKeys.departure = m_data.dataSet().makeTagKey("mx:departure"); 0058 createLabels(); 0059 } 0060 endResetModel(); 0061 Q_EMIT mapDataChanged(); 0062 Q_EMIT platformIndexChanged(); 0063 } 0064 0065 bool PlatformModel::isEmpty() const 0066 { 0067 return rowCount() == 0; 0068 } 0069 0070 int PlatformModel::columnCount(const QModelIndex& parent) const 0071 { 0072 Q_UNUSED(parent); 0073 return 1; 0074 } 0075 0076 int PlatformModel::rowCount(const QModelIndex &parent) const 0077 { 0078 if (parent.isValid()) { 0079 return parent.internalId() == TOP_PARENT ? m_platforms[parent.row()].sections().size() : 0; 0080 } 0081 0082 return m_platforms.size(); 0083 } 0084 0085 QVariant PlatformModel::data(const QModelIndex &index, int role) const 0086 { 0087 if (!index.isValid()) { 0088 return {}; 0089 } 0090 0091 if (index.internalId() == TOP_PARENT) { 0092 const auto &platform = m_platforms[index.row()]; 0093 switch (role) { 0094 case Qt::DisplayRole: 0095 return platform.name(); 0096 case CoordinateRole: 0097 return QPointF(platform.position().lonF(), platform.position().latF()); 0098 case ElementRole: 0099 return QVariant::fromValue(OSM::Element(m_platformLabels[index.row()])); 0100 case LevelRole: 0101 return platform.level(); 0102 case TransportModeRole: 0103 return platform.mode(); 0104 case LinesRole: 0105 return platform.lines(); 0106 case ArrivalPlatformRole: 0107 return index.row() == m_arrivalPlatformRow; 0108 case DeparturePlatformRole: 0109 return index.row() == m_departurePlatformRow; 0110 } 0111 } else { 0112 const auto &platform = m_platforms[index.internalId()]; 0113 const auto §ion = platform.sections()[index.row()]; 0114 switch (role) { 0115 case Qt::DisplayRole: 0116 return section.name(); 0117 case CoordinateRole: 0118 return QPointF(section.position().center().lonF(), section.position().center().latF()); 0119 case ElementRole: 0120 return QVariant::fromValue(OSM::Element(m_sectionsLabels[index.internalId()][index.row()])); 0121 case LevelRole: 0122 return platform.level(); 0123 } 0124 } 0125 0126 return {}; 0127 } 0128 0129 QModelIndex PlatformModel::index(int row, int column, const QModelIndex &parent) const 0130 { 0131 if (!parent.isValid()) { 0132 return createIndex(row, column, TOP_PARENT); 0133 } 0134 return createIndex(row, column, parent.row()); 0135 } 0136 0137 QModelIndex PlatformModel::parent(const QModelIndex &child) const 0138 { 0139 if (!child.isValid() || child.internalId() == TOP_PARENT) { 0140 return {}; 0141 } 0142 return createIndex(child.internalId(), 0, TOP_PARENT); 0143 } 0144 0145 QHash<int, QByteArray> PlatformModel::roleNames() const 0146 { 0147 auto n = QAbstractItemModel::roleNames(); 0148 n.insert(CoordinateRole, "coordinate"); 0149 n.insert(ElementRole, "osmElement"); 0150 n.insert(LevelRole, "level"); 0151 n.insert(TransportModeRole, "mode"); 0152 n.insert(LinesRole, "lines"); 0153 n.insert(ArrivalPlatformRole, "isArrivalPlatform"); 0154 n.insert(DeparturePlatformRole, "isDeparturePlatform"); 0155 return n; 0156 } 0157 0158 Platform PlatformModel::arrivalPlatform() const 0159 { 0160 return m_arrivalPlatform; 0161 } 0162 0163 void PlatformModel::setArrivalPlatform(const Platform &platform) 0164 { 0165 m_arrivalPlatform = platform; 0166 Q_EMIT arrivalPlatformChanged(); 0167 } 0168 0169 void PlatformModel::setArrivalPlatform(const QString &name, Platform::Mode mode) 0170 { 0171 m_arrivalPlatform.setName(name); 0172 m_arrivalPlatform.setMode(mode); 0173 Q_EMIT arrivalPlatformChanged(); 0174 } 0175 0176 Platform PlatformModel::departurePlatform() const 0177 { 0178 return m_departurePlatform; 0179 } 0180 0181 void PlatformModel::setDeparturePlatform(const Platform &platform) 0182 { 0183 m_departurePlatform = platform; 0184 Q_EMIT departurePlatformChanged(); 0185 } 0186 0187 void PlatformModel::setDeparturePlatform(const QString &name, Platform::Mode mode) 0188 { 0189 m_departurePlatform.setName(name); 0190 m_departurePlatform.setMode(mode); 0191 Q_EMIT departurePlatformChanged(); 0192 } 0193 0194 int PlatformModel::arrivalPlatformRow() const 0195 { 0196 return m_arrivalPlatformRow; 0197 } 0198 0199 int PlatformModel::departurePlatformRow() const 0200 { 0201 return m_departurePlatformRow; 0202 } 0203 0204 void PlatformModel::matchPlatforms() 0205 { 0206 setPlatformTag(m_arrivalPlatformRow, m_tagKeys.arrival, false); 0207 applySectionSelection(m_arrivalPlatformRow, m_tagKeys.arrival, {}); 0208 m_arrivalPlatformRow = matchPlatform(m_arrivalPlatform); 0209 setPlatformTag(m_arrivalPlatformRow, m_tagKeys.arrival, true); 0210 setPlatformTag(m_departurePlatformRow, m_tagKeys.departure, false); 0211 applySectionSelection(m_departurePlatformRow, m_tagKeys.departure, {}); 0212 m_departurePlatformRow = matchPlatform(m_departurePlatform); 0213 setPlatformTag(m_departurePlatformRow, m_tagKeys.departure, true); 0214 Q_EMIT platformIndexChanged(); 0215 0216 if (m_arrivalPlatformRow >= 0) { 0217 const auto idx = index(m_arrivalPlatformRow, 0); 0218 Q_EMIT dataChanged(idx, idx); 0219 applySectionSelection(m_arrivalPlatformRow, m_tagKeys.arrival, effectiveArrivalSections()); 0220 Q_EMIT dataChanged(index(0, 0, idx), index(rowCount(idx) - 1, 0, idx)); 0221 } 0222 if (m_departurePlatformRow >= 0) { 0223 const auto idx = index(m_departurePlatformRow, 0); 0224 Q_EMIT dataChanged(idx, idx); 0225 applySectionSelection(m_departurePlatformRow, m_tagKeys.departure, effectiveDepartureSections()); 0226 Q_EMIT dataChanged(index(0, 0, idx), index(rowCount(idx) - 1, 0, idx)); 0227 } 0228 } 0229 0230 static bool isPossiblySamePlatformName(const QString &name, const QString &platform) 0231 { 0232 // <platform>\w?<section(s)> 0233 if (name.size() > platform.size()) { 0234 QRegularExpression exp(QStringLiteral("(\\d+)\\s?[A-Z-]+")); 0235 const auto match = exp.match(name); 0236 return match.hasMatch() && match.captured(1) == platform; 0237 } 0238 0239 return false; 0240 } 0241 0242 int PlatformModel::matchPlatform(const Platform &platform) const 0243 { 0244 if (!platform.ifopt().isEmpty()) { // try IFOPT first, if we have that 0245 const auto it = std::find_if(m_platforms.begin(), m_platforms.end(), [platform](const auto &p) { 0246 return p.ifopt() == platform.ifopt(); 0247 }); 0248 if (it != m_platforms.end()) { 0249 return std::distance(m_platforms.begin(), it); 0250 } 0251 } 0252 0253 if (platform.name().isEmpty()) { 0254 return -1; 0255 } 0256 0257 // exact match 0258 int i = 0; 0259 for (const auto &p : m_platforms) { 0260 if (p.name() == platform.name() && p.mode() == platform.mode()) { 0261 return i; 0262 } 0263 ++i; 0264 } 0265 0266 // fuzzy match 0267 // TODO this likely will need to handle more scenarios 0268 // TODO when we get section ranges here, we might want to use those as well? 0269 i = 0; 0270 for (const auto &p : m_platforms) { 0271 if (p.mode() == platform.mode() && isPossiblySamePlatformName(platform.name(), p.name())) { 0272 return i; 0273 } 0274 ++i; 0275 } 0276 0277 return -1; 0278 } 0279 0280 void PlatformModel::createLabels() 0281 { 0282 const auto platformTag = m_data.dataSet().makeTagKey("mx:platform"); 0283 const auto sectionTag = m_data.dataSet().makeTagKey("mx:platform_section"); 0284 0285 m_platformLabels.reserve(m_platforms.size()); 0286 m_sectionsLabels.resize(m_platforms.size()); 0287 for (std::size_t i = 0; i < m_platforms.size(); ++i) { 0288 const auto &p = m_platforms[i]; 0289 0290 // TODO using the full edge/track path here might be better for layouting 0291 auto node = new OSM::Node; 0292 node->id = m_data.dataSet().nextInternalId(); 0293 node->coordinate = p.position(); 0294 OSM::setTagValue(*node, platformTag, p.name().toUtf8()); 0295 m_platformLabels.push_back(OSM::UniqueElement(node)); 0296 0297 m_sectionsLabels[i].reserve(p.sections().size()); 0298 for (const auto &sec : p.sections()) { 0299 auto node = new OSM::Node; 0300 node->id = m_data.dataSet().nextInternalId(); 0301 node->coordinate = sec.position().center(); 0302 OSM::setTagValue(*node, sectionTag, sec.name().toUtf8()); 0303 m_sectionsLabels[i].push_back(OSM::UniqueElement(node)); 0304 } 0305 } 0306 } 0307 0308 void PlatformModel::setPlatformTag(int idx, OSM::TagKey key, bool enabled) 0309 { 0310 if (idx < 0) { 0311 return; 0312 } 0313 0314 m_platformLabels[idx].setTagValue(key, enabled ? "1" : "0"); 0315 } 0316 0317 static QStringView stripPlatform(QStringView p) 0318 { 0319 while (!p.empty() && (p[0].isDigit() || p[0].isSpace())) { 0320 p = p.mid(1); 0321 } 0322 return p; 0323 } 0324 0325 QStringView PlatformModel::effectiveArrivalSections() const 0326 { 0327 // TODO prefer explicit section selectors once implemented/when present 0328 return stripPlatform(m_arrivalPlatform.name()); 0329 } 0330 0331 QStringView PlatformModel::effectiveDepartureSections() const 0332 { 0333 // TODO prefer explicit section selectors once implemented/when present 0334 return stripPlatform(m_departurePlatform.name()); 0335 } 0336 0337 static std::vector<QChar> parseSectionSet(QStringView sections) 0338 { 0339 std::vector<QChar> result; 0340 const auto ranges = sections.split(QLatin1Char(',')); 0341 for (const auto &r : ranges) { 0342 if (r.size() == 1) { 0343 result.push_back(r[0]); 0344 continue; 0345 } 0346 if (r.size() == 3 && r[1] == QLatin1Char('-') && r[0] < r[2]) { 0347 for (QChar c = r[0]; c <= r[2]; c = QChar(c.unicode() + 1)) { 0348 result.push_back(c); 0349 } 0350 continue; 0351 } 0352 qDebug() << "failed to parse platform section expression:" << r; 0353 } 0354 return result; 0355 } 0356 0357 void PlatformModel::applySectionSelection(int platformIdx, OSM::TagKey key, QStringView sections) 0358 { 0359 if (platformIdx < 0) { 0360 return; 0361 } 0362 0363 const auto sectionSet = parseSectionSet(sections); 0364 0365 std::size_t totalSelected = 0; 0366 for (std::size_t i = 0; i < m_platforms[platformIdx].sections().size(); ++i) { 0367 if (std::any_of(sectionSet.begin(), sectionSet.end(), [this, i, platformIdx](const QChar s) { 0368 return s == m_platforms[platformIdx].sections()[i].name(); 0369 })) { 0370 m_sectionsLabels[platformIdx][i].setTagValue(key, "1"); 0371 ++totalSelected; 0372 } else { 0373 m_sectionsLabels[platformIdx][i].setTagValue(key, "0"); 0374 } 0375 } 0376 0377 // if we enabled all sections, disable them again, highlighting adds no value then 0378 if (totalSelected == m_sectionsLabels[platformIdx].size()) { 0379 for (auto &s : m_sectionsLabels[platformIdx]) { 0380 s.setTagValue(key, "0"); 0381 } 0382 } 0383 } 0384 0385 #include "moc_platformmodel.cpp"