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 "MarbleDirs.h"
11 #include "MarbleDebug.h"
12 
13 namespace Marble
14 {
15 
16 class VoiceNavigationModelPrivate
17 {
18 public:
19 
20     struct Announcement
21     {
22         bool announcementDone;
23         bool turnInstructionDone;
24 
AnnouncementMarble::VoiceNavigationModelPrivate::Announcement25         Announcement(){
26             announcementDone = false;
27             turnInstructionDone = false;
28         }
29     };
30 
31     VoiceNavigationModel* m_parent;
32 
33     QString m_speaker;
34 
35     bool m_speakerEnabled;
36 
37     QMap<Maneuver::Direction, QString> m_turnTypeMap;
38 
39     QMap<Maneuver::Direction, QString> m_announceMap;
40 
41     qreal m_lastDistance;
42 
43     qreal m_lastDistanceTraversed;
44 
45     GeoDataLineString m_lastRoutePath;
46 
47     Maneuver::Direction m_lastTurnType;
48 
49     GeoDataCoordinates m_lastTurnPoint;
50 
51     QStringList m_queue;
52 
53     QString m_announcementText;
54 
55     bool m_destinationReached;
56 
57     bool m_deviated;
58 
59     QVector<Announcement> m_announcementList;
60 
61     explicit VoiceNavigationModelPrivate( VoiceNavigationModel* parent );
62 
63     void reset();
64 
65     QString audioFile(const QString &name) const;
66 
67     QString distanceAudioFile( qreal dest ) const;
68 
69     QString turnTypeAudioFile( Maneuver::Direction turnType, qreal distance );
70 
71     QString announcementText( Maneuver::Direction turnType, qreal distance );
72 
73     void updateInstruction(const RouteSegment &segment, qreal distance, Maneuver::Direction turnType );
74 
75     void updateInstruction( const QString &name );
76 
77     void initializeMaps();
78 };
79 
VoiceNavigationModelPrivate(VoiceNavigationModel * parent)80 VoiceNavigationModelPrivate::VoiceNavigationModelPrivate( VoiceNavigationModel* parent ) :
81     m_parent( parent ),
82     m_speakerEnabled( true ),
83     m_lastDistance( 0.0 ),
84     m_lastDistanceTraversed( 0.0 ),
85     m_lastTurnType( Maneuver::Unknown ),
86     m_destinationReached( false ),
87     m_deviated( false )
88 {
89     initializeMaps();
90 }
91 
reset()92 void VoiceNavigationModelPrivate::reset()
93 {
94     m_lastDistance = 0.0;
95     m_lastDistanceTraversed = 0.0;
96 }
97 
audioFile(const QString & name) const98 QString VoiceNavigationModelPrivate::audioFile( const QString &name ) const
99 {
100 #ifdef Q_OS_ANDROID
101     return name;
102 #else
103     QStringList const formats = QStringList() << "ogg" << "mp3" << "wav";
104     if ( m_speakerEnabled ) {
105         QString const audioTemplate = "%1/%2.%3";
106         for( const QString &format: formats ) {
107             QString const result = audioTemplate.arg( m_speaker, name, format );
108             QFileInfo audioFile( result );
109             if ( audioFile.exists() ) {
110                 return result;
111             }
112         }
113     }
114 
115     QString const audioTemplate = "audio/%1.%2";
116     for( const QString &format: formats ) {
117         QString const result = MarbleDirs::path( audioTemplate.arg( name, format ) );
118         if ( !result.isEmpty() ) {
119             return result;
120         }
121     }
122 
123     return QString();
124 #endif
125 }
126 
distanceAudioFile(qreal dest) const127 QString VoiceNavigationModelPrivate::distanceAudioFile( qreal dest ) const
128 {
129     if ( dest > 0.0 && dest < 900.0 ) {
130         qreal minDistance = 0.0;
131         int targetDistance = 0;
132         QVector<int> distances;
133         distances << 50 << 80 << 100 << 200 << 300 << 400 << 500 << 600 << 700 << 800;
134         for( int distance: distances ) {
135             QString file = audioFile( QString::number( distance ) );
136             qreal currentDistance = qAbs( distance - dest );
137             if ( !file.isEmpty() && ( minDistance == 0.0 || currentDistance < minDistance ) ) {
138                 minDistance = currentDistance;
139                 targetDistance = distance;
140             }
141         }
142 
143         if ( targetDistance > 0 ) {
144             return audioFile( QString::number( targetDistance ) );
145         }
146     }
147 
148     return QString();
149 }
150 
turnTypeAudioFile(Maneuver::Direction turnType,qreal distance)151 QString VoiceNavigationModelPrivate::turnTypeAudioFile( Maneuver::Direction turnType, qreal distance )
152 {
153     bool const announce = distance >= 75;
154     QMap<Maneuver::Direction, QString> const & map = announce ? m_announceMap : m_turnTypeMap;
155     if ( m_speakerEnabled && map.contains( turnType ) ) {
156         return audioFile( map[turnType] );
157     }
158 
159     return audioFile( announce ? "ListEnd" : "AppPositive" );
160 }
161 
announcementText(Maneuver::Direction turnType,qreal distance)162 QString VoiceNavigationModelPrivate::announcementText( Maneuver::Direction turnType, qreal distance )
163 {
164     QString announcementText = QString("");
165     if (distance >= 75) {
166         announcementText = QString("In "+distanceAudioFile(distance)+" meters, ");
167     }
168     switch (turnType) {
169     case Maneuver::Continue:
170     case Maneuver::Straight:
171         announcementText += QString("Continue straight");
172         break;
173     case Maneuver::SlightRight:
174         announcementText += QString("Turn slight right");
175         break;
176     case Maneuver::SlightLeft:
177         announcementText += QString("Turn slight left");
178         break;
179     case Maneuver::Right:
180     case Maneuver::SharpRight:
181         announcementText += QString("Turn right");
182         break;
183     case Maneuver::Left:
184     case Maneuver::SharpLeft:
185         announcementText += QString("Turn left");
186         break;
187     case Maneuver::TurnAround:
188         announcementText += QString("Take a U-turn");
189         break;
190     case Maneuver::ExitLeft:
191         announcementText += QString("Exit left");
192         break;
193     case Maneuver::ExitRight:
194         announcementText += QString("Exit right");
195         break;
196     case Maneuver::RoundaboutFirstExit:
197         announcementText += QString("Take the first exit");
198         break;
199     case Maneuver::RoundaboutSecondExit:
200         announcementText += QString("Take the second exit");
201         break;
202     case Maneuver::RoundaboutThirdExit:
203         announcementText += QString("Take the third exit");
204         break;
205     default:
206         announcementText = QString("");
207         break;
208     }
209     return announcementText;
210 }
211 
updateInstruction(const RouteSegment & segment,qreal distance,Maneuver::Direction turnType)212 void VoiceNavigationModelPrivate::updateInstruction( const RouteSegment & segment, qreal distance, Maneuver::Direction turnType )
213 {
214     QString turnTypeAudio = turnTypeAudioFile( turnType, distance );
215     if ( turnTypeAudio.isEmpty() ) {
216         mDebug() << "Missing audio file for turn type " << turnType << " and speaker " << m_speaker;
217         return;
218     }
219 
220     m_queue.clear();
221     m_queue << turnTypeAudio;
222     m_announcementText = announcementText(turnType, distance);
223     qreal nextSegmentDistance = segment.nextRouteSegment().distance();
224     Maneuver::Direction nextSegmentDirection = segment.nextRouteSegment().nextRouteSegment().maneuver().direction();
225     if (!m_announcementText.isEmpty() && distance < 75 && nextSegmentDistance != 0 && nextSegmentDistance < 75) {
226         QString nextSegmentAnnouncementText = announcementText(nextSegmentDirection, nextSegmentDistance);
227         if (!nextSegmentAnnouncementText.isEmpty()) {
228             m_announcementText += QLatin1String(", then ") + nextSegmentAnnouncementText;
229         }
230     }
231     emit m_parent->instructionChanged();
232 }
233 
updateInstruction(const QString & name)234 void VoiceNavigationModelPrivate::updateInstruction( const QString &name )
235 {
236     m_queue.clear();
237     m_queue << audioFile( name );
238     m_announcementText = name;
239     emit m_parent->instructionChanged();
240 }
241 
initializeMaps()242 void VoiceNavigationModelPrivate::initializeMaps()
243 {
244     m_turnTypeMap.clear();
245     m_announceMap.clear();
246 
247     m_announceMap[Maneuver::Continue] = "Straight";
248     // none of our voice navigation commands fits, so leave out
249     // Maneuver::Merge here to have a sound play instead
250     m_announceMap[Maneuver::Straight] = "Straight";
251     m_announceMap[Maneuver::SlightRight] = "AhKeepRight";
252     m_announceMap[Maneuver::Right] = "AhRightTurn";
253     m_announceMap[Maneuver::SharpRight] = "AhRightTurn";
254     m_announceMap[Maneuver::TurnAround] = "AhUTurn";
255     m_announceMap[Maneuver::SharpLeft] = "AhLeftTurn";
256     m_announceMap[Maneuver::Left] = "AhLeftTurn";
257     m_announceMap[Maneuver::SlightLeft] = "AhKeepLeft";
258     m_announceMap[Maneuver::RoundaboutFirstExit] = "RbExit1";
259     m_announceMap[Maneuver::RoundaboutSecondExit] = "RbExit2";
260     m_announceMap[Maneuver::RoundaboutThirdExit] = "RbExit3";
261     m_announceMap[Maneuver::ExitLeft] = "AhExitLeft";
262     m_announceMap[Maneuver::ExitRight] = "AhExitRight";
263 
264     m_turnTypeMap[Maneuver::Continue] = "Straight";
265     // none of our voice navigation commands fits, so leave out
266     // Maneuver::Merge here to have a sound play instead
267     m_turnTypeMap[Maneuver::Straight] = "Straight";
268     m_turnTypeMap[Maneuver::SlightRight] = "BearRight";
269     m_turnTypeMap[Maneuver::Right] = "TurnRight";
270     m_turnTypeMap[Maneuver::SharpRight] = "SharpRight";
271     m_turnTypeMap[Maneuver::TurnAround] = "UTurn";
272     m_turnTypeMap[Maneuver::SharpLeft] = "SharpLeft";
273     m_turnTypeMap[Maneuver::Left] = "TurnLeft";
274     m_turnTypeMap[Maneuver::SlightLeft] = "BearLeft";
275     m_turnTypeMap[Maneuver::RoundaboutFirstExit] = "";
276     m_turnTypeMap[Maneuver::RoundaboutSecondExit] = "";
277     m_turnTypeMap[Maneuver::RoundaboutThirdExit] = "";
278     m_turnTypeMap[Maneuver::ExitLeft] = "TurnLeft";
279     m_turnTypeMap[Maneuver::ExitRight] = "TurnRight";
280 }
281 
VoiceNavigationModel(QObject * parent)282 VoiceNavigationModel::VoiceNavigationModel( QObject *parent ) :
283     QObject( parent ), d( new VoiceNavigationModelPrivate( this ) )
284 {
285     // nothing to do
286 }
287 
~VoiceNavigationModel()288 VoiceNavigationModel::~VoiceNavigationModel()
289 {
290     delete d;
291 }
292 
speaker() const293 QString VoiceNavigationModel::speaker() const
294 {
295     return d->m_speaker;
296 }
297 
setSpeaker(const QString & speaker)298 void 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(QLatin1String("/audio/speakers/") + speaker);
304         } else {
305             d->m_speaker = speaker;
306         }
307 
308         emit speakerChanged();
309         emit previewChanged();
310     }
311 }
312 
isSpeakerEnabled() const313 bool VoiceNavigationModel::isSpeakerEnabled() const
314 {
315     return d->m_speakerEnabled;
316 }
317 
setSpeakerEnabled(bool enabled)318 void VoiceNavigationModel::setSpeakerEnabled( bool enabled )
319 {
320     if ( enabled != d->m_speakerEnabled ) {
321         d->m_speakerEnabled = enabled;
322         emit isSpeakerEnabledChanged();
323         emit previewChanged();
324     }
325 }
326 
reset()327 void VoiceNavigationModel::reset()
328 {
329     d->reset();
330 }
331 
update(const Route & route,qreal distanceManuever,qreal distanceTarget,bool deviated)332 void 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 ? "You have arrived at your destination" : "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 ? "Deviated from the route" : "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 
preview() const395 QString VoiceNavigationModel::preview() const
396 {
397     return d->audioFile( d->m_speakerEnabled ? "The Marble team wishes you a pleasant and safe journey!" : "AppPositive" );
398 }
399 
instruction() const400 QString VoiceNavigationModel::instruction() const
401 {
402     return d->m_announcementText;
403 }
404 
405 }
406 
407 #include "moc_VoiceNavigationModel.cpp"
408