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 &section = 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"