KOSMIndoorMap

amenitymodel.cpp
1/*
2 SPDX-FileCopyrightText: 2023 Volker Krause <vkrause@kde.org>
3 SPDX-License-Identifier: LGPL-2.0-or-later
4*/
5
6#include "amenitymodel.h"
7#include "localization.h"
8#include "logging.h"
9#include "osmelement.h"
10
11#include <style/mapcssdeclaration_p.h>
12#include <style/mapcssstate_p.h>
13
14#include <KOSMIndoorMap/MapCSSParser>
15#include <KOSMIndoorMap/MapCSSResult>
16
17#include <KLocalizedString>
18
19#include <QDebug>
20#include <QFile>
21#include <QPointF>
22
23#include <limits>
24
25using namespace KOSMIndoorMap;
26
27AmenityModel::AmenityModel(QObject *parent)
28 : QAbstractListModel(parent)
29 , m_langs(OSM::Languages::fromQLocale(QLocale()))
30{
31}
32
33AmenityModel::~AmenityModel() = default;
34
35MapData AmenityModel::mapData() const
36{
37 return m_data;
38}
39
40void AmenityModel::setMapData(const MapData &data)
41{
42 if (m_data == data) {
43 return;
44 }
45
46 if (m_style.isEmpty()) {
48 m_style = p.parse(QStringLiteral(":/org.kde.kosmindoormap/assets/quick/amenity-model.mapcss"));
49 if (p.hasError()) {
50 qWarning() << p.errorMessage();
51 return;
52 }
53 }
54
56 m_entries.clear();
57 m_data = data;
58 if (!m_data.isEmpty()) {
59 m_style.compile(m_data.dataSet());
60 }
62 Q_EMIT mapDataChanged();
63}
64
65int AmenityModel::rowCount(const QModelIndex &parent) const
66{
67 if (parent.isValid()) {
68 return 0;
69 }
70
71 if (m_entries.empty() && !m_data.isEmpty()) {
72 // we assume that this is expensive but almost never will result in an empty result
73 // and if it does nevertheless, it's a sparsely populated tile where this is cheap
74 const_cast<AmenityModel*>(this)->populateModel();
75 }
76
77 return (int)m_entries.size();
78}
79
80static QString groupName(AmenityModel::Group group)
81{
82 switch (group) {
83 case AmenityModel::UndefinedGroup:
84 return {};
85 case AmenityModel::FoodGroup:
86 return i18nc("amenity category", "Food & Drinks");
87 case AmenityModel::ShopGroup:
88 return i18nc("amenity category", "Shops");
89 case AmenityModel::ToiletGroup:
90 return i18nc("amenity category", "Toilets");
91 case AmenityModel::HealthcareGroup:
92 return i18nc("amenity category", "Healthcare");
93 case AmenityModel::AmenityGroup:
94 return i18nc("amenity category", "Amenities");
95 case AmenityModel::AccommodationGroup:
96 return i18nc("amenity category", "Accommodations");
97 }
98 return {};
99}
100
101QString AmenityModel::iconSource(const AmenityModel::Entry &entry)
102{
103 QString s = QLatin1String(":/org.kde.kosmindoormap/assets/icons/") + entry.icon + QLatin1String(".svg");
104 return QFile::exists(s) ? s : QStringLiteral("map-symbolic");
105}
106
107QVariant AmenityModel::data(const QModelIndex &index, int role) const
108{
109 if (!checkIndex(index)) {
110 return {};
111 }
112
113 const auto &entry = m_entries[index.row()];
114 switch (role) {
115 case Qt::DisplayRole:
116 return QString::fromUtf8(entry.element.tagValue(m_langs, "name", "loc_name", "int_name"));
117 // TODO see name transliteration in OSM info model
118 case TypeNameRole:
119 {
120 const auto types = entry.element.tagValue(entry.typeKey.constData()).split(';');
121 QStringList l;
122 for (const auto &type : types) {
123 auto s = Localization::amenityType(type.trimmed().constData(), Localization::ReturnEmptyOnUnknownKey);
124 if (!s.isEmpty()) {
125 l.push_back(std::move(s));
126 }
127 }
128 return QLocale().createSeparatedList(l);
129 }
130 case CoordinateRole:
131 {
132 const auto center = entry.element.center();
133 return QPointF(center.lonF(), center.latF());
134 }
135 case LevelRole:
136 return entry.level;
137 case ElementRole:
138 return QVariant::fromValue(OSMElement(entry.element));
139 case GroupRole:
140 return entry.group;
141 case GroupNameRole:
142 return groupName(entry.group);
143 case IconSourceRole:
144 return iconSource(entry);
145 case CuisineRole:
146 return Localization::cuisineTypes(entry.element.tagValue("cuisine"), Localization::ReturnEmptyOnUnknownKey);
147 case FallbackNameRole:
148 return QString::fromUtf8(entry.element.tagValue(m_langs, "brand", "operator", "network"));
149 case OpeningHoursRole:
150 return QString::fromUtf8(entry.element.tagValue("opening_hours"));
151 }
152
153 return {};
154}
155
156QHash<int, QByteArray> AmenityModel::roleNames() const
157{
159 r.insert(NameRole, "name");
160 r.insert(TypeNameRole, "typeName");
161 r.insert(CoordinateRole, "coordinate");
162 r.insert(LevelRole, "level");
163 r.insert(ElementRole, "element");
164 r.insert(GroupRole, "group");
165 r.insert(GroupNameRole, "groupName");
166 r.insert(IconSourceRole, "iconSource");
167 r.insert(CuisineRole, "cuisine");
168 r.insert(FallbackNameRole, "fallbackName");
169 r.insert(OpeningHoursRole, "openingHours");
170 return r;
171}
172
173struct {
174 const char *groupName;
175 AmenityModel::Group group;
176} constexpr const group_map[] = {
177 { "accommodation", AmenityModel::AccommodationGroup },
178 { "amenity", AmenityModel::AmenityGroup },
179 { "healthcare", AmenityModel::HealthcareGroup },
180 { "food", AmenityModel::FoodGroup },
181 { "shop", AmenityModel::ShopGroup },
182 { "toilets", AmenityModel::ToiletGroup },
183};
184
185void AmenityModel::populateModel()
186{
187 const auto layerKey = m_data.dataSet().tagKey("layer");
188
189 MapCSSResult filterResult;
190 for (auto it = m_data.levelMap().begin(); it != m_data.levelMap().end(); ++it) {
191 for (const auto &e : (*it).second) {
192 if (!OSM::contains(m_data.boundingBox(), e.center())) {
193 continue;
194 }
195
196 MapCSSState filterState;
197 filterState.element = e;
198 m_style.evaluate(std::move(filterState), filterResult);
199
200 const auto &res = filterResult[{}];
201 if (auto prop = res.declaration(MapCSSProperty::Opacity); !prop || prop->doubleValue() < 1.0) {
202 continue; // hidden element
203 }
204
205 const auto group = res.tagValue(layerKey);
206 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; });
207 if (groupIt == std::end(group_map)) {
208 continue; // no group assigned
209 }
210
211 Entry entry;
212 entry.element = e;
213 entry.group = (*groupIt).group;
214
215 QByteArray typeKey;
216 if (auto prop = res.declaration(MapCSSProperty::FontFamily); prop) {
217 typeKey = prop->keyValue();
218 }
219 if (typeKey.isEmpty()) {
220 continue;
221 }
222
223 const auto types = e.tagValue(typeKey.constData()).split(';');
224 for (const auto &type : types) {
225 if (Localization::hasAmenityTypeTranslation(type.trimmed().constData())) {
226 entry.typeKey = std::move(typeKey);
227 break;
228 }
229 }
230 if (entry.typeKey.isEmpty()) {
231 qCDebug(Log) << "unknown type: " << types << e.url();
232 continue;
233 }
234
235 if (auto prop = res.declaration(MapCSSProperty::IconImage); prop) {
236 entry.icon = prop->stringValue();
237 if (entry.icon.isEmpty()) {
238 entry.icon = QString::fromUtf8(e.tagValue(prop->keyValue().constData()));
239 }
240 }
241
242 entry.level = (*it).first.numericLevel(); // TODO we only need one entry, not one per level!
243 m_entries.push_back(std::move(entry));
244 }
245 }
246
247 // de-duplicate multi-level entries
248 // we could also just iterate over the non-level-split data, but
249 // then we need to reparse the level data here...
250 std::sort(m_entries.begin(), m_entries.end(), [](const auto &lhs, const auto &rhs) {
251 if (lhs.element == rhs.element) {
252 return std::abs(lhs.level) < std::abs(rhs.level);
253 }
254 return lhs.element < rhs.element;
255 });
256 m_entries.erase(std::unique(m_entries.begin(), m_entries.end(), [](const auto &lhs, const auto &rhs) {
257 return lhs.element == rhs.element;
258 }), m_entries.end());
259
260 // sort by group
261 std::sort(m_entries.begin(), m_entries.end(), [](const auto &lhs, const auto &rhs) {
262 return lhs.group < rhs.group;
263 });
264 qCDebug(Log) << m_entries.size() << "amenities found";
265}
266
267#include "moc_amenitymodel.cpp"
List all amenities in a given data set.
@ CuisineRole
details on entries in the FoodGroup
@ FallbackNameRole
Brand/operator/network name, better than nothing but not the first choice to display.
@ OpeningHoursRole
opening hours expression
Result of MapCSS stylesheet evaluation for all layer selectors.
void compile(const OSM::DataSet &dataSet)
Optimizes style sheet rules for application against dataSet.
void evaluate(MapCSSState &&state, MapCSSResult &result) const
Evaluates the style sheet for a given state state (OSM element, view state, element state,...
bool isEmpty() const
Returns true if this is a default-constructed or otherwise empty/invalud style.
Raw OSM map data, separated by levels.
Definition mapdata.h:60
QML wrapper around an OSM element.
Definition osmelement.h:19
TagKey tagKey(const char *keyName) const
Look up a tag key for the given tag name, if it exists.
Definition datatypes.cpp:27
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString cuisineTypes(const QByteArray &value, Localization::TranslationOption opt=Localization::ReturnUnknownKey)
Translated values of the cuisine tag (does list splitting).
bool hasAmenityTypeTranslation(const char *value)
Returns true if we can translate value.
QString amenityType(const char *value, Localization::TranslationOption opt=Localization::ReturnUnknownKey)
Translated name for an amenity tag value (after list splitting).
OSM-based multi-floor indoor maps for buildings.
@ IconImage
image to fill the area with
@ FontFamily
the equivalent to CartoCSS's ignore-placement, non-standard extension
Low-level types and functions to work with raw OSM data as efficiently as possible.
bool checkIndex(const QModelIndex &index, CheckIndexOptions options) const const
virtual QHash< int, QByteArray > roleNames() const const
virtual QModelIndex index(int row, int column, const QModelIndex &parent) const const override
const char * constData() const const
bool isEmpty() const const
bool exists() const const
void push_back(parameter_type value)
QString createSeparatedList(const QStringList &list) const const
int row() const const
Q_EMITQ_EMIT
QObject * parent() const const
QString fromUtf8(QByteArrayView str)
DisplayRole
QTextStream & center(QTextStream &stream)
QVariant fromValue(T &&value)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:20:03 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.