File indexing completed on 2024-05-12 04:42:08

0001 /*
0002     SPDX-FileCopyrightText: 2023 Volker Krause <vkrause@kde.org>
0003     SPDX-License-Identifier: LGPL-2.0-or-later
0004 */
0005 
0006 #include "amenitymodel.h"
0007 #include "localization.h"
0008 #include "logging.h"
0009 #include "osmelement.h"
0010 
0011 #include <style/mapcssdeclaration_p.h>
0012 #include <style/mapcssstate_p.h>
0013 
0014 #include <KOSMIndoorMap/MapCSSParser>
0015 #include <KOSMIndoorMap/MapCSSResult>
0016 
0017 #include <KLocalizedString>
0018 
0019 #include <QDebug>
0020 #include <QFile>
0021 #include <QPointF>
0022 
0023 #include <limits>
0024 
0025 using namespace KOSMIndoorMap;
0026 
0027 AmenityModel::AmenityModel(QObject *parent)
0028     : QAbstractListModel(parent)
0029     , m_langs(OSM::Languages::fromQLocale(QLocale()))
0030 {
0031 }
0032 
0033 AmenityModel::~AmenityModel() = default;
0034 
0035 MapData AmenityModel::mapData() const
0036 {
0037     return m_data;
0038 }
0039 
0040 void AmenityModel::setMapData(const MapData &data)
0041 {
0042     if (m_data == data) {
0043         return;
0044     }
0045 
0046     if (m_style.isEmpty()) {
0047         MapCSSParser p;
0048         m_style = p.parse(QStringLiteral(":/org.kde.kosmindoormap/assets/quick/amenity-model.mapcss"));
0049         if (p.hasError()) {
0050             qWarning() << p.errorMessage();
0051             return;
0052         }
0053     }
0054 
0055     beginResetModel();
0056     m_entries.clear();
0057     m_data = data;
0058     if (!m_data.isEmpty()) {
0059         m_style.compile(m_data.dataSet());
0060     }
0061     endResetModel();
0062     Q_EMIT mapDataChanged();
0063 }
0064 
0065 int AmenityModel::rowCount(const QModelIndex &parent) const
0066 {
0067     if (parent.isValid()) {
0068         return 0;
0069     }
0070 
0071     if (m_entries.empty() && !m_data.isEmpty()) {
0072         // we assume that this is expensive but almost never will result in an empty result
0073         // and if it does nevertheless, it's a sparsely populated tile where this is cheap
0074         const_cast<AmenityModel*>(this)->populateModel();
0075     }
0076 
0077     return (int)m_entries.size();
0078 }
0079 
0080 static QString groupName(AmenityModel::Group group)
0081 {
0082     switch (group) {
0083         case AmenityModel::UndefinedGroup:
0084             return {};
0085         case AmenityModel::FoodGroup:
0086             return i18nc("amenity category", "Food & Drinks");
0087         case AmenityModel::ShopGroup:
0088             return i18nc("amenity category", "Shops");
0089         case AmenityModel::ToiletGroup:
0090             return i18nc("amenity category", "Toilets");
0091         case AmenityModel::HealthcareGroup:
0092             return i18nc("amenity category", "Healthcare");
0093         case AmenityModel::AmenityGroup:
0094             return i18nc("amenity category", "Amenities");
0095         case AmenityModel::AccommodationGroup:
0096             return i18nc("amenity category", "Accommodations");
0097     }
0098     return {};
0099 }
0100 
0101 QString AmenityModel::iconSource(const AmenityModel::Entry &entry)
0102 {
0103     QString s = QLatin1String(":/org.kde.kosmindoormap/assets/icons/") + entry.icon + QLatin1String(".svg");
0104     return QFile::exists(s) ? s : QStringLiteral("map-symbolic");
0105 }
0106 
0107 QVariant AmenityModel::data(const QModelIndex &index, int role) const
0108 {
0109     if (!checkIndex(index)) {
0110         return {};
0111     }
0112 
0113     const auto &entry = m_entries[index.row()];
0114     switch (role) {
0115         case Qt::DisplayRole:
0116             return QString::fromUtf8(entry.element.tagValue(m_langs, "name", "loc_name", "int_name"));
0117             // TODO see name transliteration in OSM info model
0118         case TypeNameRole:
0119         {
0120             const auto types = entry.element.tagValue(entry.typeKey.constData()).split(';');
0121             QStringList l;
0122             for (const auto &type : types) {
0123                 auto s = Localization::amenityType(type.trimmed().constData(), Localization::ReturnEmptyOnUnknownKey);
0124                 if (!s.isEmpty()) {
0125                     l.push_back(std::move(s));
0126                 }
0127             }
0128             return QLocale().createSeparatedList(l);
0129         }
0130         case CoordinateRole:
0131         {
0132             const auto center = entry.element.center();
0133             return QPointF(center.lonF(), center.latF());
0134         }
0135         case LevelRole:
0136             return entry.level;
0137         case ElementRole:
0138             return QVariant::fromValue(OSMElement(entry.element));
0139         case GroupRole:
0140             return entry.group;
0141         case GroupNameRole:
0142             return groupName(entry.group);
0143         case IconSourceRole:
0144             return iconSource(entry);
0145         case CuisineRole:
0146             return Localization::cuisineTypes(entry.element.tagValue("cuisine"), Localization::ReturnEmptyOnUnknownKey);
0147         case FallbackNameRole:
0148             return QString::fromUtf8(entry.element.tagValue(m_langs, "brand", "operator", "network"));
0149         case OpeningHoursRole:
0150             return QString::fromUtf8(entry.element.tagValue("opening_hours"));
0151     }
0152 
0153     return {};
0154 }
0155 
0156 QHash<int, QByteArray> AmenityModel::roleNames() const
0157 {
0158     auto r = QAbstractListModel::roleNames();
0159     r.insert(NameRole, "name");
0160     r.insert(TypeNameRole, "typeName");
0161     r.insert(CoordinateRole, "coordinate");
0162     r.insert(LevelRole, "level");
0163     r.insert(ElementRole, "element");
0164     r.insert(GroupRole, "group");
0165     r.insert(GroupNameRole, "groupName");
0166     r.insert(IconSourceRole, "iconSource");
0167     r.insert(CuisineRole, "cuisine");
0168     r.insert(FallbackNameRole, "fallbackName");
0169     r.insert(OpeningHoursRole, "openingHours");
0170     return r;
0171 }
0172 
0173 struct {
0174     const char *groupName;
0175     AmenityModel::Group group;
0176 } constexpr const group_map[] = {
0177     { "accommodation", AmenityModel::AccommodationGroup },
0178     { "amenity", AmenityModel::AmenityGroup },
0179     { "healthcare", AmenityModel::HealthcareGroup },
0180     { "food", AmenityModel::FoodGroup },
0181     { "shop", AmenityModel::ShopGroup },
0182     { "toilets", AmenityModel::ToiletGroup },
0183 };
0184 
0185 void AmenityModel::populateModel()
0186 {
0187     const auto layerKey = m_data.dataSet().tagKey("layer");
0188 
0189     MapCSSResult filterResult;
0190     for (auto it = m_data.levelMap().begin(); it != m_data.levelMap().end(); ++it) {
0191         for (const auto &e : (*it).second) {
0192             if (!OSM::contains(m_data.boundingBox(), e.center())) {
0193                 continue;
0194             }
0195 
0196             MapCSSState filterState;
0197             filterState.element = e;
0198             m_style.evaluate(std::move(filterState), filterResult);
0199 
0200             const auto &res = filterResult[{}];
0201             if (auto prop = res.declaration(MapCSSProperty::Opacity); !prop || prop->doubleValue() < 1.0) {
0202                 continue; // hidden element
0203             }
0204 
0205             const auto group = res.tagValue(layerKey);
0206             const auto groupIt = std::find_if(std::begin(group_map), std::end(group_map), [&group](const auto &m) { return std::strcmp(m.groupName, group.constData()) == 0; });
0207             if (groupIt == std::end(group_map)) {
0208                 continue; // no group assigned
0209             }
0210 
0211             Entry entry;
0212             entry.element = e;
0213             entry.group = (*groupIt).group;
0214 
0215             QByteArray typeKey;
0216             if (auto prop = res.declaration(MapCSSProperty::FontFamily); prop) {
0217                 typeKey = prop->keyValue();
0218             }
0219             if (typeKey.isEmpty()) {
0220                 continue;
0221             }
0222 
0223             const auto types = e.tagValue(typeKey.constData()).split(';');
0224             for (const auto &type : types) {
0225                 if (Localization::hasAmenityTypeTranslation(type.trimmed().constData())) {
0226                     entry.typeKey = std::move(typeKey);
0227                     break;
0228                 }
0229             }
0230             if (entry.typeKey.isEmpty()) {
0231                 qCDebug(Log) << "unknown type: " << types << e.url();
0232                 continue;
0233             }
0234 
0235             if (auto prop = res.declaration(MapCSSProperty::IconImage); prop) {
0236                 entry.icon = prop->stringValue();
0237                 if (entry.icon.isEmpty()) {
0238                     entry.icon = QString::fromUtf8(e.tagValue(prop->keyValue().constData()));
0239                 }
0240             }
0241 
0242             entry.level = (*it).first.numericLevel(); // TODO we only need one entry, not one per level!
0243             m_entries.push_back(std::move(entry));
0244         }
0245     }
0246 
0247     // de-duplicate multi-level entries
0248     // we could also just iterate over the non-level-split data, but
0249     // then we need to reparse the level data here...
0250     std::sort(m_entries.begin(), m_entries.end(), [](const auto &lhs, const auto &rhs) {
0251         if (lhs.element == rhs.element) {
0252             return std::abs(lhs.level) < std::abs(rhs.level);
0253         }
0254         return lhs.element < rhs.element;
0255     });
0256     m_entries.erase(std::unique(m_entries.begin(), m_entries.end(), [](const auto &lhs, const auto &rhs) {
0257         return lhs.element == rhs.element;
0258     }), m_entries.end());
0259 
0260     // sort by group
0261     std::sort(m_entries.begin(), m_entries.end(), [](const auto &lhs, const auto &rhs) {
0262         return lhs.group < rhs.group;
0263     });
0264     qCDebug(Log) << m_entries.size() << "amenities found";
0265 }
0266 
0267 #include "moc_amenitymodel.cpp"