KOSMIndoorMap

roommodel.cpp
1/*
2 SPDX-FileCopyrightText: 2024 Volker Krause <vkrause@kde.org>
3 SPDX-License-Identifier: LGPL-2.0-or-later
4*/
5
6#include "roommodel.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
27RoomModel::RoomModel(QObject *parent)
28 : QAbstractListModel(parent)
29 , m_langs(OSM::Languages::fromQLocale(QLocale()))
30{
31 connect(this, &RoomModel::timeChanged, this, [this]() {
32 beginResetModel();
33 m_rooms.clear();
34 m_buildings.clear();
35 endResetModel();
36 });
37}
38
39RoomModel::~RoomModel() = default;
40
41MapData RoomModel::mapData() const
42{
43 return m_data;
44}
45
46void RoomModel::setMapData(const MapData &data)
47{
48 if (m_data == data) {
49 return;
50 }
51
52 if (m_style.isEmpty()) {
54 m_style = p.parse(QStringLiteral(":/org.kde.kosmindoormap/assets/quick/room-model.mapcss"));
55 if (p.hasError()) {
56 qWarning() << p.errorMessage();
57 return;
58 }
59 }
60
62 m_buildings.clear();
63 m_rooms.clear();
64 m_data = data;
65 if (!m_data.isEmpty()) {
66 m_style.compile(m_data.dataSet());
67 }
69 Q_EMIT mapDataChanged();
70}
71
72int RoomModel::rowCount(const QModelIndex &parent) const
73{
74 if (parent.isValid()) {
75 return 0;
76 }
77
78 ensurePopulated();
79 return (int)m_rooms.size();
80}
81
82QVariant RoomModel::data(const QModelIndex &index, int role) const
83{
84 if (!checkIndex(index)) {
85 return {};
86 }
87
88 const auto &room = m_rooms[index.row()];
89 switch (role) {
90 case NameRole:
91 // TODO better name/number handling - separate roles?
92 return room.name;
93 case NumberRole:
94 return QString::fromUtf8(room.element.tagValue("ref"));
95 case TypeNameRole:
96 {
97 const auto types = room.element.tagValue("room", "amenity").split(';');
99 for (const auto &type : types) {
100 if (type == "yes") {
101 continue;
102 }
103 auto s = Localization::amenityType(type.trimmed().constData(), Localization::ReturnEmptyOnUnknownKey);
104 if (!s.isEmpty()) {
105 l.push_back(std::move(s));
106 }
107 }
108 return QLocale().createSeparatedList(l);
109 }
110 case CoordinateRole:
111 {
112 const auto center = room.element.center();
113 return QPointF(center.lonF(), center.latF());
114 }
115 case LevelRole:
116 return room.level;
117 case ElementRole:
118 return QVariant::fromValue(OSMElement(room.element));
119 case BuildingNameRole:
120 return QString::fromUtf8(room.buildingElement.tagValue(m_langs, "name", "local_ref", "ref"));
122 {
123 auto s = QString::fromUtf8(room.levelElement.tagValue(m_langs, "name", "level:ref"));
124 if (!s.isEmpty()) {
125 return s;
126 }
127
128 if ((room.level / 10) == 0) {
129 return i18n("Ground floor");
130 }
131 return i18n("Floor %1", room.level / 10); // TODO this isn't properly localized...
132 }
134 {
135 auto s = QString::fromUtf8(room.levelElement.tagValue(m_langs, "level:ref"));
136 if (!s.isEmpty()) {
137 return s;
138 }
139 return QString::number(room.level/ 10); // TODO this could use localized floor level abbrevations
140 }
141
142 }
143
144 return {};
145}
146
147QHash<int, QByteArray> RoomModel::roleNames() const
148{
150 r.insert(NameRole, "name");
151 r.insert(NumberRole, "number");
152 r.insert(TypeNameRole, "typeName");
153 r.insert(CoordinateRole, "coordinate");
154 r.insert(LevelRole, "level");
155 r.insert(ElementRole, "element");
156 r.insert(BuildingNameRole, "buildingName");
157 r.insert(LevelLongNameRole, "levelLongName");
158 r.insert(LevelShortNameRole, "levelShortName");
159 return r;
160}
161
162int RoomModel::buildingCount() const
163{
164 return (int)m_buildings.size();
165}
166
167bool RoomModel::isEmpty() const
168{
169 return rowCount() == 0;
170}
171
172void RoomModel::ensurePopulated() const
173{
174 if (m_rooms.empty() && !m_data.isEmpty()) {
175 // we assume that this is expensive but almost never will result in an empty result
176 // and if it does nevertheless, it's a sparsely populated tile where this is cheap
177 const_cast<RoomModel*>(this)->populateModel();
178 }
179}
180
181void RoomModel::populateModel()
182{
183 // find all buildings
184 const auto buildingKey = m_data.dataSet().tagKey("building");
185 const auto nameKey = m_data.dataSet().tagKey("name");
186 const auto refKey = m_data.dataSet().tagKey("ref");
187
188 for (auto it = m_data.levelMap().begin(); it != m_data.levelMap().end(); ++it) {
189 for (const auto &e : (*it).second) {
190 if (e.type() == OSM::Type::Node || !OSM::contains(m_data.boundingBox(), e.center())) {
191 continue;
192 }
193 if (e.hasTag(buildingKey) && (e.hasTag(nameKey) || e.hasTag(refKey))) {
194 Building building;
195 building.element = e;
196 // building.outerPath = e.outerPath(m_data.dataSet()); TODO needed?
197 m_buildings.push_back(std::move(building));
198 }
199 }
200 }
201
202 // find floor levels for each building
203 const auto indoorKey = m_data.dataSet().tagKey("indoor");
204 for (auto it = m_data.levelMap().begin(); it != m_data.levelMap().end(); ++it) {
205 for (const auto &e : (*it).second) {
206 if (e.type() == OSM::Type::Node || !OSM::contains(m_data.boundingBox(), e.center())) {
207 continue;
208 }
209 if (e.tagValue(indoorKey) == "level") {
210 Level level;
211 level.element = e;
212 level.level = (*it).first.numericLevel();
213
214 // find building this level belongs to
215 for (auto &building : m_buildings) {
216 // TODO this is likely not precise enough?
217 if (OSM::intersects(e.boundingBox(), building.element.boundingBox())) {
218 building.levels.push_back(level);
219 break;
220 }
221 }
222 }
223 }
224 }
225
226 // find all rooms
227 MapCSSResult filterResult;
228 OpeningHoursCache ohCache;
229 ohCache.setMapData(mapData());
230 ohCache.setTimeRange(m_beginTime, m_endTime);
231
232 for (auto it = m_data.levelMap().begin(); it != m_data.levelMap().end(); ++it) {
233 for (const auto &e : (*it).second) {
234 if (e.type() == OSM::Type::Node || !OSM::contains(m_data.boundingBox(), e.center())) {
235 continue;
236 }
237
238 MapCSSState filterState;
239 filterState.element = e;
240 filterState.openingHours = &ohCache;
241 m_style.initializeState(filterState);
242 m_style.evaluate(filterState, filterResult);
243
244 const auto &res = filterResult[{}];
245 if (auto prop = res.declaration(MapCSSProperty::Opacity); !prop || prop->doubleValue() < 1.0) {
246 continue; // hidden element
247 }
248
249 Room room;
250 room.element = e;
251 room.level = (*it).first.numericLevel(); // TODO we only need one entry, not one per level!
252
253 // find the building this room is in
254 for (auto &building :m_buildings) {
255 // TODO this is likely not precise enough?
256 if (OSM::intersects(e.boundingBox(), building.element.boundingBox())) {
257 room.buildingElement = building.element;
258 ++building.roomCount;
259
260 // find level meta-data if available
261 for (const auto &level : building.levels) {
262 if (level.level == room.level) {
263 room.levelElement = level.element;
264 break;
265 }
266 }
267
268 break;
269 }
270 }
271
272 const auto name = filterResult[{}].resolvedTagValue(m_langs, "name", filterState);
273 if (name) {
274 room.name = QString::fromUtf8(*name);
275 }
276 m_rooms.push_back(std::move(room));
277 }
278 }
279
280 // TODO we could accumulate the covered levels and show all of them?
281 // de-duplicate multi-level entries
282 // we could also just iterate over the non-level-split data, but
283 // then we need to reparse the level data here...
284 std::sort(m_rooms.begin(), m_rooms.end(), [](const auto &lhs, const auto &rhs) {
285 if (lhs.element == rhs.element) {
286 return std::abs(lhs.level) < std::abs(rhs.level);
287 }
288 return lhs.element < rhs.element;
289 });
290 m_rooms.erase(std::unique(m_rooms.begin(), m_rooms.end(), [](const auto &lhs, const auto &rhs) {
291 return lhs.element == rhs.element;
292 }), m_rooms.end());
293
294 // de-duplicate multi-level rooms that consist of multiple OSM elements (e.g. due to varying sizes per floor)
295 // TODO
296
297 // sort by building
298 std::sort(m_rooms.begin(), m_rooms.end(), [](const auto &lhs, const auto &rhs) {
299 return lhs.buildingElement < rhs.buildingElement;
300 });
301
302 // remove buildings without rooms
303 m_buildings.erase(std::remove_if(m_buildings.begin(), m_buildings.end(), [](const auto &b) { return b.roomCount == 0; }), m_buildings.end());
304
305 qCDebug(Log) << m_buildings.size() << "buildings found";
306 qCDebug(Log) << m_rooms.size() << "rooms found";
307 Q_EMIT populated();
308}
309
310int RoomModel::findRoom(const QString &name) const
311{
312 if (name.isEmpty()) {
313 return -1;
314 }
315
316 ensurePopulated();
317 for (auto it = m_rooms.begin(); it != m_rooms.end(); ++it) {
318 // TODO match room numbers, space-ignoring fuzzy match, unambiguous substring matching
319 if ((*it).name.compare(name, Qt::CaseInsensitive) == 0) {
320 return (int)std::distance(m_rooms.begin(), it);
321 }
322 }
323
324 return -1;
325}
326
327#include "moc_roommodel.cpp"
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
List all rooms of buildings in a given data set.
Definition roommodel.h:22
Q_INVOKABLE int findRoom(const QString &name) const
Tries to identify the given room name or number and returns the row index if found.
int buildingCount
Number of buildings found in the model data.
Definition roommodel.h:26
bool isEmpty
Returns true if there are no rooms in the current map data.
Definition roommodel.h:30
@ TypeNameRole
Type of the room as translated human readable text, if set.
Definition roommodel.h:51
@ NumberRole
room number, if set
Definition roommodel.h:48
@ LevelRole
numeric level for positioning rather than for display
Definition roommodel.h:49
@ ElementRole
OSM element for this room.
Definition roommodel.h:50
@ LevelShortNameRole
Name of the floor the room is on (short form, if available)
Definition roommodel.h:54
@ NameRole
room name, if set
Definition roommodel.h:46
@ LevelLongNameRole
Name of the floor the room is on (long form, if available)
Definition roommodel.h:53
@ BuildingNameRole
Name of the building the room is in.
Definition roommodel.h:52
TagKey tagKey(const char *keyName) const
Look up a tag key for the given tag name, if it exists.
Definition datatypes.cpp:38
QString i18n(const char *text, const TYPE &arg...)
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.
QStringView level(QStringView ifopt)
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
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)
QString number(double n, char format, int precision)
QChar first() const const
CaseInsensitive
QTextStream & center(QTextStream &stream)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
QVariant fromValue(T &&value)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 18 2024 12:17:55 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.