Marble

VoiceNavigationModel.cpp
1// SPDX-License-Identifier: LGPL-2.1-or-later
2//
3// SPDX-FileCopyrightText: 2012 Dennis Nienhüser <nienhueser@kde.org>
4//
5
6#include "VoiceNavigationModel.h"
7
8#include "Route.h"
9
10#include "MarbleDebug.h"
11#include "MarbleDirs.h"
12
13namespace Marble
14{
15
16class VoiceNavigationModelPrivate
17{
18public:
19 struct Announcement {
20 bool announcementDone;
21 bool turnInstructionDone;
22
23 Announcement()
24 {
25 announcementDone = false;
26 turnInstructionDone = false;
27 }
28 };
29
30 VoiceNavigationModel *m_parent = nullptr;
31
32 QString m_speaker;
33
34 bool m_speakerEnabled;
35
37
39
40 qreal m_lastDistance;
41
42 qreal m_lastDistanceTraversed;
43
44 GeoDataLineString m_lastRoutePath;
45
46 Maneuver::Direction m_lastTurnType;
47
48 GeoDataCoordinates m_lastTurnPoint;
49
50 QStringList m_queue;
51
52 QString m_announcementText;
53
54 bool m_destinationReached;
55
56 bool m_deviated;
57
58 QList<Announcement> m_announcementList;
59
60 explicit VoiceNavigationModelPrivate(VoiceNavigationModel *parent);
61
62 void reset();
63
64 QString audioFile(const QString &name) const;
65
66 QString distanceAudioFile(qreal dest) const;
67
68 QString turnTypeAudioFile(Maneuver::Direction turnType, qreal distance);
69
70 QString announcementText(Maneuver::Direction turnType, qreal distance);
71
72 void updateInstruction(const RouteSegment &segment, qreal distance, Maneuver::Direction turnType);
73
74 void updateInstruction(const QString &name);
75
76 void initializeMaps();
77};
78
79VoiceNavigationModelPrivate::VoiceNavigationModelPrivate(VoiceNavigationModel *parent)
80 : m_parent(parent)
81 , m_speakerEnabled(true)
82 , m_lastDistance(0.0)
83 , m_lastDistanceTraversed(0.0)
84 , m_lastTurnType(Maneuver::Unknown)
85 , m_destinationReached(false)
86 , m_deviated(false)
87{
88 initializeMaps();
89}
90
91void VoiceNavigationModelPrivate::reset()
92{
93 m_lastDistance = 0.0;
94 m_lastDistanceTraversed = 0.0;
95}
96
97QString VoiceNavigationModelPrivate::audioFile(const QString &name) const
98{
99#ifdef Q_OS_ANDROID
100 return name;
101#else
102 QStringList const formats = QStringList() << QStringLiteral("ogg") << QStringLiteral("mp3") << QStringLiteral("wav");
103 if (m_speakerEnabled) {
104 QString const audioTemplate = QStringLiteral("%1/%2.%3");
105 for (const QString &format : formats) {
106 QString const result = audioTemplate.arg(m_speaker, name, format);
107 QFileInfo audioFile(result);
108 if (audioFile.exists()) {
109 return result;
110 }
111 }
112 }
113
114 QString const audioTemplate = QStringLiteral("audio/%1.%2");
115 for (const QString &format : formats) {
116 QString const result = MarbleDirs::path(audioTemplate.arg(name, format));
117 if (!result.isEmpty()) {
118 return result;
119 }
120 }
121
122 return {};
123#endif
124}
125
126QString VoiceNavigationModelPrivate::distanceAudioFile(qreal dest) const
127{
128 if (dest > 0.0 && dest < 900.0) {
129 qreal minDistance = 0.0;
130 int targetDistance = 0;
131 QList<int> distances;
132 distances << 50 << 80 << 100 << 200 << 300 << 400 << 500 << 600 << 700 << 800;
133 for (int distance : std::as_const(distances)) {
134 QString file = audioFile(QString::number(distance));
135 qreal currentDistance = qAbs(distance - dest);
136 if (!file.isEmpty() && (minDistance == 0.0 || currentDistance < minDistance)) {
137 minDistance = currentDistance;
138 targetDistance = distance;
139 }
140 }
141
142 if (targetDistance > 0) {
143 return audioFile(QString::number(targetDistance));
144 }
145 }
146
147 return {};
148}
149
150QString VoiceNavigationModelPrivate::turnTypeAudioFile(Maneuver::Direction turnType, qreal distance)
151{
152 bool const announce = distance >= 75;
153 QMap<Maneuver::Direction, QString> const &map = announce ? m_announceMap : m_turnTypeMap;
154 if (m_speakerEnabled && map.contains(turnType)) {
155 return audioFile(map[turnType]);
156 }
157
158 return audioFile(announce ? QStringLiteral("ListEnd") : QStringLiteral("AppPositive"));
159}
160
161QString VoiceNavigationModelPrivate::announcementText(Maneuver::Direction turnType, qreal distance)
162{
163 QString announcementText;
164 if (distance >= 75) {
165 announcementText = QString(QStringLiteral("In ") + distanceAudioFile(distance) + QStringLiteral(" meters, "));
166 }
167 switch (turnType) {
168 case Maneuver::Continue:
169 case Maneuver::Straight:
170 announcementText += QStringLiteral("Continue straight");
171 break;
172 case Maneuver::SlightRight:
173 announcementText += QStringLiteral("Turn slight right");
174 break;
175 case Maneuver::SlightLeft:
176 announcementText += QStringLiteral("Turn slight left");
177 break;
178 case Maneuver::Right:
179 case Maneuver::SharpRight:
180 announcementText += QStringLiteral("Turn right");
181 break;
182 case Maneuver::Left:
183 case Maneuver::SharpLeft:
184 announcementText += QStringLiteral("Turn left");
185 break;
186 case Maneuver::TurnAround:
187 announcementText += QStringLiteral("Take a U-turn");
188 break;
189 case Maneuver::ExitLeft:
190 announcementText += QStringLiteral("Exit left");
191 break;
192 case Maneuver::ExitRight:
193 announcementText += QStringLiteral("Exit right");
194 break;
195 case Maneuver::RoundaboutFirstExit:
196 announcementText += QStringLiteral("Take the first exit");
197 break;
198 case Maneuver::RoundaboutSecondExit:
199 announcementText += QStringLiteral("Take the second exit");
200 break;
201 case Maneuver::RoundaboutThirdExit:
202 announcementText += QStringLiteral("Take the third exit");
203 break;
204 default:
205 announcementText = QStringLiteral("");
206 break;
207 }
208 return announcementText;
209}
210
211void VoiceNavigationModelPrivate::updateInstruction(const RouteSegment &segment, qreal distance, Maneuver::Direction turnType)
212{
213 QString turnTypeAudio = turnTypeAudioFile(turnType, distance);
214 if (turnTypeAudio.isEmpty()) {
215 mDebug() << "Missing audio file for turn type " << turnType << " and speaker " << m_speaker;
216 return;
217 }
218
219 m_queue.clear();
220 m_queue << turnTypeAudio;
221 m_announcementText = announcementText(turnType, distance);
222 qreal nextSegmentDistance = segment.nextRouteSegment().distance();
223 Maneuver::Direction nextSegmentDirection = segment.nextRouteSegment().nextRouteSegment().maneuver().direction();
224 if (!m_announcementText.isEmpty() && distance < 75 && nextSegmentDistance != 0 && nextSegmentDistance < 75) {
225 QString nextSegmentAnnouncementText = announcementText(nextSegmentDirection, nextSegmentDistance);
226 if (!nextSegmentAnnouncementText.isEmpty()) {
227 m_announcementText += QLatin1StringView(", then ") + nextSegmentAnnouncementText;
228 }
229 }
230 Q_EMIT m_parent->instructionChanged();
231}
232
233void VoiceNavigationModelPrivate::updateInstruction(const QString &name)
234{
235 m_queue.clear();
236 m_queue << audioFile(name);
237 m_announcementText = name;
238 Q_EMIT m_parent->instructionChanged();
239}
240
241void VoiceNavigationModelPrivate::initializeMaps()
242{
243 m_turnTypeMap.clear();
244 m_announceMap.clear();
245
246 m_announceMap[Maneuver::Continue] = QStringLiteral("Straight");
247 // none of our voice navigation commands fits, so leave out
248 // Maneuver::Merge here to have a sound play instead
249 m_announceMap[Maneuver::Straight] = QStringLiteral("Straight");
250 m_announceMap[Maneuver::SlightRight] = QStringLiteral("AhKeepRight");
251 m_announceMap[Maneuver::Right] = QStringLiteral("AhRightTurn");
252 m_announceMap[Maneuver::SharpRight] = QStringLiteral("AhRightTurn");
253 m_announceMap[Maneuver::TurnAround] = QStringLiteral("AhUTurn");
254 m_announceMap[Maneuver::SharpLeft] = QStringLiteral("AhLeftTurn");
255 m_announceMap[Maneuver::Left] = QStringLiteral("AhLeftTurn");
256 m_announceMap[Maneuver::SlightLeft] = QStringLiteral("AhKeepLeft");
257 m_announceMap[Maneuver::RoundaboutFirstExit] = QStringLiteral("RbExit1");
258 m_announceMap[Maneuver::RoundaboutSecondExit] = QStringLiteral("RbExit2");
259 m_announceMap[Maneuver::RoundaboutThirdExit] = QStringLiteral("RbExit3");
260 m_announceMap[Maneuver::ExitLeft] = QStringLiteral("AhExitLeft");
261 m_announceMap[Maneuver::ExitRight] = QStringLiteral("AhExitRight");
262
263 m_turnTypeMap[Maneuver::Continue] = QStringLiteral("Straight");
264 // none of our voice navigation commands fits, so leave out
265 // Maneuver::Merge here to have a sound play instead
266 m_turnTypeMap[Maneuver::Straight] = QStringLiteral("Straight");
267 m_turnTypeMap[Maneuver::SlightRight] = QStringLiteral("BearRight");
268 m_turnTypeMap[Maneuver::Right] = QStringLiteral("TurnRight");
269 m_turnTypeMap[Maneuver::SharpRight] = QStringLiteral("SharpRight");
270 m_turnTypeMap[Maneuver::TurnAround] = QStringLiteral("UTurn");
271 m_turnTypeMap[Maneuver::SharpLeft] = QStringLiteral("SharpLeft");
272 m_turnTypeMap[Maneuver::Left] = QStringLiteral("TurnLeft");
273 m_turnTypeMap[Maneuver::SlightLeft] = QStringLiteral("BearLeft");
274 m_turnTypeMap[Maneuver::RoundaboutFirstExit] = QString();
275 m_turnTypeMap[Maneuver::RoundaboutSecondExit] = QString();
276 m_turnTypeMap[Maneuver::RoundaboutThirdExit] = QString();
277 m_turnTypeMap[Maneuver::ExitLeft] = QStringLiteral("TurnLeft");
278 m_turnTypeMap[Maneuver::ExitRight] = QStringLiteral("TurnRight");
279}
280
281VoiceNavigationModel::VoiceNavigationModel(QObject *parent)
282 : QObject(parent)
283 , d(new VoiceNavigationModelPrivate(this))
284{
285 // nothing to do
286}
287
288VoiceNavigationModel::~VoiceNavigationModel()
289{
290 delete d;
291}
292
293QString VoiceNavigationModel::speaker() const
294{
295 return d->m_speaker;
296}
297
298void VoiceNavigationModel::setSpeaker(const QString &speaker)
299{
300 if (speaker != d->m_speaker) {
301 QFileInfo speakerDir = QFileInfo(speaker);
302 if (!speakerDir.exists()) {
303 d->m_speaker = MarbleDirs::path(QLatin1StringView("/audio/speakers/") + speaker);
304 } else {
305 d->m_speaker = speaker;
306 }
307
308 Q_EMIT speakerChanged();
309 Q_EMIT previewChanged();
310 }
311}
312
313bool VoiceNavigationModel::isSpeakerEnabled() const
314{
315 return d->m_speakerEnabled;
316}
317
318void VoiceNavigationModel::setSpeakerEnabled(bool enabled)
319{
320 if (enabled != d->m_speakerEnabled) {
321 d->m_speakerEnabled = enabled;
322 Q_EMIT isSpeakerEnabledChanged();
323 Q_EMIT previewChanged();
324 }
325}
326
327void VoiceNavigationModel::reset()
328{
329 d->reset();
330}
331
332void VoiceNavigationModel::update(const Route &route, qreal distanceManuever, qreal distanceTarget, bool deviated)
333{
334 if (d->m_lastRoutePath != route.path()) {
335 d->m_announcementList.clear();
336 d->m_announcementList.resize(route.size());
337 d->m_lastRoutePath = route.path();
338 }
339
340 if (d->m_destinationReached && distanceTarget < 250) {
341 return;
342 }
343
344 if (!d->m_destinationReached && distanceTarget < 50) {
345 d->m_destinationReached = true;
346 d->updateInstruction(d->m_speakerEnabled ? QStringLiteral("You have arrived at your destination") : QStringLiteral("AppPositive"));
347 return;
348 }
349
350 if (distanceTarget > 150) {
351 d->m_destinationReached = false;
352 }
353
354 if (deviated && !d->m_deviated) {
355 d->updateInstruction(d->m_speakerEnabled ? QStringLiteral("Deviated from the route") : QStringLiteral("ListEnd"));
356 }
357 d->m_deviated = deviated;
358 if (deviated) {
359 return;
360 }
361
362 Maneuver::Direction turnType = route.currentSegment().nextRouteSegment().maneuver().direction();
363 if (!(d->m_lastTurnPoint == route.currentSegment().nextRouteSegment().maneuver().position()) || turnType != d->m_lastTurnType) {
364 d->m_lastTurnPoint = route.currentSegment().nextRouteSegment().maneuver().position();
365 d->reset();
366 }
367
368 int index = route.indexOf(route.currentSegment());
369
370 qreal const distanceTraversed = route.currentSegment().distance() - distanceManuever;
371 bool const minDistanceTraversed = d->m_lastDistanceTraversed < 40 && distanceTraversed >= 40;
372 bool const announcementAfterTurn = minDistanceTraversed && distanceManuever >= 75;
373 bool const announcement = (d->m_lastDistance > 850 || announcementAfterTurn) && distanceManuever <= 850;
374 bool const turn = (d->m_lastDistance == 0 || d->m_lastDistance > 75) && distanceManuever <= 75;
375
376 bool const announcementDone = d->m_announcementList[index].announcementDone;
377 bool const turnInstructionDone = d->m_announcementList[index].turnInstructionDone;
378
379 if ((announcement && !announcementDone) || (turn && !turnInstructionDone)) {
380 d->updateInstruction(route.currentSegment(), distanceManuever, turnType);
381 VoiceNavigationModelPrivate::Announcement &curAnnouncement = d->m_announcementList[index];
382 if (announcement) {
383 curAnnouncement.announcementDone = true;
384 }
385 if (turn) {
386 curAnnouncement.turnInstructionDone = true;
387 }
388 }
389
390 d->m_lastTurnType = turnType;
391 d->m_lastDistance = distanceManuever;
392 d->m_lastDistanceTraversed = distanceTraversed;
393}
394
395QString VoiceNavigationModel::preview() const
396{
397 return d->audioFile(d->m_speakerEnabled ? QStringLiteral("The Marble team wishes you a pleasant and safe journey!") : QStringLiteral("AppPositive"));
398}
399
400QString VoiceNavigationModel::instruction() const
401{
402 return d->m_announcementText;
403}
404
405}
406
407#include "moc_VoiceNavigationModel.cpp"
QString name(GameStandardAction id)
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)
bool exists(const QString &path)
QString arg(Args &&... args) const const
void clear()
bool isEmpty() const const
QString number(double n, char format, int precision)
QFuture< void > map(Iterator begin, Iterator end, MapFunctor &&function)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Nov 8 2024 12:02:44 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.