Marble

AlternativeRoutesModel.cpp
1// SPDX-License-Identifier: LGPL-2.1-or-later
2//
3// SPDX-FileCopyrightText: 2010 Dennis Nienhüser <nienhueser@kde.org>
4//
5
6#include "AlternativeRoutesModel.h"
7
8#include "GeoDataDocument.h"
9#include "GeoDataExtendedData.h"
10#include "GeoDataFolder.h"
11#include "GeoDataLatLonAltBox.h"
12#include "GeoDataLineString.h"
13#include "GeoDataPlacemark.h"
14
15#include <QElapsedTimer>
16#include <QPainter>
17#include <QTimer>
18
19namespace Marble
20{
21
22class Q_DECL_HIDDEN AlternativeRoutesModel::Private
23{
24public:
25 Private();
26
27 /**
28 * Returns true if there exists a route with high similarity to the given one
29 */
30 bool filter(const GeoDataDocument *document) const;
31
32 /**
33 * Returns a similarity measure in the range of [0..1]. Two routes with a similarity of 0 can
34 * be treated as totally different (e.g. different route requests), two routes with a similarity
35 * of 1 are considered equal. Otherwise the routes overlap to an extent indicated by the
36 * similarity value -- the higher, the more they do overlap.
37 * @note: The direction of routes is important; reversed routes are not considered equal
38 */
39 static qreal similarity(const GeoDataDocument *routeA, const GeoDataDocument *routeB);
40
41 /**
42 * Returns the distance between the given polygon and the given point
43 */
44 static qreal distance(const GeoDataLineString &wayPoints, const GeoDataCoordinates &position);
45
46 /**
47 * Returns the bearing of the great circle path defined by the coordinates one and two
48 * Based on https://www.movable-type.co.uk/scripts/latlong.html
49 */
50 static qreal bearing(const GeoDataCoordinates &one, const GeoDataCoordinates &two);
51
52 /**
53 * Returns the distance between the given point and the line segment (not line) defined
54 * by the two coordinates lineA and lineB
55 */
56 static qreal distance(const GeoDataCoordinates &satellite, const GeoDataCoordinates &lineA, const GeoDataCoordinates &lineB);
57
58 /**
59 * Returns the point reached when traveling the given distance from start with the given direction
60 */
61 static GeoDataCoordinates coordinates(const GeoDataCoordinates &start, qreal distance, qreal bearing);
62
63 /**
64 * Returns the similarity between routeA and routeB. This method is not symmetric, i.e. in
65 * general unidirectionalSimilarity(a,b) != unidirectionalSimilarity(b,a)
66 */
67 static qreal unidirectionalSimilarity(const GeoDataDocument *routeA, const GeoDataDocument *routeB);
68
69 /**
70 * (Primitive) scoring for routes
71 */
72 static bool higherScore(const GeoDataDocument *one, const GeoDataDocument *two);
73
74 /**
75 * Returns true if the given route contains instructions (placemarks with turn instructions)
76 */
77 static qreal instructionScore(const GeoDataDocument *document);
78
79 static const GeoDataLineString *waypoints(const GeoDataDocument *document);
80
81 static int nonZero(const QImage &image);
82
83 static QPolygonF polygon(const GeoDataLineString &lineString, qreal x, qreal y, qreal sx, qreal sy);
84
85 /** The currently shown alternative routes (model data) */
87
88 /** Pending route data (waiting for other results to come in) */
89 QList<GeoDataDocument *> m_restrainedRoutes;
90
91 /** Counts the time between route request and first result */
92 QElapsedTimer m_responseTime;
93
94 int m_currentIndex;
95};
96
97AlternativeRoutesModel::Private::Private()
98 : m_currentIndex(-1)
99{
100 // nothing to do
101}
102
103int AlternativeRoutesModel::Private::nonZero(const QImage &image)
104{
105 QRgb const black = qRgb(0, 0, 0);
106 int count = 0;
107 for (int y = 0; y < image.height(); ++y) {
108 QRgb *destLine = (QRgb *)image.scanLine(y);
109 for (int x = 0; x < image.width(); ++x) {
110 count += destLine[x] == black ? 0 : 1;
111 }
112 }
113 return count;
114}
115
116QPolygonF AlternativeRoutesModel::Private::polygon(const GeoDataLineString &lineString, qreal x, qreal y, qreal sx, qreal sy)
117{
118 QPolygonF poly;
119 for (int i = 0; i < lineString.size(); ++i) {
120 poly << QPointF(qAbs((lineString)[i].longitude() - x) * sx, qAbs((lineString)[i].latitude() - y) * sy);
121 }
122 return poly;
123}
124
125bool AlternativeRoutesModel::Private::filter(const GeoDataDocument *document) const
126{
127 for (int i = 0; i < m_routes.size(); ++i) {
128 qreal similarity = Private::similarity(document, m_routes.at(i));
129 if (similarity > 0.8) {
130 return true;
131 }
132 }
133
134 return false;
135}
136
137qreal AlternativeRoutesModel::Private::similarity(const GeoDataDocument *routeA, const GeoDataDocument *routeB)
138{
139 return qMax<qreal>(unidirectionalSimilarity(routeA, routeB), unidirectionalSimilarity(routeB, routeA));
140}
141
142qreal AlternativeRoutesModel::Private::distance(const GeoDataLineString &wayPoints, const GeoDataCoordinates &position)
143{
144 Q_ASSERT(!wayPoints.isEmpty());
145 qreal minDistance = 0;
146 for (int i = 1; i < wayPoints.size(); ++i) {
147 qreal dist = distance(position, wayPoints.at(i - 1), wayPoints.at(i));
148 if (minDistance <= 0 || dist < minDistance) {
149 minDistance = dist;
150 }
151 }
152
153 return minDistance;
154}
155
156qreal AlternativeRoutesModel::Private::bearing(const GeoDataCoordinates &one, const GeoDataCoordinates &two)
157{
158 qreal delta = two.longitude() - one.longitude();
159 qreal lat1 = one.latitude();
160 qreal lat2 = two.latitude();
161 return fmod(atan2(sin(delta) * cos(lat2), cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(delta)), 2 * M_PI);
162}
163
164GeoDataCoordinates AlternativeRoutesModel::Private::coordinates(const GeoDataCoordinates &start, qreal distance, qreal bearing)
165{
166 qreal lat1 = start.latitude();
167 qreal lon1 = start.longitude();
168 qreal lat2 = asin(sin(lat1) * cos(distance) + cos(lat1) * sin(distance) * cos(bearing));
169 qreal lon2 = lon1 + atan2(sin(bearing) * sin(distance) * cos(lat1), cos(distance) - sin(lat1) * sin(lat2));
170 return {lon2, lat2};
171}
172
173qreal AlternativeRoutesModel::Private::distance(const GeoDataCoordinates &satellite, const GeoDataCoordinates &lineA, const GeoDataCoordinates &lineB)
174{
175 const qreal dist = lineA.sphericalDistanceTo(satellite);
176 qreal bearA = bearing(lineA, satellite);
177 qreal bearB = bearing(lineA, lineB);
178 qreal result = asin(sin(dist) * sin(bearB - bearA));
179 Q_ASSERT(qMax<qreal>(satellite.sphericalDistanceTo(lineA), satellite.sphericalDistanceTo(lineB)) >= qAbs<qreal>(result));
180
181 result = acos(cos(dist) / cos(result));
182 /** @todo: This is a naive approach. Look into the maths. */
183 const qreal final = qMin<qreal>(satellite.sphericalDistanceTo(lineA), satellite.sphericalDistanceTo(lineB));
184 if (result >= 0 && result <= lineA.sphericalDistanceTo(lineB)) {
185 GeoDataCoordinates nearest = coordinates(lineA, result, bearB);
186 return qMin<qreal>(final, satellite.sphericalDistanceTo(nearest));
187 } else {
188 return final;
189 }
190}
191
192qreal AlternativeRoutesModel::Private::unidirectionalSimilarity(const GeoDataDocument *routeA, const GeoDataDocument *routeB)
193{
194 const GeoDataLineString *waypointsA = waypoints(routeA);
195 const GeoDataLineString *waypointsB = waypoints(routeB);
196 if (!waypointsA || !waypointsB) {
197 return 0.0;
198 }
199
201 image.fill(qRgb(0, 0, 0));
202 GeoDataLatLonBox box = GeoDataLatLonBox::fromLineString(*waypointsA);
203 box = box.united(GeoDataLatLonBox::fromLineString(*waypointsB));
204 if (!box.width() || !box.height()) {
205 return 0.0;
206 }
207
208 qreal const sw = image.width() / box.width();
209 qreal const sh = image.height() / box.height();
210
211 QPainter painter(&image);
212 painter.setPen(QColor(Qt::white));
213
214 painter.drawPoints(Private::polygon(*waypointsA, box.west(), box.north(), sw, sh));
215 int const countA = Private::nonZero(image);
216
217 painter.drawPoints(Private::polygon(*waypointsB, box.west(), box.north(), sw, sh));
218 int const countB = Private::nonZero(image);
219 Q_ASSERT(countA <= countB);
220 return countB ? 1.0 - qreal(countB - countA) / countB : 0;
221}
222
223bool AlternativeRoutesModel::Private::higherScore(const GeoDataDocument *one, const GeoDataDocument *two)
224{
225 qreal instructionScoreA = instructionScore(one);
226 qreal instructionScoreB = instructionScore(two);
227 if (instructionScoreA != instructionScoreB) {
228 return instructionScoreA > instructionScoreB;
229 }
230
231 qreal lengthA = waypoints(one)->length(EARTH_RADIUS);
232 qreal lengthB = waypoints(two)->length(EARTH_RADIUS);
233
234 return lengthA < lengthB;
235}
236
237qreal AlternativeRoutesModel::Private::instructionScore(const GeoDataDocument *document)
238{
239 bool hasInstructions = false;
240
241 QStringList blacklist = QStringList() << QString() << QStringLiteral("Route") << QStringLiteral("Tessellated");
242 QList<GeoDataFolder *> folders = document->folderList();
243 for (const GeoDataFolder *folder : std::as_const(folders)) {
244 for (const GeoDataPlacemark *placemark : folder->placemarkList()) {
245 if (!blacklist.contains(placemark->name())) {
246 hasInstructions = true;
247 break;
248 }
249 }
250 }
251
252 for (const GeoDataPlacemark *placemark : document->placemarkList()) {
253 if (!blacklist.contains(placemark->name())) {
254 hasInstructions = true;
255
256 if (placemark->extendedData().contains(QStringLiteral("turnType"))) {
257 return 1.0;
258 }
259 }
260 }
261
262 return hasInstructions ? 0.5 : 0.0;
263}
264
265const GeoDataLineString *AlternativeRoutesModel::Private::waypoints(const GeoDataDocument *document)
266{
267 QList<GeoDataFolder *> folders = document->folderList();
268 for (const GeoDataFolder *folder : std::as_const(folders)) {
269 for (const GeoDataPlacemark *placemark : folder->placemarkList()) {
270 const GeoDataGeometry *geometry = placemark->geometry();
271 const auto lineString = dynamic_cast<const GeoDataLineString *>(geometry);
272 if (lineString) {
273 return lineString;
274 }
275 }
276 }
277
278 for (const GeoDataPlacemark *placemark : document->placemarkList()) {
279 const GeoDataGeometry *geometry = placemark->geometry();
280 const auto lineString = dynamic_cast<const GeoDataLineString *>(geometry);
281 if (lineString) {
282 return lineString;
283 }
284 }
285
286 return nullptr;
287}
288
289AlternativeRoutesModel::AlternativeRoutesModel(QObject *parent)
290 : QAbstractListModel(parent)
291 , d(new Private())
292{
293 // nothing to do
294}
295
296AlternativeRoutesModel::~AlternativeRoutesModel()
297{
298 clear();
299 delete d;
300}
301
302int AlternativeRoutesModel::rowCount(const QModelIndex &) const
303{
304 return d->m_routes.size();
305}
306
307QVariant AlternativeRoutesModel::headerData(int, Qt::Orientation, int) const
308{
309 return {};
310}
311
312QHash<int, QByteArray> AlternativeRoutesModel::roleNames() const
313{
314 return {
315 {Qt::DisplayRole, "routeName"},
316 };
317}
318
319QVariant AlternativeRoutesModel::data(const QModelIndex &index, int role) const
320{
321 if (role == Qt::DisplayRole && index.column() == 0 && index.row() >= 0 && index.row() < d->m_routes.size()) {
322 return d->m_routes.at(index.row())->name();
323 }
324
325 return {};
326}
327
328const GeoDataDocument *AlternativeRoutesModel::route(int index) const
329{
330 if (index >= 0 && index < d->m_routes.size()) {
331 return d->m_routes.at(index);
332 }
333
334 return nullptr;
335}
336
337void AlternativeRoutesModel::newRequest(RouteRequest *)
338{
339 d->m_responseTime.start();
340 d->m_currentIndex = -1;
341 clear();
342}
343
344void AlternativeRoutesModel::addRestrainedRoutes()
345{
346 Q_ASSERT(d->m_routes.isEmpty());
347 std::sort(d->m_restrainedRoutes.begin(), d->m_restrainedRoutes.end(), Private::higherScore);
348
349 for (GeoDataDocument *route : std::as_const(d->m_restrainedRoutes)) {
350 if (!d->filter(route)) {
351 int affected = d->m_routes.size();
352 beginInsertRows(QModelIndex(), affected, affected);
353 // GeoDataDocument* base = d->m_routes.isEmpty() ? 0 : d->m_routes.first();
354 d->m_routes.push_back(route);
355 endInsertRows();
356 }
357 }
358
359 d->m_restrainedRoutes.clear();
360 Q_ASSERT(!d->m_routes.isEmpty());
361 setCurrentRoute(0);
362}
363
364void AlternativeRoutesModel::addRoute(GeoDataDocument *document, WritePolicy policy)
365{
366 if (policy != Instant) {
367 if (d->m_routes.isEmpty()) {
368 d->m_restrainedRoutes.push_back(document);
369
370 if (d->m_restrainedRoutes.isEmpty()) {
371 // First
372 const int responseTime = d->m_responseTime.elapsed();
373 const int timeout = qMin<int>(500, qMax<int>(50, responseTime * 2));
374 QTimer::singleShot(timeout, this, SLOT(addRestrainedRoutes()));
375
376 return;
377 }
378 }
379
380 for (int i = 0; i < d->m_routes.size(); ++i) {
381 qreal similarity = Private::similarity(document, d->m_routes.at(i));
382 if (similarity > 0.8) {
383 if (Private::higherScore(document, d->m_routes.at(i))) {
384 d->m_routes[i] = document;
385 QModelIndex changed = index(i);
386 Q_EMIT dataChanged(changed, changed);
387 }
388
389 return;
390 }
391 }
392 }
393
394 const int affected = d->m_routes.size();
395 beginInsertRows(QModelIndex(), affected, affected);
396 d->m_routes.push_back(document);
397 endInsertRows();
398}
399
400const GeoDataLineString *AlternativeRoutesModel::waypoints(const GeoDataDocument *document)
401{
402 return Private::waypoints(document);
403}
404
405void AlternativeRoutesModel::setCurrentRoute(int index)
406{
407 if (index >= 0 && index < rowCount() && d->m_currentIndex != index) {
408 d->m_currentIndex = index;
409 Q_EMIT currentRouteChanged(currentRoute());
410 Q_EMIT currentRouteChanged(d->m_currentIndex);
411 }
412}
413
414const GeoDataDocument *AlternativeRoutesModel::currentRoute() const
415{
416 const GeoDataDocument *result = nullptr;
417 if (d->m_currentIndex >= 0 && d->m_currentIndex < rowCount()) {
418 result = d->m_routes[d->m_currentIndex];
419 }
420
421 return result;
422}
423
424void AlternativeRoutesModel::clear()
425{
426 beginResetModel();
427 QList<GeoDataDocument *> routes = d->m_routes;
428 d->m_currentIndex = -1;
429 d->m_routes.clear();
430 qDeleteAll(routes);
431 endResetModel();
432}
433
434} // namespace Marble
435
436#include "moc_AlternativeRoutesModel.cpp"
Q_SCRIPTABLE Q_NOREPLY void start()
Binds a QML item to a specific geodetic location in screen coordinates.
KOSM_EXPORT double distance(const std::vector< const OSM::Node * > &path, Coordinate coord)
Format_ARGB32_Premultiplied
void fill(Qt::GlobalColor color)
int height() const const
uchar * scanLine(int i)
int width() const const
void clear()
int column() const const
int row() const const
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
DisplayRole
Orientation
QFuture< void > filter(QThreadPool *pool, Sequence &sequence, KeepFunctor &&filterFunction)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sat Dec 21 2024 17:04:14 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.