1 // SPDX-License-Identifier: LGPL-2.1-or-later
2 //
3 // SPDX-FileCopyrightText: 2009 Bastian Holst <bastianholst@gmx.de>
4 //
5 
6 // Self
7 #include "BBCParser.h"
8 
9 // Marble
10 #include "MarbleGlobal.h"
11 #include "BBCWeatherItem.h"
12 #include "MarbleDebug.h"
13 
14 // Qt
15 #include <QDateTime>
16 #include <QFile>
17 #include <QMutexLocker>
18 #include <QRegExp>
19 
20 using namespace Marble;
21 
BBCParser(QObject * parent)22 BBCParser::BBCParser( QObject *parent ) :
23     AbstractWorkerThread( parent ),
24     m_dayConditions(),
25     m_nightConditions(),
26     m_windDirections(),
27     m_pressureDevelopments(),
28     m_visibilityStates(),
29     m_monthNames()
30 {
31     m_dayConditions["sunny"] = WeatherData::ClearDay;
32     m_dayConditions["clear"] = WeatherData::ClearDay;
33     m_dayConditions["clear sky"] = WeatherData::ClearDay;
34     m_dayConditions["sunny intervals"] = WeatherData::FewCloudsDay;
35     m_dayConditions["partly cloudy"] = WeatherData::PartlyCloudyDay;
36     m_dayConditions["white cloud"] = WeatherData::Overcast;
37     m_dayConditions["grey cloud"] = WeatherData::Overcast;
38     m_dayConditions["cloudy"] = WeatherData::Overcast;
39     m_dayConditions["drizzle"] = WeatherData::LightRain;
40     m_dayConditions["misty"] = WeatherData::Mist;
41     m_dayConditions["mist"] = WeatherData::Mist;
42     m_dayConditions["fog"] = WeatherData::Mist;
43     m_dayConditions["foggy"] = WeatherData::Mist;
44     m_dayConditions["dense fog"] = WeatherData::Mist;
45     m_dayConditions["Thick Fog"] = WeatherData::Mist;
46     m_dayConditions["tropical storm"] = WeatherData::Thunderstorm;
47     m_dayConditions["hazy"] = WeatherData::Mist;
48     m_dayConditions["light shower"] = WeatherData::LightShowersDay;
49     m_dayConditions["light rain shower"] = WeatherData::LightShowersDay;
50     m_dayConditions["light showers"] = WeatherData::LightShowersDay;
51     m_dayConditions["light rain"] = WeatherData::ShowersDay;
52     m_dayConditions["heavy rain"] = WeatherData::Rain;
53     m_dayConditions["heavy showers"] = WeatherData::Rain;
54     m_dayConditions["heavy shower"] = WeatherData::Rain;
55     m_dayConditions["heavy rain shower"] = WeatherData::Rain;
56     m_dayConditions["thundery shower"] = WeatherData::Thunderstorm;
57     m_dayConditions["thunderstorm"] = WeatherData::Thunderstorm;
58     m_dayConditions["thunder storm"] = WeatherData::Thunderstorm;
59     m_dayConditions["cloudy with sleet"] = WeatherData::RainSnow;
60     m_dayConditions["sleet shower"] = WeatherData::RainSnow;
61     m_dayConditions["sleet showers"] = WeatherData::RainSnow;
62     m_dayConditions["sleet"] = WeatherData::RainSnow;
63     m_dayConditions["cloudy with hail"] = WeatherData::Hail;
64     m_dayConditions["hail shower"] = WeatherData::Hail;
65     m_dayConditions["hail showers"] = WeatherData::Hail;
66     m_dayConditions["hail"] = WeatherData::Hail;
67     m_dayConditions["light snow"] = WeatherData::LightSnow;
68     m_dayConditions["light snow shower"] = WeatherData::ChanceSnowDay;
69     m_dayConditions["light snow showers"] = WeatherData::ChanceSnowDay;
70     m_dayConditions["cloudy with light snow"] = WeatherData::LightSnow;
71     m_dayConditions["heavy snow"] = WeatherData::Snow;
72     m_dayConditions["heavy snow shower"] = WeatherData::Snow;
73     m_dayConditions["heavy snow showers"] = WeatherData::Snow;
74     m_dayConditions["cloudy with heavy snow"] = WeatherData::Snow;
75     m_dayConditions["sandstorm"] = WeatherData::SandStorm;
76     m_dayConditions["na"] = WeatherData::ConditionNotAvailable;
77     m_dayConditions["N/A"] = WeatherData::ConditionNotAvailable;
78 
79     m_nightConditions["sunny"] = WeatherData::ClearNight;
80     m_nightConditions["clear"] = WeatherData::ClearNight;
81     m_nightConditions["clear sky"] = WeatherData::ClearNight;
82     m_nightConditions["sunny intervals"] = WeatherData::FewCloudsNight;
83     m_nightConditions["partly cloudy"] = WeatherData::PartlyCloudyNight;
84     m_nightConditions["white cloud"] = WeatherData::Overcast;
85     m_nightConditions["grey cloud"] = WeatherData::Overcast;
86     m_nightConditions["cloudy"] = WeatherData::Overcast;
87     m_nightConditions["drizzle"] = WeatherData::LightRain;
88     m_nightConditions["misty"] = WeatherData::Mist;
89     m_nightConditions["mist"] = WeatherData::Mist;
90     m_nightConditions["fog"] = WeatherData::Mist;
91     m_nightConditions["foggy"] = WeatherData::Mist;
92     m_nightConditions["dense fog"] = WeatherData::Mist;
93     m_nightConditions["Thick Fog"] = WeatherData::Mist;
94     m_nightConditions["tropical storm"] = WeatherData::Thunderstorm;
95     m_nightConditions["hazy"] = WeatherData::Mist;
96     m_nightConditions["light shower"] = WeatherData::LightShowersNight;
97     m_nightConditions["light rain shower"] = WeatherData::LightShowersNight;
98     m_nightConditions["light showers"] = WeatherData::LightShowersNight;
99     m_nightConditions["light rain"] = WeatherData::ShowersNight;
100     m_nightConditions["heavy rain"] = WeatherData::Rain;
101     m_nightConditions["heavy showers"] = WeatherData::Rain;
102     m_nightConditions["heavy shower"] = WeatherData::Rain;
103     m_nightConditions["heavy rain shower"] = WeatherData::Rain;
104     m_nightConditions["thundery shower"] = WeatherData::Thunderstorm;
105     m_nightConditions["thunderstorm"] = WeatherData::Thunderstorm;
106     m_nightConditions["thunder storm"] = WeatherData::Thunderstorm;
107     m_nightConditions["cloudy with sleet"] = WeatherData::RainSnow;
108     m_nightConditions["sleet shower"] = WeatherData::RainSnow;
109     m_nightConditions["sleet showers"] = WeatherData::RainSnow;
110     m_nightConditions["sleet"] = WeatherData::RainSnow;
111     m_nightConditions["cloudy with hail"] = WeatherData::Hail;
112     m_nightConditions["hail shower"] = WeatherData::Hail;
113     m_nightConditions["hail showers"] = WeatherData::Hail;
114     m_nightConditions["hail"] = WeatherData::Hail;
115     m_nightConditions["light snow"] = WeatherData::LightSnow;
116     m_nightConditions["light snow shower"] = WeatherData::ChanceSnowNight;
117     m_nightConditions["light snow showers"] = WeatherData::ChanceSnowNight;
118     m_nightConditions["cloudy with light snow"] = WeatherData::LightSnow;
119     m_nightConditions["heavy snow"] = WeatherData::Snow;
120     m_nightConditions["heavy snow shower"] = WeatherData::Snow;
121     m_nightConditions["heavy snow showers"] = WeatherData::Snow;
122     m_nightConditions["cloudy with heavy snow"] = WeatherData::Snow;
123     m_nightConditions["sandstorm"] = WeatherData::SandStorm;
124     m_nightConditions["na"] = WeatherData::ConditionNotAvailable;
125     m_nightConditions["N/A"] = WeatherData::ConditionNotAvailable;
126 
127     m_windDirections["N"] = WeatherData::N;
128     m_windDirections["NE"] = WeatherData::NE;
129     m_windDirections["ENE"] = WeatherData::ENE;
130     m_windDirections["NNE"] = WeatherData::NNE;
131     m_windDirections["E"] = WeatherData::E;
132     m_windDirections["SSE"] = WeatherData::SSE;
133     m_windDirections["SE"] = WeatherData::SE;
134     m_windDirections["ESE"] = WeatherData::ESE;
135     m_windDirections["S"] = WeatherData::S;
136     m_windDirections["NNW"] = WeatherData::NNW;
137     m_windDirections["NW"] = WeatherData::NW;
138     m_windDirections["WNW"] = WeatherData::WNW;
139     m_windDirections["W"] = WeatherData::W;
140     m_windDirections["SSW"] = WeatherData::SSW;
141     m_windDirections["SW"] = WeatherData::SW;
142     m_windDirections["WSW"] = WeatherData::WSW;
143     m_windDirections["N/A"] = WeatherData::DirectionNotAvailable;
144 
145     m_pressureDevelopments["falling"] = WeatherData::Falling;
146     m_pressureDevelopments["no change"] = WeatherData::NoChange;
147     m_pressureDevelopments["steady"] = WeatherData::NoChange;
148     m_pressureDevelopments["rising"] = WeatherData::Rising;
149     m_pressureDevelopments["N/A"] = WeatherData::PressureDevelopmentNotAvailable;
150 
151     m_visibilityStates["excellent"] = WeatherData::VeryGood;
152     m_visibilityStates["very good"] = WeatherData::VeryGood;
153     m_visibilityStates["good"] = WeatherData::Good;
154     m_visibilityStates["moderate"] = WeatherData::Normal;
155     m_visibilityStates["poor"] = WeatherData::Poor;
156     m_visibilityStates["very poor"] = WeatherData::VeryPoor;
157     m_visibilityStates["fog"] = WeatherData::Fog;
158     m_visibilityStates["n/a"] = WeatherData::VisibilityNotAvailable;
159 
160     m_monthNames["Jan"] = 1;
161     m_monthNames["Feb"] = 2;
162     m_monthNames["Mar"] = 3;
163     m_monthNames["Apr"] = 4;
164     m_monthNames["May"] = 5;
165     m_monthNames["Jun"] = 6;
166     m_monthNames["Jul"] = 7;
167     m_monthNames["Aug"] = 8;
168     m_monthNames["Sep"] = 9;
169     m_monthNames["Oct"] = 10;
170     m_monthNames["Nov"] = 11;
171     m_monthNames["Dec"] = 12;
172 }
173 
~BBCParser()174 BBCParser::~BBCParser()
175 {
176 }
177 
instance()178 BBCParser *BBCParser::instance()
179 {
180     static BBCParser parser;
181     return &parser;
182 }
183 
scheduleRead(const QString & path,BBCWeatherItem * item,const QString & type)184 void BBCParser::scheduleRead( const QString& path,
185                               BBCWeatherItem *item,
186                               const QString& type )
187 {
188     ScheduleEntry entry;
189     entry.path = path;
190     entry.item = item;
191     entry.type = type;
192 
193     m_scheduleMutex.lock();
194     m_schedule.push( entry );
195     m_scheduleMutex.unlock();
196 
197     ensureRunning();
198 }
199 
workAvailable()200 bool BBCParser::workAvailable()
201 {
202     QMutexLocker locker( &m_scheduleMutex );
203     return !m_schedule.isEmpty();
204 }
205 
work()206 void BBCParser::work()
207 {
208     m_scheduleMutex.lock();
209     ScheduleEntry entry = m_schedule.pop();
210     m_scheduleMutex.unlock();
211 
212     QFile file( entry.path );
213     if( !file.open( QIODevice::ReadOnly | QIODevice::Text ) ) {
214         return;
215     }
216 
217     QList<WeatherData> data = read( &file );
218 
219     if( !data.isEmpty() && !entry.item.isNull() ) {
220         if (entry.type == QLatin1String("bbcobservation")) {
221             entry.item->setCurrentWeather( data.at( 0 ) );
222         }
223         else if (entry.type == QLatin1String("bbcforecast")) {
224             entry.item->addForecastWeather( data );
225         }
226 
227         emit parsedFile();
228     }
229 }
230 
read(QIODevice * device)231 QList<WeatherData> BBCParser::read( QIODevice *device )
232 {
233     m_list.clear();
234     setDevice( device );
235 
236     while ( !atEnd() ) {
237         readNext();
238 
239         if ( isStartElement() ) {
240             if (name() == QLatin1String("rss"))
241                 readBBC();
242             else
243                 raiseError( QObject::tr("The file is not a valid BBC answer.") );
244         }
245     }
246 
247     return m_list;
248 }
249 
readUnknownElement()250 void BBCParser::readUnknownElement()
251 {
252     Q_ASSERT( isStartElement() );
253 
254     while ( !atEnd() ) {
255         readNext();
256 
257         if ( isEndElement() )
258             break;
259 
260         if ( isStartElement() )
261             readUnknownElement();
262     }
263 }
264 
readBBC()265 void BBCParser::readBBC()
266 {
267     Q_ASSERT( isStartElement()
268               && name() == QLatin1String("rss"));
269 
270     while( !atEnd() ) {
271         readNext();
272 
273         if( isEndElement() )
274             break;
275 
276         if( isStartElement() ) {
277             if (name() == QLatin1String("channel"))
278                 readChannel();
279             else
280                 readUnknownElement();
281         }
282     }
283 }
284 
readChannel()285 void BBCParser::readChannel()
286 {
287     Q_ASSERT( isStartElement()
288               && name() == QLatin1String("channel"));
289 
290     while( !atEnd() ) {
291         readNext();
292 
293         if( isEndElement() )
294             break;
295 
296         if( isStartElement() ) {
297             if (name() == QLatin1String("item"))
298                 readItem();
299             else
300                 readUnknownElement();
301         }
302     }
303 }
304 
readItem()305 void BBCParser::readItem()
306 {
307     Q_ASSERT( isStartElement()
308               && name() == QLatin1String("item"));
309 
310     WeatherData item;
311 
312     while( !atEnd() ) {
313         readNext();
314 
315         if( isEndElement() )
316             break;
317 
318         if( isStartElement() ) {
319             if (name() == QLatin1String("description"))
320                 readDescription( &item );
321             else if(name() == QLatin1String("title"))
322                 readTitle( &item );
323             else if (name() == QLatin1String("pubDate"))
324                 readPubDate( &item );
325             else
326                 readUnknownElement();
327         }
328     }
329 
330     m_list.append( item );
331 }
332 
readDescription(WeatherData * data)333 void BBCParser::readDescription( WeatherData *data )
334 {
335     Q_ASSERT( isStartElement()
336               && name() == QLatin1String("description"));
337 
338     while( !atEnd() ) {
339         readNext();
340 
341         if( isEndElement() )
342             break;
343 
344         if( isStartElement() ) {
345             readUnknownElement();
346         }
347 
348         if( isCharacters() ) {
349             QString description = text().toString();
350             QRegExp regExp;
351 
352             // Temperature
353             regExp.setPattern( "(Temperature:\\s*)(-?\\d+)(.C)" );
354             int pos = regExp.indexIn( description );
355             if ( pos > -1 ) {
356                 QString value = regExp.cap( 2 );
357                 data->setTemperature( value.toDouble(), WeatherData::Celsius );
358             }
359 
360             // Max Temperature
361             regExp.setPattern( "(Max Temp:\\s*)(-?\\d+)(.C)" );
362             pos = regExp.indexIn( description );
363             if ( pos > -1 ) {
364                 QString value = regExp.cap( 2 );
365                 data->setMaxTemperature( value.toDouble(), WeatherData::Celsius );
366             }
367 
368             // Min Temperature
369             regExp.setPattern( "(Min Temp:\\s*)(-?\\d+)(.C)" );
370             pos = regExp.indexIn( description );
371             if ( pos > -1 ) {
372                 QString value = regExp.cap( 2 );
373                 data->setMinTemperature( value.toDouble(), WeatherData::Celsius );
374             }
375 
376             // Wind direction
377             regExp.setPattern( "(Wind Direction:\\s*)([NESW]+)(,)" );
378             pos = regExp.indexIn( description );
379             if ( pos > -1 ) {
380                 QString wind = regExp.cap( 2 );
381 
382                 if ( m_windDirections.contains( wind ) ) {
383                     data->setWindDirection( m_windDirections.value( wind ) );
384                 }
385                 else {
386                     mDebug() << "UNHANDLED WIND DIRECTION, PLEASE REPORT: " << wind;
387                 }
388             }
389 
390             // Wind speed
391             regExp.setPattern( "(Wind Speed:\\s*)(\\d+)(mph)" );
392             pos = regExp.indexIn( description );
393             if ( pos > -1 ) {
394                 QString speed = regExp.cap( 2 );
395                 data->setWindSpeed( speed.toFloat(), WeatherData::mph );
396             }
397 
398             // Relative Humidity
399             regExp.setPattern( "(Relative Humidity:\\s*)(\\d+)(.,)" );
400             pos = regExp.indexIn( description );
401             if ( pos > -1 ) {
402                 QString humidity = regExp.cap( 2 );
403                 data->setHumidity( humidity.toFloat() );
404             }
405 
406             // Pressure
407             regExp.setPattern( "(Pressure:\\s*)(\\d+mB|N/A)(, )([a-z ]+|N/A)(,)" );
408             pos = regExp.indexIn( description );
409             if ( pos > -1 ) {
410                 QString pressure = regExp.cap( 2 );
411                 if (pressure != QLatin1String("N/A")) {
412                     pressure.chop( 2 );
413                     data->setPressure( pressure.toFloat()/1000, WeatherData::Bar );
414                 }
415 
416                 QString pressureDevelopment = regExp.cap( 4 );
417 
418                 if ( m_pressureDevelopments.contains( pressureDevelopment ) ) {
419                     data->setPressureDevelopment( m_pressureDevelopments.value( pressureDevelopment ) );
420                 }
421                 else {
422                     mDebug() << "UNHANDLED PRESSURE DEVELOPMENT, PLEASE REPORT: "
423                              << pressureDevelopment;
424                 }
425             }
426 
427             // Visibility
428             regExp.setPattern( "(Visibility:\\s*)([^,]+)" );
429             pos = regExp.indexIn( description );
430             if ( pos > -1 ) {
431                 QString visibility = regExp.cap( 2 );
432 
433                 if ( m_visibilityStates.contains( visibility.toLower() ) ) {
434                     data->setVisibilty( m_visibilityStates.value( visibility ) );
435                 }
436                 else {
437                     mDebug() << "UNHANDLED VISIBILITY, PLEASE REPORT: " << visibility;
438                 }
439             }
440         }
441     }
442 }
443 
readTitle(WeatherData * data)444 void BBCParser::readTitle( WeatherData *data )
445 {
446     Q_ASSERT( isStartElement()
447               && name() == QLatin1String("title"));
448 
449     while( !atEnd() ) {
450         readNext();
451 
452         if( isEndElement() )
453             break;
454 
455         if( isStartElement() ) {
456             readUnknownElement();
457         }
458 
459         if( isCharacters() ) {
460             QString title = text().toString();
461             QRegExp regExp;
462 
463             // Condition
464             regExp.setPattern( "(^.*)(:\\s*)([\\w ]+)([\\,\\.]\\s*)" );
465             int pos = regExp.indexIn( title );
466             if ( pos > -1 ) {
467                 QString value = regExp.cap( 3 );
468 
469                 if( m_dayConditions.contains( value ) ) {
470                     // TODO: Switch for day/night
471                     data->setCondition( m_dayConditions.value( value ) );
472                 }
473                 else {
474                     mDebug() << "UNHANDLED BBC WEATHER CONDITION, PLEASE REPORT: " << value;
475                 }
476 
477                 QString dayString = regExp.cap( 1 );
478                 Qt::DayOfWeek dayOfWeek = (Qt::DayOfWeek) 0;
479                 if (dayString.contains(QLatin1String("Monday"))) {
480                     dayOfWeek = Qt::Monday;
481                 } else if (dayString.contains(QLatin1String("Tuesday"))) {
482                     dayOfWeek = Qt::Tuesday;
483                 } else if (dayString.contains(QLatin1String("Wednesday"))) {
484                     dayOfWeek = Qt::Wednesday;
485                 } else if (dayString.contains(QLatin1String("Thursday"))) {
486                     dayOfWeek = Qt::Thursday;
487                 } else if (dayString.contains(QLatin1String("Friday"))) {
488                     dayOfWeek = Qt::Friday;
489                 } else if (dayString.contains(QLatin1String("Saturday"))) {
490                     dayOfWeek = Qt::Saturday;
491                 } else if (dayString.contains(QLatin1String("Sunday"))) {
492                     dayOfWeek = Qt::Sunday;
493                 }
494                 QDate date = QDate::currentDate();
495                 date = date.addDays( -1 );
496 
497                 for ( int i = 0; i < 7; i++ ) {
498                     if ( date.dayOfWeek() == dayOfWeek ) {
499                         data->setDataDate( date );
500                     }
501                     date = date.addDays( 1 );
502                 }
503             }
504         }
505     }
506 }
507 
readPubDate(WeatherData * data)508 void BBCParser::readPubDate( WeatherData *data )
509 {
510     Q_ASSERT( isStartElement()
511               && name() == QLatin1String("pubDate"));
512 
513     while( !atEnd() ) {
514         readNext();
515 
516         if( isEndElement() )
517             break;
518 
519         if( isStartElement() ) {
520             readUnknownElement();
521         }
522 
523         if( isCharacters() ) {
524             QString pubDate = text().toString();
525             QRegExp regExp;
526 
527             regExp.setPattern( "([A-Za-z]+,\\s+)(\\d+)(\\s+)([A-Za-z]+)(\\s+)(\\d{4,4})(\\s+)(\\d+)(:)(\\d+)(:)(\\d+)(\\s+)([+-])(\\d{2,2})(\\d{2,2})" );
528             int pos = regExp.indexIn( pubDate );
529             if ( pos > -1 ) {
530                 QDateTime dateTime;
531                 QDate date;
532                 QTime time;
533 
534                 dateTime.setTimeSpec( Qt::UTC );
535                 date.setDate( regExp.cap( 6 ).toInt(),
536                              m_monthNames.value( regExp.cap( 4 ) ),
537                              regExp.cap( 2 ).toInt() );
538                 time.setHMS( regExp.cap( 8 ).toInt(),
539                              regExp.cap( 10 ).toInt(),
540                              regExp.cap( 12 ).toInt() );
541 
542                 dateTime.setDate( date );
543                 dateTime.setTime( time );
544 
545                 // Timezone
546                 if (regExp.cap(14) == QLatin1String("-")) {
547                     dateTime = dateTime.addSecs( 60*60*regExp.cap( 15 ).toInt() );
548                     dateTime = dateTime.addSecs( 60   *regExp.cap( 16 ).toInt() );
549                 }
550                 else {
551                     dateTime = dateTime.addSecs( -60*60*regExp.cap( 15 ).toInt() );
552                     dateTime = dateTime.addSecs( -60   *regExp.cap( 16 ).toInt() );
553                 }
554 
555                 data->setPublishingTime( dateTime );
556             }
557         }
558     }
559 }
560 
561 #include "moc_BBCParser.cpp"
562