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 <KCountry>
18#include <KCountrySubdivision>
19#include <KLocalizedString>
20
21#include <QDebug>
22#include <QFile>
23#include <QPointF>
24#include <QTimeZone>
25
26#include <limits>
27
28using namespace KOSMIndoorMap;
29
30AmenityModel::AmenityModel(QObject *parent)
31 : QAbstractListModel(parent)
32 , m_langs(OSM::Languages::fromQLocale(QLocale()))
33{
34}
35
36AmenityModel::~AmenityModel() = default;
37
38MapData AmenityModel::mapData() const
39{
40 return m_data;
41}
42
43void AmenityModel::setMapData(const MapData &data)
44{
45 if (m_data == data) {
46 return;
47 }
48
49 if (m_style.isEmpty()) {
51 m_style = p.parse(QStringLiteral(":/org.kde.kosmindoormap/assets/quick/amenity-model.mapcss"));
52 if (p.hasError()) {
53 qWarning() << p.errorMessage();
54 return;
55 }
56 }
57
59 m_entries.clear();
60 m_data = data;
61 if (!m_data.isEmpty()) {
62 m_style.compile(m_data.dataSet());
63 }
65 Q_EMIT mapDataChanged();
66}
67
68int AmenityModel::rowCount(const QModelIndex &parent) const
69{
70 if (parent.isValid()) {
71 return 0;
72 }
73
74 if (m_entries.empty() && !m_data.isEmpty()) {
75 // we assume that this is expensive but almost never will result in an empty result
76 // and if it does nevertheless, it's a sparsely populated tile where this is cheap
77 const_cast<AmenityModel*>(this)->populateModel();
78 }
79
80 return (int)m_entries.size();
81}
82
83static QString groupName(AmenityModel::Group group)
84{
85 switch (group) {
86 case AmenityModel::UndefinedGroup:
87 return {};
88 case AmenityModel::FoodGroup:
89 return i18nc("amenity category", "Food & Drinks");
90 case AmenityModel::ShopGroup:
91 return i18nc("amenity category", "Shops");
92 case AmenityModel::ToiletGroup:
93 return i18nc("amenity category", "Toilets");
94 case AmenityModel::HealthcareGroup:
95 return i18nc("amenity category", "Healthcare");
96 case AmenityModel::AmenityGroup:
97 return i18nc("amenity category", "Amenities");
98 case AmenityModel::AccommodationGroup:
99 return i18nc("amenity category", "Accommodations");
100 }
101 return {};
102}
103
104QString AmenityModel::iconSource(const AmenityModel::Entry &entry)
105{
106 QString s = QLatin1String(":/org.kde.kosmindoormap/assets/icons/") + entry.icon + QLatin1String(".svg");
107 return QFile::exists(s) ? s : QStringLiteral("map-symbolic");
108}
109
110QVariant AmenityModel::data(const QModelIndex &index, int role) const
111{
112 if (!checkIndex(index)) {
113 return {};
114 }
115
116 const auto &entry = m_entries[index.row()];
117 switch (role) {
118 case Qt::DisplayRole:
119 return QString::fromUtf8(entry.element.tagValue(m_langs, "name", "loc_name", "int_name"));
120 // TODO see name transliteration in OSM info model
121 case TypeNameRole:
122 return Localization::amenityTypes(entry.element.tagValue(entry.typeKey.constData()), Localization::ReturnEmptyOnUnknownKey);
123 case CoordinateRole:
124 {
125 const auto center = entry.element.center();
126 return QPointF(center.lonF(), center.latF());
127 }
128 case LevelRole:
129 return entry.level;
130 case ElementRole:
131 return QVariant::fromValue(OSMElement(entry.element));
132 case GroupRole:
133 return entry.group;
134 case GroupNameRole:
135 return groupName(entry.group);
136 case IconSourceRole:
137 return iconSource(entry);
138 case CuisineRole:
139 {
140 auto s = Localization::cuisineTypes(entry.element.tagValue("cuisine"), Localization::ReturnEmptyOnUnknownKey);
141 if (!s.isEmpty()) {
142 return s;
143 }
144 return Localization::amenityTypes(entry.element.tagValue("vending"), Localization::ReturnEmptyOnUnknownKey);
145 }
146 case FallbackNameRole:
147 return QString::fromUtf8(entry.element.tagValue(m_langs, "brand", "operator", "network"));
148 case OpeningHoursRole:
149 return QString::fromUtf8(entry.element.tagValue("opening_hours"));
150 case TimeZoneRole:
151 return QString::fromUtf8(m_data.timeZone().id());
152 case RegionCodeRole:
153 if (m_data.regionCode().size() > 3) {
154 return m_data.regionCode();
155 }
156 if (const auto subdiv = KCountrySubdivision::fromLocation((float)entry.element.center().latF(), (float)entry.element.center().lonF()); subdiv.isValid()) {
157 return subdiv.code();
158 }
159 if (const auto c = KCountry::fromLocation((float)entry.element.center().latF(), (float)entry.element.center().lonF()); c.isValid()) {
160 return c.alpha2();
161 }
162 return m_data.regionCode();
164 return Localization::genderSegregation(entry.element);
165 case DetailsLabelRole:
166 switch (entry.group) {
167 case FoodGroup:
168 return data(index, CuisineRole);
169 case ToiletGroup:
170 return data(index, ToiletDetailsRole);
171 default:
172 break;
173 }
174 break;
175 }
176
177 return {};
178}
179
180QHash<int, QByteArray> AmenityModel::roleNames() const
181{
183 r.insert(NameRole, "name");
184 r.insert(TypeNameRole, "typeName");
185 r.insert(CoordinateRole, "coordinate");
186 r.insert(LevelRole, "level");
187 r.insert(ElementRole, "element");
188 r.insert(GroupRole, "group");
189 r.insert(GroupNameRole, "groupName");
190 r.insert(IconSourceRole, "iconSource");
191 r.insert(CuisineRole, "cuisine");
192 r.insert(FallbackNameRole, "fallbackName");
193 r.insert(OpeningHoursRole, "openingHours");
194 r.insert(TimeZoneRole, "timeZone");
195 r.insert(RegionCodeRole, "regionCode");
196 r.insert(ToiletDetailsRole, "toiletDetails");
197 r.insert(DetailsLabelRole, "detailsLabel");
198 return r;
199}
200
201struct {
202 const char *groupName;
203 AmenityModel::Group group;
204} constexpr const group_map[] = {
205 { "accommodation", AmenityModel::AccommodationGroup },
206 { "amenity", AmenityModel::AmenityGroup },
207 { "healthcare", AmenityModel::HealthcareGroup },
208 { "food", AmenityModel::FoodGroup },
209 { "shop", AmenityModel::ShopGroup },
210 { "toilets", AmenityModel::ToiletGroup },
211};
212
213void AmenityModel::populateModel()
214{
215 const auto layerKey = m_data.dataSet().tagKey("layer");
216
217 MapCSSResult filterResult;
218 for (auto it = m_data.levelMap().begin(); it != m_data.levelMap().end(); ++it) {
219 for (const auto &e : (*it).second) {
220 if (!OSM::contains(m_data.boundingBox(), e.center())) {
221 continue;
222 }
223
224 MapCSSState filterState;
225 filterState.element = e;
226 m_style.initializeState(filterState);
227 m_style.evaluate(filterState, filterResult);
228
229 const auto &res = filterResult[{}];
230 if (auto prop = res.declaration(MapCSSProperty::Opacity); !prop || prop->doubleValue() < 1.0) {
231 continue; // hidden element
232 }
233
234 const auto group = res.resolvedTagValue(layerKey, filterState);
235 if (!group) {
236 continue;
237 }
238 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; });
239 if (groupIt == std::end(group_map)) {
240 continue; // no group assigned
241 }
242
243 Entry entry;
244 entry.element = e;
245 entry.group = (*groupIt).group;
246
247 QByteArray typeKey;
248 if (auto prop = res.declaration(MapCSSProperty::FontFamily); prop) {
249 typeKey = prop->keyValue();
250 }
251 if (typeKey.isEmpty()) {
252 continue;
253 }
254
255 const auto types = e.tagValue(typeKey.constData()).split(';');
256 for (const auto &type : types) {
257 if (Localization::hasAmenityTypeTranslation(type.trimmed().constData())) {
258 entry.typeKey = std::move(typeKey);
259 break;
260 }
261 }
262 if (entry.typeKey.isEmpty()) {
263 qCDebug(Log) << "unknown type: " << types << e.url();
264 continue;
265 }
266
267 if (auto prop = res.declaration(MapCSSProperty::IconImage); prop) {
268 entry.icon = prop->stringValue();
269 if (entry.icon.isEmpty()) {
270 entry.icon = QString::fromUtf8(e.tagValue(prop->keyValue().constData()));
271 }
272 }
273
274 entry.level = (*it).first.numericLevel(); // TODO we only need one entry, not one per level!
275 m_entries.push_back(std::move(entry));
276 }
277 }
278
279 // de-duplicate multi-level entries
280 // we could also just iterate over the non-level-split data, but
281 // then we need to reparse the level data here...
282 std::sort(m_entries.begin(), m_entries.end(), [](const auto &lhs, const auto &rhs) {
283 if (lhs.element == rhs.element) {
284 return std::abs(lhs.level) < std::abs(rhs.level);
285 }
286 return lhs.element < rhs.element;
287 });
288 m_entries.erase(std::unique(m_entries.begin(), m_entries.end(), [](const auto &lhs, const auto &rhs) {
289 return lhs.element == rhs.element;
290 }), m_entries.end());
291
292 // sort by group
293 std::sort(m_entries.begin(), m_entries.end(), [](const auto &lhs, const auto &rhs) {
294 return lhs.group < rhs.group;
295 });
296 qCDebug(Log) << m_entries.size() << "amenities found";
297}
298
299#include "moc_amenitymodel.cpp"
static KCountrySubdivision fromLocation(float latitude, float longitude)
static KCountry fromLocation(float latitude, float longitude)
bool isValid() const
List all amenities in a given data set.
@ DetailsLabelRole
section-dependent details label (e.g. CuisineRole or ToiletDetailsRole)
@ CuisineRole
details on entries in the FoodGroup
@ RegionCodeRole
ISO 3166-1/2 code of the region this amenity is in (relevant for opening hours interpretation)
@ FallbackNameRole
Brand/operator/network name, better than nothing but not the first choice to display.
@ TimeZoneRole
IANA timezone id this amenity is in (relevant for opening hours interpretation)
@ OpeningHoursRole
opening hours expression
@ ToiletDetailsRole
details information for the ToiletGroup
bool hasError() const
Returns true if an error occured during parsing and the returned style is invalid.
Result of MapCSS stylesheet evaluation for all layer selectors.
void evaluate(const MapCSSState &state, MapCSSResult &result) const
Evaluates the style sheet for a given state state (OSM element, view state, element state,...
void compile(OSM::DataSet &dataSet)
Optimizes style sheet rules for application against dataSet.
bool isEmpty() const
Returns true if this is a default-constructed or otherwise empty/invalud style.
void initializeState(MapCSSState &state) const
Initializes the evaluation state.
Raw OSM map data, separated by levels.
Definition mapdata.h:60
QML wrapper around an OSM element.
Definition osmelement.h:21
TagKey tagKey(const char *keyName) const
Look up a tag key for the given tag name, if it exists.
Definition datatypes.cpp:38
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).
QString genderSegregation(OSM::Element element)
Translated gender segregation information e.g.
QString amenityTypes(const QByteArray &value, Localization::TranslationOption opt=Localization::ReturnUnknownKey)
Translated list of amenity tag values (including list splitting).
bool hasAmenityTypeTranslation(const char *value)
Returns true if we can translate value.
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
int row() const const
Q_EMITQ_EMIT
QObject * parent() const const
QString fromUtf8(QByteArrayView str)
qsizetype size() const const
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 Fri Jul 26 2024 11:57:46 by doxygen 1.11.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.