Fixes to speed alarm and poi alerts. Added flicker effect. Some new fields to text...
[jspeed] / src / poialerts.cpp
1 /*
2  * This file is part of jSpeed.
3  *
4  * jSpeed is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * jSpeed is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with jSpeed.  If not, see <http://www.gnu.org/licenses/>.
16  *
17  */
18
19 #include <QtCore/QString>
20 #include <QtCore/QDir>
21 #include <QtCore/QTimer>
22 #include <QtCore/QFile>
23 #include <QtCore/QDebug>
24 #include <math.h>
25 #include "poialerts.h"
26 #include "odometer.h"
27 #include "settings.h"
28 #include "location.h"
29 #include "mediaplayer.h"
30
31 namespace
32 {
33     static double const EARTH_MEAN_RADIUS = 6371.0072;
34     static double const PI = 3.14159265;
35 }
36
37 inline static double degToRad(double deg)
38 {
39     return deg * PI / 180;
40 }
41
42 inline static double radToDeg(double rad)
43 {
44     return rad * 180 / PI;
45 }
46
47 inline static double absValue(double value)
48 {
49     if(value < 0.0)
50     {
51         return -value;
52     }
53
54     return value;
55 }
56
57 PoiAlerts::PoiAlerts(): QObject(0), enabled_(false), currentPoi_(0), loaded_(false)
58 {
59 }
60
61 PoiAlerts::~PoiAlerts()
62 {
63     end();
64 }
65
66 PoiAlerts& PoiAlerts::instance()
67 {
68     static PoiAlerts instance;
69     return instance;
70 }
71
72 bool PoiAlerts::start()
73 {
74     if(!loaded_)
75     {
76         if(!loadConfig())
77         {
78             return false;
79         }
80     }
81
82     if(enabled_)
83     {
84         connect(&(Odometer::instance()), SIGNAL(dataUpdated()), this, SLOT(onDataUpdated()));
85     }
86
87     return true;
88 }
89
90 void PoiAlerts::end()
91 {
92     if(enabled_)
93     {
94         disconnect(&(Odometer::instance()), SIGNAL(dataUpdated()), this, SLOT(onDataUpdated()));
95     }
96
97     pois_.clear();
98     playedSounds_.clear();
99     travelled_ = 0;
100     enabled_ = false;
101     currentPoi_ = 0;
102 }
103
104 bool PoiAlerts::loadConfig()
105 {
106     loaded_ = true;
107
108     bool enabled = Settings::instance().value("alert_enabled", false).toBool();
109
110     if(!enabled)
111     {
112         end();
113         enabled_ = false;
114         return true;
115     }
116
117     distance_ = Settings::instance().value("alert_distance", 300).toDouble();
118     onlyOnRoute_ = Settings::instance().value("alert_only_on_route", true).toBool();
119
120     if(distance_ < 0)
121     {
122         distance_ = 0;
123     }
124
125     QString filename = Settings::instance().value("alert_sound", "").toString();
126
127     if(filename.isEmpty())
128     {
129         filename = "alert.wav";
130     }
131
132     QString soundDir = MediaPlayer::getSoundDir();
133     QString localDir = MediaPlayer::getLocalSoundDir();
134
135     if(!filename.isEmpty() && QFile::exists(soundDir + filename))
136     {
137         file_ = soundDir + filename;
138     }
139     else if(!filename.isEmpty() && QFile::exists(localDir + filename))
140     {
141         file_ = localDir + filename;
142     }
143     else
144     {
145         enabled_ = false;
146         return false;
147     }
148
149     if(!loadPois())
150     {
151         enabled_ = false;
152         return false;
153     }
154
155     if(!enabled_)
156     {
157         enabled_ = true;
158         start();
159     }
160
161     return true;
162 }
163
164 bool PoiAlerts::loadPois()
165 {
166     QString filename = Settings::instance().value("alert_poi_file", "").toString();
167
168     QString poiFile = getPoiDir() + filename;
169
170     if(filename.isEmpty() || !QFile::exists(poiFile))
171     {
172         error_ = "Poi file doesn't exist";
173         return false;
174     }
175
176     PoiReader* reader = PoiReader::getReader(poiFile);
177
178     if(!reader)
179     {
180         error_ = "Unknown file format: " + poiFile;
181         return false;
182     }
183
184     if(!reader->read(pois_))
185     {
186         error_ = reader->error();
187         return false;
188     }
189
190     return true;
191 }
192
193 double PoiAlerts::getCurrentDistance() const
194 {
195     if(!currentPoi_)
196     {
197         return 0.0;
198     }
199
200     return currentDistance_;
201 }
202
203 QString PoiAlerts::getCurrentPoi() const
204 {
205     if(currentPoi_)
206     {
207         return currentPoi_->name;
208     }
209
210     return "";
211 }
212
213 bool PoiAlerts::poiInView() const
214 {
215     return currentPoi_ != 0;
216 }
217
218 QString const& PoiAlerts::error() const
219 {
220     return error_;
221 }
222
223 void PoiAlerts::onDataUpdated()
224 {
225     const Location::Fix* fix = &(Odometer::instance().getLatestFix());
226
227     if(fix->latitude < 0.01 || fix->longitude < 0.01 ||
228        fix->kmSpeed < 0.01 || isnan(fix->eph))
229     {
230         return;
231     }
232
233     double travelled = Odometer::instance().getTotal();
234
235     if(absValue(travelled - travelled_) > 0.02)
236     {
237         double distance;
238         double inRouteMargin = IN_ROUTE_MARGIN + (fix->eph / 1000.0);
239
240         bool found = false;
241
242         travelled_ = travelled;
243
244         for(int i = 0; i < pois_.size(); i++)
245         {
246             if((distance = calculateDistance(pois_.at(i).latitude, pois_.at(i).longitude,
247                                              fix->latitude, fix->longitude)) <= distance_)
248             {
249                 qDebug() << "Distance: " << distance;
250
251                 if(onlyOnRoute_)
252                 {
253                     double track = absValue(calculateTrack(fix->latitude, fix->longitude,
254                                                            pois_.at(i).latitude, pois_.at(i).longitude) - fix->track);
255
256                     if(track > 180)
257                     {
258                         track = 360.0 - track;
259                     }
260
261                     qDebug() << "Real track: " << track;
262
263                     double trackLimit;
264
265                     if(distance < (inRouteMargin * 2.0))
266                     {
267                         trackLimit = 60.0;
268                     }
269                     else
270                     {
271                         trackLimit = 90.0 - radToDeg(acos((inRouteMargin + (distance * 0.16)) / distance));
272                     }
273
274                     qDebug() << "Tracklimit: " << trackLimit;
275
276                     if(track < trackLimit)
277                     {
278                         found = true;
279                         currentPoi_ = &pois_[i];
280                         currentDistance_ = distance;
281                         emit visibilityChanged(true);
282                         playSound(i);
283                     }
284                     else
285                     {
286                         currentPoi_ = 0;
287                         emit visibilityChanged(false);
288                     }
289
290                 }
291                 else
292                 {
293                     found = true;
294                     currentPoi_ = &pois_[i];
295                     currentDistance_ = distance;
296                     emit visibilityChanged(true);
297                     playSound(i);
298                 }
299
300                 break;
301             }
302         }
303
304         if(!found && currentPoi_)
305         {
306             currentPoi_ = 0;
307             emit visibilityChanged(false);
308         }
309     }
310 }
311
312 double PoiAlerts::calculateDistance(double latitude1, double longitude1,
313                          double latitude2, double longitude2)
314 {
315     double dlat = degToRad(latitude1 - latitude2);
316     double dlon = degToRad(longitude1 - longitude2);
317     double y = sin(dlat / 2.0) * sin(dlat / 2.0)
318         + cos(degToRad(latitude2))
319         * cos(degToRad(latitude1))
320         * sin(dlon / 2.0) * sin(dlon / 2.0);
321     double x = 2 * atan2(sqrt(y), sqrt(1 - y));
322     return x * EARTH_MEAN_RADIUS * 1000;
323 }
324
325 double PoiAlerts::calculateTrack(double latitude1, double longitude1,
326                                  double latitude2, double longitude2)
327 {
328     double dlon = degToRad(longitude2 - longitude1);
329     double lat1Rad = degToRad(latitude1);
330     double lat2Rad = degToRad(latitude2);
331     double y = sin(dlon) * cos(lat2Rad);
332     double x = cos(lat1Rad) * sin(lat2Rad) - sin(lat1Rad) * cos(lat2Rad) * cos(dlon);
333     double whole;
334     double fraction = modf(radToDeg(atan2(y, x)), &whole);
335     return (int(whole + 360) % 360) + fraction;
336 }
337
338 void PoiAlerts::playSound(int poiIndex)
339 {
340     qDebug() << "Almost play sound";
341
342     if(playedSounds_.indexOf(poiIndex) == -1)
343     {
344         playedSounds_.enqueue(poiIndex);
345
346         qDebug() << "Play sound";
347         MediaPlayer::play(file_);
348
349         QTimer::singleShot(POI_ALERT_INTERVAL * 1000, this, SLOT(removePlayed()));
350     }
351
352 }
353
354 void PoiAlerts::removePlayed()
355 {
356     if(!playedSounds_.isEmpty())
357     {
358         int removed = playedSounds_.dequeue();
359         qDebug() << "Removed: " << removed;
360     }
361 }
362
363 QString PoiAlerts::getPoiDir()
364 {
365     return Settings::getDir() + "pois" + QDir::separator();
366 }