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