Copyright file added.
[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     const Location::Fix* fix = &(Odometer::instance().getLatestFix());
201
202     return calculateDistance(currentPoi_->latitude, currentPoi_->longitude,
203                              fix->latitude, fix->longitude);
204 }
205
206 QString PoiAlerts::getCurrentPoi() const
207 {
208     if(currentPoi_)
209     {
210         return currentPoi_->name;
211     }
212
213     return "";
214 }
215
216 bool PoiAlerts::poiInView() const
217 {
218     return currentPoi_ != 0;
219 }
220
221 QString const& PoiAlerts::error() const
222 {
223     return error_;
224 }
225
226 void PoiAlerts::onDataUpdated()
227 {
228     const Location::Fix* fix = &(Odometer::instance().getLatestFix());
229
230     if(fix->latitude < 0.01 || fix->longitude < 0.01 ||
231        fix->kmSpeed < 0.01 || isnan(fix->eph))
232     {
233         return;
234     }
235
236     double travelled = Odometer::instance().getTotal();
237
238     if(absValue(travelled - travelled_) > 0.015)
239     {
240         double distance;
241         double inRouteMargin = IN_ROUTE_MARGIN + (fix->eph / 1000.0);
242
243         bool found = false;
244
245         travelled_ = travelled;
246
247         for(int i = 0; i < pois_.size(); i++)
248         {
249             if((distance = calculateDistance(pois_.at(i).latitude, pois_.at(i).longitude,
250                                              fix->latitude, fix->longitude)) <= distance_)
251             {
252                 qDebug() << "Distance: " << distance;
253
254                 if(onlyOnRoute_)
255                 {
256                     double track = absValue(calculateTrack(fix->latitude, fix->longitude,
257                                                            pois_.at(i).latitude, pois_.at(i).longitude) - fix->track);
258
259                     if(track > 180)
260                     {
261                         track = 360.0 - track;
262                     }
263
264                     qDebug() << "Real track: " << track;
265
266                     double trackLimit;
267
268                     if(distance < (inRouteMargin * 2.0))
269                     {
270                         trackLimit = 60.0;
271                     }
272                     else
273                     {
274                         trackLimit = 90.0 - radToDeg(acos((inRouteMargin + (distance * 0.17)) / distance));
275                     }
276
277                     qDebug() << "Tracklimit: " << trackLimit;
278
279                     if(track < trackLimit)
280                     {
281                         found = true;
282                         currentPoi_ = &pois_[i];
283                         emit visibilityChanged(true);
284                         playSound(i);
285                     }
286                     else
287                     {
288                         currentPoi_ = 0;
289                         emit visibilityChanged(false);
290                     }
291
292                 }
293                 else
294                 {
295                     found = true;
296                     currentPoi_ = &pois_[i];
297                     emit visibilityChanged(true);
298                     playSound(i);
299                 }
300
301                 break;
302             }
303         }
304
305         if(!found && currentPoi_)
306         {
307             currentPoi_ = 0;
308             emit visibilityChanged(false);
309         }
310     }
311 }
312
313 double PoiAlerts::calculateDistance(double latitude1, double longitude1,
314                          double latitude2, double longitude2)
315 {
316     double dlat = degToRad(latitude1 - latitude2);
317     double dlon = degToRad(longitude1 - longitude2);
318     double y = sin(dlat / 2.0) * sin(dlat / 2.0)
319         + cos(degToRad(latitude2))
320         * cos(degToRad(latitude1))
321         * sin(dlon / 2.0) * sin(dlon / 2.0);
322     double x = 2 * atan2(sqrt(y), sqrt(1 - y));
323     return x * EARTH_MEAN_RADIUS * 1000;
324 }
325
326 double PoiAlerts::calculateTrack(double latitude1, double longitude1,
327                                  double latitude2, double longitude2)
328 {
329     double dlon = degToRad(longitude2 - longitude1);
330     double lat1Rad = degToRad(latitude1);
331     double lat2Rad = degToRad(latitude2);
332     double y = sin(dlon) * cos(lat2Rad);
333     double x = cos(lat1Rad) * sin(lat2Rad) - sin(lat1Rad) * cos(lat2Rad) * cos(dlon);
334     double whole;
335     double fraction = modf(radToDeg(atan2(y, x)), &whole);
336     return (int(whole + 360) % 360) + fraction;
337 }
338
339 void PoiAlerts::playSound(int poiIndex)
340 {
341     qDebug() << "Almost play sound";
342
343     if(playedSounds_.indexOf(poiIndex) == -1)
344     {
345         playedSounds_.enqueue(poiIndex);
346
347         qDebug() << "Play sound";
348         MediaPlayer::play(file_);
349
350         QTimer::singleShot(POI_ALERT_INTERVAL * 1000, this, SLOT(removePlayed()));
351     }
352
353 }
354
355 void PoiAlerts::removePlayed()
356 {
357     if(!playedSounds_.isEmpty())
358     {
359         int removed = playedSounds_.dequeue();
360         qDebug() << "Removed: " << removed;
361     }
362 }
363
364 QString PoiAlerts::getPoiDir()
365 {
366     return Settings::getDir() + "pois" + QDir::separator();
367 }