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"