1 /* Copyright (c) 2003-2014 Xfce Development Team
2 *
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11 * General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program; if not, write to the Free Software
15 * Foundation, Inc., 51 Franklin Street, Fifth Floor,
16 * Boston, MA 02110-1301, USA.
17 */
18
19 #ifdef HAVE_CONFIG_H
20 #include <config.h>
21 #endif
22
23 #include <libxfce4util/libxfce4util.h>
24 #include <math.h>
25
26 #include "weather-parsers.h"
27 #include "weather-data.h"
28 #include "weather.h"
29 #include "weather-debug.h"
30
31 /* fallback values when astrodata is unavailable */
32 #define NIGHT_TIME_START 21
33 #define NIGHT_TIME_END 5
34
35 /* interval used for searching raw data relevant to a day */
36 #define DAY_START 3
37 #define DAY_END 33
38 #define DAYTIME_LEN 6
39
40 /* If some value is not present or cannot be computed, return this instead */
41 #define INVALID_VALUE -9999
42
43 #define CHK_NULL(s) ((s) ? g_strdup(s) : g_strdup(""))
44
45 #define ROUND_TO_INT(default_format) (round ? "%.0f" : default_format)
46
47 /* Converts temperatures in Celcius to Fahrenheit while preventing
48 * negative values rounded to zero from being displayed as "-0 °F". */
49 #define CALC_FAHRENHEIT(round, temperature) \
50 do { \
51 temperature = temperature * 9.0 / 5.0 + 32; \
52 if (round && temperature > -0.5 && temperature < 0) \
53 temperature = 0; \
54 } while (0)
55
56 #define LOCALE_DOUBLE(value, format) \
57 (value \
58 ? g_strdup_printf(format, g_ascii_strtod(value, NULL)) \
59 : g_strdup(""))
60
61 #define INTERPOLATE_OR_COPY(var, radian) \
62 if (ipol) \
63 comb->location->var = \
64 interpolate_gchar_value(start->location->var, \
65 end->location->var, \
66 comb->start, comb->end, \
67 comb->point, radian); \
68 else \
69 comb->location->var = g_strdup(end->location->var);
70
71 #define COMB_END_COPY(var) \
72 comb->location->var = g_strdup(end->location->var);
73
74
75 /* struct to store results from searches for point data */
76 typedef struct {
77 GArray *before;
78 time_t point;
79 GArray *after;
80 } point_data_results;
81
82
83 /* convert string to a double value, returning backup value on error */
84 gdouble
string_to_double(const gchar * str,const gdouble backup)85 string_to_double(const gchar *str,
86 const gdouble backup)
87 {
88 gdouble d = backup;
89 if (str && strlen(str) > 0)
90 d = g_ascii_strtod(str, NULL);
91 return d;
92 }
93
94
95 /* convert double to string, using non-local format */
96 gchar *
double_to_string(const gdouble val,const gchar * format)97 double_to_string(const gdouble val,
98 const gchar *format)
99 {
100 gchar buf[20];
101 return g_strdup(g_ascii_formatd(buf, 20,
102 format ? format : "%.1f",
103 val));
104 }
105
106
107 gchar *
format_date(time_t date_t,gchar * format,gboolean local)108 format_date(time_t date_t,
109 gchar *format,
110 gboolean local)
111 {
112 struct tm *tm;
113 gchar buf[40];
114 size_t size;
115
116 if (format == NULL)
117 format = "%Y-%m-%d %H:%M:%S";
118
119 if (G_LIKELY(local))
120 tm = localtime(&date_t);
121 else
122 tm = gmtime(&date_t);
123
124 /* A year <= 1970 means date has not been set */
125 if (G_UNLIKELY(tm == NULL) || tm->tm_year <= 70)
126 return g_strdup("-");
127 size = strftime(buf, 40, format, tm);
128 return (size ? g_strdup(buf) : g_strdup("-"));
129 }
130
131
132 /* check whether timeslice is interval or point data */
133 gboolean
timeslice_is_interval(xml_time * timeslice)134 timeslice_is_interval(xml_time *timeslice)
135 {
136 return (timeslice->location->symbol != NULL ||
137 timeslice->location->precipitation_value != NULL);
138 }
139
140
141 /*
142 * Calculate dew point in Celsius, taking the Magnus formulae as a
143 * basis. Source: http://de.wikipedia.org/wiki/Taupunkt
144 */
145 static gdouble
calc_dewpoint(const xml_location * loc)146 calc_dewpoint(const xml_location *loc)
147 {
148 gdouble temp, humidity, val;
149
150 if (G_UNLIKELY(loc->humidity_value == NULL))
151 return INVALID_VALUE;
152
153 temp = string_to_double(loc->temperature_value, 0);
154 humidity = string_to_double(loc->humidity_value, 0);
155 val = log(humidity / 100);
156 return (241.2 * val + 4222.03716 * temp / (241.2 + temp))
157 / (17.5043 - val - 17.5043 * temp / (241.2 + temp));
158 }
159
160
161 /* Calculate felt air temperature, using the chosen model. */
162 static gdouble
calc_apparent_temperature(const xml_location * loc,const apparent_temp_models model,const gboolean night_time)163 calc_apparent_temperature(const xml_location *loc,
164 const apparent_temp_models model,
165 const gboolean night_time)
166 {
167 gdouble temp = string_to_double(loc->temperature_value, 0);
168 gdouble windspeed = string_to_double(loc->wind_speed_mps, 0);
169 gdouble humidity = string_to_double(loc->humidity_value, 0);
170 gdouble dp, e;
171
172 switch (model) {
173 case WINDCHILL_HEATINDEX:
174 /* If temperature is lower than 10 °C, use wind chill index,
175 if above 26.7°C use the heat index / Summer Simmer Index. */
176
177 /* Wind chill, source:
178 http://www.nws.noaa.gov/os/windchill/index.shtml */
179 if (temp <= 10.0) {
180 /* wind chill is only defined for wind speeds above 3.0 mph */
181 windspeed *= 3.6;
182 if (windspeed < 4.828032)
183 return temp;
184
185 return 13.12 + 0.6215 * temp - 11.37 * pow(windspeed, 0.16)
186 + 0.3965 * temp * pow(windspeed, 0.16);
187 }
188
189 if (temp >= 26.7 || (night_time && temp >= 22.0)) {
190 /* humidity needs to be higher than 40% for a valid result */
191 if (humidity < 40)
192 return temp;
193
194 temp = temp * 9.0 / 5.0 + 32.0; /* both models use Fahrenheit */
195 if (!night_time)
196 /* Heat index, source:
197 Lans P. Rothfusz. "The Heat Index 'Equation' (or, More
198 Than You Ever Wanted to Know About Heat Index)",
199 Scientific Services Division (NWS Southern Region
200 Headquarters), 1 July 1990.
201 http://www.srh.noaa.gov/images/ffc/pdf/ta_htindx.PDF
202 */
203 return ((-42.379
204 + 2.04901523 * temp
205 + 10.14333127 * humidity
206 - 0.22475541 * temp * humidity
207 - 0.00683783 * temp * temp
208 - 0.05481717 * humidity * humidity
209 + 0.00122874 * temp * temp * humidity
210 + 0.00085282 * temp * humidity * humidity
211 - 0.00000199 * temp * temp * humidity * humidity)
212 - 32.0) * 5.0 / 9.0; /* convert back to Celsius */
213 else
214 /* Summer Simmer Index, sources:
215 http://www.summersimmer.com/home.htm
216 http://www.gorhamschaffler.com/humidity_formulas.htm */
217 return ((1.98 * (temp - (0.55 - 0.0055 * humidity)
218 * (temp - 58)) - 56.83)
219 - 32.0) * 5.0 / 9.0; /* convert back to Celsius */
220 }
221
222 /* otherwise simply return the temperature */
223 return temp;
224
225 case WINDCHILL_HUMIDEX:
226 /* If temperature is equal or lower than 0 °C, use wind chill index,
227 if above 20.0 °C use humidex. Source:
228 http://www.weatheroffice.gc.ca/mainmenu/faq_e.html */
229
230 if (temp <= 0) {
231 /* wind chill is only defined for wind speeds above 2.0 km/h */
232 windspeed *= 3.6;
233 if (windspeed < 2.0)
234 return temp;
235
236 /* wind chill, source:
237 http://www.nws.noaa.gov/os/windchill/index.shtml */
238 return 13.12 + 0.6215 * temp - 11.37 * pow(windspeed, 0.16)
239 + 0.3965 * temp * pow(windspeed, 0.16);
240 }
241
242 if (temp >= 20.0) {
243 /* Canadian humidex, source:
244 http://www.weatheroffice.gc.ca/mainmenu/faq_e.html#weather6 */
245 dp = calc_dewpoint(loc);
246
247 /* dew point needs to be above a certain limit for
248 valid results, see
249 http://www.weatheroffice.gc.ca/mainmenu/faq_e.html#weather5 */
250 if (dp < 0 || dp == INVALID_VALUE)
251 return temp;
252
253 /* dew point needs to be converted to Kelvin (easy job ;-) */
254 e = 6.11 * exp(5417.7530 * (1/273.16 - 1/(dp + 273.15)));
255 return temp + 0.5555 * (e - 10.0);
256 }
257 return temp;
258
259 case STEADMAN:
260 /* Australians use a different formula. Source:
261 http://www.bom.gov.au/info/thermal_stress/#atapproximation */
262 e = humidity / 100 * 6.105 * exp(17.27 * temp / (237.7 + temp));
263 return temp + 0.33 * e - 0.7 * windspeed - 4.0;
264
265 case QUAYLE_STEADMAN:
266 /* R. G. Quayle, R. G. Steadman: The Steadman wind chill: an
267 improvement over present scales. In: Weather and
268 Forecasting. 13, 1998, S. 1187–1193 */
269 return 1.41 - 1.162 * windspeed + 0.980 * temp
270 + 0.0124 * windspeed * windspeed + 0.0185 * windspeed * temp;
271
272 default:
273 return temp;
274 }
275 }
276
277
278 /*
279 * Return wind direction name for wind degrees, which gives the
280 * direction the wind is coming _from_.
281 */
282 static gchar*
wind_dir_name_by_deg(gchar * degrees,gboolean long_name)283 wind_dir_name_by_deg(gchar *degrees, gboolean long_name)
284 {
285 gdouble deg;
286
287 if (G_UNLIKELY(degrees == NULL))
288 return "";
289
290 deg = string_to_double(degrees, 0);
291
292 if (deg >= 360 - 22.5 || deg < 45 - 22.5)
293 return (long_name) ? _("North") : _("N");
294
295 if (deg >= 45 - 22.5 && deg < 45 + 22.5)
296 return (long_name) ? _("North-East") : _("NE");
297
298 if (deg >= 90 - 22.5 && deg < 90 + 22.5)
299 return (long_name) ? _("East") : _("E");
300
301 if (deg >= 135 - 22.5 && deg < 135 + 22.5)
302 return (long_name) ? _("South-East") : _("SE");
303
304 if (deg >= 180 - 22.5 && deg < 180 + 22.5)
305 return (long_name) ? _("South") : _("S");
306
307 if (deg >= 225 - 22.5 && deg < 225 + 22.5)
308 return (long_name) ? _("South-West") : _("SW");
309
310 if (deg >= 270 - 22.5 && deg < 270 + 22.5)
311 return (long_name) ? _("West") : _("W");
312
313 if (deg >= 315 - 22.5 && deg < 315 + 22.5)
314 return (long_name) ? _("North-West") : _("NW");
315
316 return "";
317 }
318
319
320 gchar *
get_data(const xml_time * timeslice,const units_config * units,const data_types type,const gboolean round,const gboolean night_time)321 get_data(const xml_time *timeslice,
322 const units_config *units,
323 const data_types type,
324 const gboolean round,
325 const gboolean night_time)
326 {
327 const xml_location *loc = NULL;
328 gdouble val, temp;
329
330 if (timeslice == NULL || timeslice->location == NULL || units == NULL)
331 return g_strdup("");
332
333 loc = timeslice->location;
334
335 switch (type) {
336 case ALTITUDE:
337 switch (units->altitude) {
338 case METERS:
339 return LOCALE_DOUBLE(loc->altitude, "%.0f");
340
341 case FEET:
342 val = string_to_double(loc->altitude, 0);
343 val /= 0.3048;
344 return g_strdup_printf(ROUND_TO_INT("%.2f"), val);
345 }
346 break;
347
348 case LATITUDE:
349 return LOCALE_DOUBLE(loc->latitude, "%.4f");
350
351 case LONGITUDE:
352 return LOCALE_DOUBLE(loc->longitude, "%.4f");
353
354 case TEMPERATURE: /* source is in °C */
355 val = string_to_double(loc->temperature_value, 0);
356 if (units->temperature == FAHRENHEIT)
357 CALC_FAHRENHEIT(round, val);
358 return g_strdup_printf(ROUND_TO_INT("%.1f"), val);
359
360 case PRESSURE: /* source is in hectopascals */
361 val = string_to_double(loc->pressure_value, 0);
362 switch (units->pressure) {
363 case INCH_MERCURY:
364 val *= 0.03;
365 break;
366 case PSI:
367 val *= 0.01450378911491;
368 break;
369 case TORR:
370 val /= 1.333224;
371 break;
372 }
373 return g_strdup_printf(ROUND_TO_INT("%.1f"), val);
374
375 case WIND_SPEED: /* source is in meters per hour */
376 val = string_to_double(loc->wind_speed_mps, 0);
377 switch (units->windspeed) {
378 case KMH:
379 val *= 3.6;
380 break;
381 case MPH:
382 val *= 2.2369362920544;
383 break;
384 case FTS:
385 val *= 3.2808399;
386 break;
387 case KNOTS:
388 val *= 1.9438445;
389 break;
390 }
391 return g_strdup_printf(ROUND_TO_INT("%.1f"), val);
392
393 case WIND_BEAUFORT:
394 val = string_to_double(loc->wind_speed_beaufort, 0);
395 return g_strdup_printf("%.0f", val);
396
397 case WIND_DIRECTION:
398 return g_strdup(wind_dir_name_by_deg(loc->wind_dir_deg, FALSE));
399
400 case WIND_DIRECTION_DEG:
401 return LOCALE_DOUBLE(loc->wind_dir_deg, ROUND_TO_INT("%.1f"));
402
403 case HUMIDITY:
404 return LOCALE_DOUBLE(loc->humidity_value, ROUND_TO_INT("%.1f"));
405
406 case DEWPOINT:
407 val = calc_dewpoint(loc);
408 if (val == INVALID_VALUE)
409 return g_strdup("");
410 if (units->temperature == FAHRENHEIT)
411 CALC_FAHRENHEIT(round, val);
412 return g_strdup_printf(ROUND_TO_INT("%.1f"), val);
413
414 case APPARENT_TEMPERATURE:
415 val = calc_apparent_temperature(loc, units->apparent_temperature,
416 night_time);
417 if (units->temperature == FAHRENHEIT)
418 CALC_FAHRENHEIT(round, val);
419 return g_strdup_printf(ROUND_TO_INT("%.1f"), val);
420
421 case CLOUDS_LOW:
422 return LOCALE_DOUBLE(loc->clouds_percent[CLOUDS_PERC_LOW],
423 ROUND_TO_INT("%.1f"));
424
425 case CLOUDS_MID:
426 return LOCALE_DOUBLE(loc->clouds_percent[CLOUDS_PERC_MID],
427 ROUND_TO_INT("%.1f"));
428
429 case CLOUDS_HIGH:
430 return LOCALE_DOUBLE(loc->clouds_percent[CLOUDS_PERC_HIGH],
431 ROUND_TO_INT("%.1f"));
432
433 case CLOUDINESS:
434 return LOCALE_DOUBLE(loc->clouds_percent[CLOUDS_PERC_CLOUDINESS],
435 ROUND_TO_INT("%.1f"));
436
437 case FOG:
438 return LOCALE_DOUBLE(loc->fog_percent, ROUND_TO_INT("%.1f"));
439
440 case PRECIPITATION: /* source is in millimeters */
441 val = string_to_double(loc->precipitation_value, 0);
442
443 /* For snow, adjust precipitation dependent on temperature. Source:
444 http://answers.yahoo.com/question/index?qid=20061230123635AAAdZAe */
445 if (loc->symbol_id == SYMBOL_SNOWSUN ||
446 loc->symbol_id == SYMBOL_SNOW ||
447 loc->symbol_id == SYMBOL_SNOWTHUNDER ||
448 loc->symbol_id == SYMBOL_SNOWSUNPOLAR ||
449 loc->symbol_id == SYMBOL_SNOWSUNTHUNDER) {
450 temp = string_to_double(loc->temperature_value, 0);
451 if (temp < -11.1111) /* below 12 °F, low snow density */
452 val *= 12;
453 else if (temp < -4.4444) /* 12 to 24 °F, still low density */
454 val *= 10;
455 else if (temp < -2.2222) /* 24 to 28 °F, more density */
456 val *= 7;
457 else if (temp < -0.5556) /* 28 to 31 °F, wet, dense, melting */
458 val *= 5;
459 else /* anything above 31 °F */
460 val *= 3;
461 }
462
463 if (units->precipitation == INCHES) {
464 val /= 25.4;
465 return g_strdup_printf("%.2f", val);
466 } else
467 return g_strdup_printf("%.1f", val);
468
469 case SYMBOL:
470 return CHK_NULL(loc->symbol);
471 }
472
473 return g_strdup("");
474 }
475
476
477 const gchar *
get_unit(const units_config * units,const data_types type)478 get_unit(const units_config *units,
479 const data_types type)
480 {
481 if (units == NULL)
482 return "";
483
484 switch (type) {
485 case ALTITUDE:
486 return (units->altitude == FEET) ? _("ft") : _("m");
487 case TEMPERATURE:
488 case DEWPOINT:
489 case APPARENT_TEMPERATURE:
490 return (units->temperature == FAHRENHEIT) ? _("°F") : _("°C");
491 case PRESSURE:
492 switch (units->pressure) {
493 case HECTOPASCAL:
494 return _("hPa");
495 case INCH_MERCURY:
496 return _("inHg");
497 case PSI:
498 return _("psi");
499 case TORR:
500 return _("mmHg");
501 }
502 break;
503 case WIND_SPEED:
504 switch (units->windspeed) {
505 case KMH:
506 return _("km/h");
507 case MPH:
508 return _("mph");
509 case MPS:
510 return _("m/s");
511 case FTS:
512 return _("ft/s");
513 case KNOTS:
514 return _("kt");
515 }
516 break;
517 case WIND_DIRECTION_DEG:
518 case LATITUDE:
519 case LONGITUDE:
520 /* TRANSLATORS: The degree sign is used like a unit for
521 latitude, longitude, wind direction */
522 return _("°");
523 case HUMIDITY:
524 case CLOUDS_LOW:
525 case CLOUDS_MID:
526 case CLOUDS_HIGH:
527 case CLOUDINESS:
528 case FOG:
529 /* TRANSLATORS: Percentage sign is used like a unit for
530 clouds, fog, humidity */
531 return _("%");
532 case PRECIPITATION:
533 return (units->precipitation == INCHES) ? _("in") : _("mm");
534 case SYMBOL:
535 case WIND_BEAUFORT:
536 case WIND_DIRECTION:
537 return "";
538 }
539 return "";
540 }
541
542
543 /*
544 * Find out whether it's night or day.
545 *
546 * Either use the exact times for sunrise and sunset if
547 * available, or fallback to reasonable arbitrary values.
548 */
549 gboolean
is_night_time(const xml_astro * astro)550 is_night_time(const xml_astro *astro)
551 {
552 time_t now_t;
553 struct tm now_tm;
554
555 time(&now_t);
556
557 if (G_LIKELY(astro)) {
558 if (astro->sun_never_rises || astro->sun_never_sets){
559 /* Polar night */
560 if (astro->solarnoon_elevation <= 0)
561 return TRUE;
562 /* Polar day */
563 if (astro->solarmidnight_elevation > 0)
564 return FALSE;
565 }
566
567 /* Sunrise and sunset are known */
568 if (difftime(astro->sunrise, now_t) > 0)
569 return TRUE;
570
571 if (difftime(astro->sunset, now_t) <= 0)
572 return TRUE;
573
574 return FALSE;
575 }
576
577 /* no astrodata available, use fallback values */
578 now_tm = *localtime(&now_t);
579 return (now_tm.tm_hour >= NIGHT_TIME_START ||
580 now_tm.tm_hour < NIGHT_TIME_END);
581 }
582
583
584 static void
calculate_symbol(xml_time * timeslice,gboolean current_conditions)585 calculate_symbol(xml_time *timeslice,
586 gboolean current_conditions)
587 {
588 xml_location *loc;
589 gdouble fog, cloudiness, precipitation;
590
591 g_assert(timeslice != NULL && timeslice->location != NULL);
592 if (G_UNLIKELY(timeslice == NULL || timeslice->location == NULL))
593 return;
594
595 loc = timeslice->location;
596
597 precipitation = string_to_double(loc->precipitation_value, 0);
598 if (precipitation > 0)
599 return;
600
601 /* do some modifications only if we're making a timeslice for
602 current conditions */
603 if (current_conditions) {
604 cloudiness =
605 string_to_double(loc->clouds_percent[CLOUDS_PERC_CLOUDINESS], 0);
606 if (cloudiness >= 90)
607 loc->symbol_id = SYMBOL_CLOUD;
608 else if (cloudiness >= 30)
609 loc->symbol_id = SYMBOL_PARTLYCLOUD;
610 else if (cloudiness >= 1.0 / 8.0)
611 loc->symbol_id = SYMBOL_LIGHTCLOUD;
612 }
613
614 fog = string_to_double(loc->fog_percent, 0);
615 if (fog >= 80)
616 loc->symbol_id = SYMBOL_FOG;
617
618 /* update symbol name */
619 g_free(loc->symbol);
620 loc->symbol = g_strdup(get_symbol_name(loc->symbol_id));
621 }
622
623
624 /*
625 * Interpolate data for a certain time in a given interval
626 */
627 static gdouble
interpolate_value(gdouble value_start,gdouble value_end,time_t start_t,time_t end_t,time_t between_t)628 interpolate_value(gdouble value_start,
629 gdouble value_end,
630 time_t start_t,
631 time_t end_t,
632 time_t between_t)
633 {
634 gdouble total, part, ratio, delta, result;
635
636 /* calculate durations from start to end and start to between */
637 total = difftime(end_t, start_t);
638 part = difftime(between_t, start_t);
639
640 /* calculate ratio of these durations */
641 ratio = part / total;
642
643 /* now how big is that change? */
644 delta = (value_end - value_start) * ratio;
645
646 /* apply change and return corresponding value for between_t */
647 result = value_start + delta;
648 return result;
649 }
650
651
652 /*
653 * convert gchar in a gdouble and interpolate the value
654 */
655 static gchar *
interpolate_gchar_value(gchar * value_start,gchar * value_end,time_t start_t,time_t end_t,time_t between_t,gboolean radian)656 interpolate_gchar_value(gchar *value_start,
657 gchar *value_end,
658 time_t start_t,
659 time_t end_t,
660 time_t between_t,
661 gboolean radian)
662 {
663 gdouble val_start, val_end, val_result;
664
665 if (G_UNLIKELY(value_end == NULL))
666 return NULL;
667
668 if (value_start == NULL)
669 return g_strdup(value_end);
670
671 val_start = string_to_double(value_start, 0);
672 val_end = string_to_double(value_end, 0);
673
674 if (radian) {
675 if (val_end > val_start && val_end - val_start > 180)
676 val_start += 360;
677 else if (val_start > val_end && val_start - val_end > 180)
678 val_end += 360;
679 }
680
681 val_result = interpolate_value(val_start, val_end,
682 start_t, end_t, between_t);
683 if (radian && val_result >= 360)
684 val_result -= 360;
685
686 weather_debug("Interpolated data: start=%f, end=%f, result=%f",
687 val_start, val_end, val_result);
688 return double_to_string(val_result, "%.1f");
689 }
690
691
692 /* Create a new combined timeslice, with optionally interpolated data */
693 static xml_time *
make_combined_timeslice(xml_weather * wd,const xml_time * interval,const time_t * between_t,gboolean current_conditions)694 make_combined_timeslice(xml_weather *wd,
695 const xml_time *interval,
696 const time_t *between_t,
697 gboolean current_conditions)
698 {
699 xml_time *comb, *start, *end;
700 gboolean ipol = (between_t != NULL) ? TRUE : FALSE;
701 gint i;
702
703 /* find point data at start of interval (may not be available) */
704 start = get_timeslice(wd, interval->start, interval->start, NULL);
705
706 /* find point interval at end of interval */
707 end = get_timeslice(wd, interval->end, interval->end, NULL);
708
709 if (start == NULL && end == NULL)
710 return NULL;
711
712 /* create new timeslice to hold our copy */
713 comb = g_slice_new0(xml_time);
714 if (comb == NULL)
715 return NULL;
716
717 comb->location = g_slice_new0(xml_location);
718 if (comb->location == NULL) {
719 g_slice_free(xml_time, comb);
720 return NULL;
721 }
722
723 /* do not interpolate if no point data available at start of interval */
724 if (start == NULL) {
725 comb->point = end->start;
726 start = end;
727 } else if (ipol) {
728 /* deal with timeslices that are in the near future and use point
729 data available at the start of the interval */
730 if (difftime(*between_t, start->start) <= 0)
731 comb->point = start->start;
732 else
733 comb->point = *between_t;
734 }
735
736 comb->start = interval->start;
737 comb->end = interval->end;
738
739 COMB_END_COPY(altitude);
740 COMB_END_COPY(latitude);
741 COMB_END_COPY(longitude);
742
743 INTERPOLATE_OR_COPY(temperature_value, FALSE);
744 COMB_END_COPY(temperature_unit);
745
746 INTERPOLATE_OR_COPY(wind_dir_deg, TRUE);
747 comb->location->wind_dir_name =
748 g_strdup(wind_dir_name_by_deg(comb->location->wind_dir_deg, FALSE));
749
750 INTERPOLATE_OR_COPY(wind_speed_mps, FALSE);
751 INTERPOLATE_OR_COPY(wind_speed_beaufort, FALSE);
752 INTERPOLATE_OR_COPY(humidity_value, FALSE);
753 COMB_END_COPY(humidity_unit);
754
755 INTERPOLATE_OR_COPY(pressure_value, FALSE);
756 COMB_END_COPY(pressure_unit);
757
758 for (i = 0; i < CLOUDS_PERC_NUM; i++)
759 INTERPOLATE_OR_COPY(clouds_percent[i], FALSE);
760
761 INTERPOLATE_OR_COPY(fog_percent, FALSE);
762
763 /* it makes no sense to interpolate the following (interval) values */
764 comb->location->precipitation_value =
765 g_strdup(interval->location->precipitation_value);
766 comb->location->precipitation_unit =
767 g_strdup(interval->location->precipitation_unit);
768
769 comb->location->symbol_id = interval->location->symbol_id;
770 comb->location->symbol = g_strdup(interval->location->symbol);
771
772 calculate_symbol(comb, current_conditions);
773 return comb;
774 }
775
776
777 void
merge_astro(GArray * astrodata,const xml_astro * astro)778 merge_astro(GArray *astrodata,
779 const xml_astro *astro)
780 {
781 xml_astro *old_astro, *new_astro;
782 guint index;
783
784 g_assert(astrodata != NULL);
785 if (G_UNLIKELY(astrodata == NULL))
786 return;
787
788 /* copy astro, as it may be deleted by the calling function */
789 new_astro = xml_astro_copy(astro);
790
791 /* check for and replace existing astrodata of the same date */
792 if ((old_astro = get_astro(astrodata, astro->day, &index))) {
793 xml_astro_free(old_astro);
794 g_array_remove_index(astrodata, index);
795 g_array_insert_val(astrodata, index, new_astro);
796 weather_debug("Replaced existing astrodata at %d.", index);
797 } else {
798 g_array_append_val(astrodata, new_astro);
799 weather_debug("Appended new astrodata to the existing data.");
800 }
801 }
802
803
804 void
merge_timeslice(xml_weather * wd,const xml_time * timeslice)805 merge_timeslice(xml_weather *wd,
806 const xml_time *timeslice)
807 {
808 xml_time *old_ts, *new_ts;
809 time_t now_t = time(NULL);
810 guint index;
811
812 g_assert(wd != NULL);
813 if (G_UNLIKELY(wd == NULL))
814 return;
815
816 /* first check if it isn't too old */
817 if (difftime(now_t, timeslice->end) > DATA_EXPIRY_TIME) {
818 weather_debug("Not merging timeslice because it has expired.");
819 return;
820 }
821
822 /* Copy timeslice, as it will be deleted by the calling function */
823 new_ts = xml_time_copy(timeslice);
824
825 /* check if there is a timeslice with the same interval and
826 replace it with the current data */
827 old_ts = get_timeslice(wd, timeslice->start, timeslice->end, &index);
828 if (old_ts) {
829 xml_time_free(old_ts);
830 g_array_remove_index(wd->timeslices, index);
831 g_array_insert_val(wd->timeslices, index, new_ts);
832 weather_debug("Replaced existing timeslice at %d.", index);
833 } else {
834 g_array_prepend_val(wd->timeslices, new_ts);
835 //weather_debug("Prepended timeslice to the existing timeslices.");
836 }
837 }
838
839
840 /* Return current weather conditions, or NULL if not available. */
841 xml_time *
get_current_conditions(const xml_weather * wd)842 get_current_conditions(const xml_weather *wd)
843 {
844 return wd ? wd->current_conditions : NULL;
845 }
846
847
848 time_t
time_calc(const struct tm time_tm,const gint year,const gint month,const gint day,const gint hour,const gint min,const gint sec)849 time_calc(const struct tm time_tm,
850 const gint year,
851 const gint month,
852 const gint day,
853 const gint hour,
854 const gint min,
855 const gint sec)
856 {
857 time_t result;
858 struct tm new_tm;
859
860 new_tm = time_tm;
861 new_tm.tm_isdst = -1;
862 if (year)
863 new_tm.tm_year += year;
864 if (month)
865 new_tm.tm_mon += month;
866 if (day)
867 new_tm.tm_mday += day;
868 if (hour)
869 new_tm.tm_hour += hour;
870 if (min)
871 new_tm.tm_min += min;
872 if (sec)
873 new_tm.tm_sec += sec;
874 result = mktime(&new_tm);
875 return result;
876 }
877
878
879 time_t
time_calc_hour(const struct tm time_tm,const gint hours)880 time_calc_hour(const struct tm time_tm,
881 const gint hours)
882 {
883 return time_calc(time_tm, 0, 0, 0, hours, 0, 0);
884 }
885
886
887 time_t
time_calc_day(const struct tm time_tm,const gint days)888 time_calc_day(const struct tm time_tm,
889 const gint days)
890 {
891 return time_calc(time_tm, 0, 0, days, 0, 0, 0);
892 }
893
894
895 /*
896 * Compare two xml_astro structs using their date (days) field.
897 */
898 gint
xml_astro_compare(gconstpointer a,gconstpointer b)899 xml_astro_compare(gconstpointer a,
900 gconstpointer b)
901 {
902 xml_astro *a1 = *(xml_astro **) a;
903 xml_astro *a2 = *(xml_astro **) b;
904
905 if (G_UNLIKELY(a1 == NULL && a2 == NULL))
906 return 0;
907 if (G_UNLIKELY(a1 == NULL))
908 return 1;
909 if (G_UNLIKELY(a2 == NULL))
910 return -1;
911
912 return (gint) difftime(a2->day, a1->day) * -1;
913 }
914
915
916 void
astrodata_clean(GArray * astrodata)917 astrodata_clean(GArray *astrodata)
918 {
919 xml_astro *astro;
920 time_t now_t = time(NULL);
921 guint i;
922
923 if (G_UNLIKELY(astrodata == NULL))
924 return;
925
926 for (i = 0; i < astrodata->len; i++) {
927 astro = g_array_index(astrodata, xml_astro *, i);
928 if (G_UNLIKELY(astro == NULL))
929 continue;
930 if (difftime(now_t, astro->day) >= 24 * 3600) {
931 weather_debug("Removing expired astrodata:");
932 weather_dump(weather_dump_astro, astro);
933 xml_astro_free(astro);
934 g_array_remove_index(astrodata, i--);
935 weather_debug("Remaining astrodata entries: %d", astrodata->len);
936 }
937 }
938 }
939
940
941 /*
942 * Compare two xml_time structs using their start and end times,
943 * returning the result as a qsort()-style comparison function (less
944 * than zero for first arg is less than second arg, zero for equal,
945 * greater zero if first arg is greater than second arg).
946 */
947 gint
xml_time_compare(gconstpointer a,gconstpointer b)948 xml_time_compare(gconstpointer a,
949 gconstpointer b)
950 {
951 xml_time *ts1 = *(xml_time **) a;
952 xml_time *ts2 = *(xml_time **) b;
953 gdouble diff;
954
955 if (G_UNLIKELY(ts1 == NULL && ts2 == NULL))
956 return 0;
957 if (G_UNLIKELY(ts1 == NULL))
958 return -1;
959 if (G_UNLIKELY(ts2 == NULL))
960 return 1;
961
962 diff = difftime(ts2->start, ts1->start);
963 if (diff != 0)
964 return (gint) (diff * -1);
965
966 /* start time is equal, now it's easy to check end time ;-) */
967 return (gint) (difftime(ts2->end, ts1->end) * -1);
968 }
969
970
971 static void
point_data_results_free(point_data_results * pdr)972 point_data_results_free(point_data_results *pdr)
973 {
974 g_assert(pdr != NULL);
975 if (G_UNLIKELY(pdr == NULL))
976 return;
977
978 g_assert(pdr->before != NULL);
979 g_array_free(pdr->before, FALSE);
980 g_assert(pdr->after != NULL);
981 g_array_free(pdr->after, FALSE);
982 g_slice_free(point_data_results, pdr);
983 return;
984 }
985
986 /*
987 * Given an array of point data, find two points for which
988 * corresponding interval data can be found so that the interval is as
989 * small as possible, returning NULL if such interval data doesn't
990 * exist.
991 */
992 static xml_time *
find_smallest_interval(xml_weather * wd,const point_data_results * pdr)993 find_smallest_interval(xml_weather *wd,
994 const point_data_results *pdr)
995 {
996 GArray *before = pdr->before, *after = pdr->after;
997 xml_time *ts_before, *ts_after, *found;
998 guint i, j;
999
1000 if (before->len == 0)
1001 return NULL;
1002
1003 for (i = before->len - 1; i > 0; i--) {
1004 ts_before = g_array_index(before, xml_time *, i);
1005 for (j = 0; j < after->len; j++) {
1006 ts_after = g_array_index(after, xml_time *, j);
1007 found = get_timeslice(wd, ts_before->start, ts_after->end, NULL);
1008 if (found)
1009 return found;
1010 }
1011 }
1012 return NULL;
1013 }
1014
1015
1016 static xml_time *
find_smallest_incomplete_interval(xml_weather * wd,time_t end_t)1017 find_smallest_incomplete_interval(xml_weather *wd,
1018 time_t end_t)
1019 {
1020 xml_time *timeslice, *found = NULL;
1021 guint i;
1022
1023 weather_debug("Searching for the smallest incomplete interval.");
1024 /* search for all timeslices with interval data that have end time end_t */
1025 for (i = 0; i < wd->timeslices->len; i++) {
1026 timeslice = g_array_index(wd->timeslices, xml_time *, i);
1027 if (timeslice && difftime(timeslice->end, end_t) == 0
1028 && difftime(timeslice->end, timeslice->start) != 0) {
1029 if (found == NULL)
1030 found = timeslice;
1031 else
1032 if (difftime(timeslice->start, found->start) > 0)
1033 found = timeslice;
1034 weather_dump(weather_dump_timeslice, found);
1035 }
1036 }
1037 weather_debug("Search result for smallest incomplete interval is:");
1038 weather_dump(weather_dump_timeslice, found);
1039 return found;
1040 }
1041
1042
1043 /* find point data within certain limits around a point in time */
1044 static point_data_results *
find_point_data(const xml_weather * wd,const time_t point_t,const gdouble min_diff,const gdouble max_diff)1045 find_point_data(const xml_weather *wd,
1046 const time_t point_t,
1047 const gdouble min_diff,
1048 const gdouble max_diff)
1049 {
1050 point_data_results *found;
1051 xml_time *timeslice;
1052 gdouble diff;
1053 guint i;
1054
1055 found = g_slice_new0(point_data_results);
1056 found->before = g_array_new(FALSE, TRUE, sizeof(xml_time *));
1057 found->after = g_array_new(FALSE, TRUE, sizeof(xml_time *));
1058
1059 weather_debug("Checking %d timeslices for point data.",
1060 wd->timeslices->len);
1061 for (i = 0; i < wd->timeslices->len; i++) {
1062 timeslice = g_array_index(wd->timeslices, xml_time *, i);
1063 /* look only for point data, not intervals */
1064 if (G_UNLIKELY(timeslice == NULL) || timeslice_is_interval(timeslice))
1065 continue;
1066
1067 /* add point data if within limits */
1068 diff = difftime(timeslice->end, point_t);
1069 if (diff <= 0) { /* before point_t */
1070 diff *= -1;
1071 if (diff < min_diff || diff > max_diff)
1072 continue;
1073 g_array_append_val(found->before, timeslice);
1074 weather_dump(weather_dump_timeslice, timeslice);
1075 } else { /* after point_t */
1076 if (diff < min_diff || diff > max_diff)
1077 continue;
1078 g_array_append_val(found->after, timeslice);
1079 weather_dump(weather_dump_timeslice, timeslice);
1080 }
1081 }
1082 g_array_sort(found->before, (GCompareFunc) xml_time_compare);
1083 g_array_sort(found->after, (GCompareFunc) xml_time_compare);
1084 found->point = point_t;
1085 weather_debug("Found %d timeslices with point data, "
1086 "%d before and %d after point_t.",
1087 (found->before->len + found->after->len),
1088 found->before->len, found->after->len);
1089 return found;
1090 }
1091
1092
1093 xml_time *
make_current_conditions(xml_weather * wd,time_t now_t)1094 make_current_conditions(xml_weather *wd,
1095 time_t now_t)
1096 {
1097 point_data_results *found = NULL;
1098 xml_time *interval = NULL, *incomplete;
1099 struct tm point_tm = *localtime(&now_t);
1100 time_t point_t = now_t;
1101 gint i = 0;
1102
1103 g_assert(wd != NULL);
1104 if (G_UNLIKELY(wd == NULL))
1105 return NULL;
1106
1107 /* there may not be a timeslice available for the current
1108 interval, so look max three hours ahead */
1109 while (i < 3 && interval == NULL) {
1110 point_t = time_calc_hour(point_tm, i);
1111 found = find_point_data(wd, point_t, 1, 4 * 3600);
1112 interval = find_smallest_interval(wd, found);
1113 point_data_results_free(found);
1114
1115 /* There may be interval data where point data is only
1116 available at the end of that interval. If such an interval
1117 exists, use it, it's still better than the next one where
1118 now_t is not in between. */
1119 if (interval && difftime(interval->start, now_t) > 0)
1120 if ((incomplete =
1121 find_smallest_incomplete_interval(wd, interval->start)))
1122 interval = incomplete;
1123 point_tm = *localtime(&point_t);
1124 i++;
1125 }
1126 weather_dump(weather_dump_timeslice, interval);
1127 if (interval == NULL)
1128 return NULL;
1129
1130 return make_combined_timeslice(wd, interval, &now_t, TRUE);
1131 }
1132
1133
1134 /*
1135 * Add days to time_t and set the calculated day to midnight.
1136 */
1137 time_t
day_at_midnight(time_t day_t,const gint add_days)1138 day_at_midnight(time_t day_t,
1139 const gint add_days)
1140 {
1141 struct tm day_tm;
1142
1143 day_tm = *localtime(&day_t);
1144 day_tm.tm_mday += add_days;
1145 day_tm.tm_hour = day_tm.tm_min = day_tm.tm_sec = 0;
1146 day_tm.tm_isdst = -1;
1147 day_t = mktime(&day_tm);
1148 return day_t;
1149 }
1150
1151
1152 /*
1153 * Returns astro data for a given day.
1154 */
1155 xml_astro *
get_astro_data_for_day(const GArray * astrodata,const gint day)1156 get_astro_data_for_day(const GArray *astrodata,
1157 const gint day)
1158 {
1159 xml_astro *astro;
1160 time_t day_t = time(NULL);
1161 guint i;
1162
1163 if (G_UNLIKELY(astrodata == NULL))
1164 return NULL;
1165
1166 day_t = day_at_midnight(day_t, day);
1167
1168 for (i = 0; i < astrodata->len; i++) {
1169 astro = g_array_index(astrodata, xml_astro *, i);
1170 if (astro && (difftime(astro->day, day_t) == 0))
1171 return astro;
1172 }
1173
1174 return NULL;
1175 }
1176
1177
1178 /*
1179 * Get all point data relevant for a given day.
1180 */
1181 GArray *
get_point_data_for_day(xml_weather * wd,gint day)1182 get_point_data_for_day(xml_weather *wd,
1183 gint day)
1184 {
1185 GArray *found;
1186 xml_time *timeslice;
1187 time_t day_t = time(NULL);
1188 guint i;
1189
1190 day_t = day_at_midnight(day_t, day);
1191
1192 /* loop over weather data and pick relevant point data */
1193 found = g_array_new(FALSE, TRUE, sizeof(xml_time *));
1194 g_assert(found != NULL);
1195 if (G_UNLIKELY(found == NULL))
1196 return NULL;
1197
1198 weather_debug("Checking %d timeslices for point data relevant to day %d.",
1199 wd->timeslices->len, day);
1200 for (i = 0; i < wd->timeslices->len; i++) {
1201 timeslice = g_array_index(wd->timeslices, xml_time *, i);
1202
1203 /* look only for point data, not intervals */
1204 if (G_UNLIKELY(timeslice == NULL) || timeslice_is_interval(timeslice))
1205 continue;
1206
1207 if (difftime(timeslice->start, day_t) >= DAY_START * 3600 &&
1208 difftime(timeslice->end, day_t) <= DAY_END * 3600) {
1209 weather_dump(weather_dump_timeslice, timeslice);
1210 g_array_append_val(found, timeslice);
1211 }
1212 }
1213 g_array_sort(found, (GCompareFunc) xml_time_compare);
1214 weather_debug("Found %d timeslices for day %d.", found->len, day);
1215 return found;
1216 }
1217
1218
1219 /*
1220 * Return forecast data for a given daytime, using the data provided.
1221 */
1222 xml_time *
make_forecast_data(xml_weather * wd,GArray * daydata,gint day,daytime dt)1223 make_forecast_data(xml_weather *wd,
1224 GArray *daydata,
1225 gint day,
1226 daytime dt)
1227 {
1228 xml_time *ts1, *ts2, *interval = NULL;
1229 struct tm point_tm, start_tm, end_tm, tm1, tm2;
1230 time_t point_t, start_t, end_t;
1231 gint min = 0, max = 0, point = 0;
1232 guint i, j;
1233
1234 g_assert(wd != NULL);
1235 if (G_UNLIKELY(wd == NULL))
1236 return NULL;
1237
1238 g_assert(daydata != NULL);
1239 if (G_UNLIKELY(daydata == NULL))
1240 return NULL;
1241
1242 /* choose search interval and desired point in time depending on daytime */
1243 switch (dt) {
1244 case MORNING:
1245 min = 3;
1246 max = 15;
1247 point = 9;
1248 break;
1249 case AFTERNOON:
1250 min = 9;
1251 max = 21;
1252 point = 15;
1253 break;
1254 case EVENING:
1255 min = 15;
1256 max = 27;
1257 point = 21;
1258 break;
1259 case NIGHT:
1260 min = 21;
1261 max = 33;
1262 point = 27;
1263 break;
1264 }
1265
1266 /* initialize times to the current day */
1267 time(&point_t);
1268 start_tm = end_tm = point_tm = *localtime(&point_t);
1269
1270 /* calculate daytime limits for the requested day */
1271 point_tm.tm_mday += day;
1272 point_tm.tm_hour = point;
1273 point_tm.tm_min = point_tm.tm_sec = 0;
1274 point_tm.tm_isdst = -1;
1275 point_t = mktime(&point_tm);
1276
1277 start_tm.tm_mday += day;
1278 start_tm.tm_hour = min;
1279 start_tm.tm_min = start_tm.tm_sec = 0;
1280 start_tm.tm_isdst = -1;
1281 start_t = mktime(&start_tm);
1282
1283 end_tm.tm_mday += day;
1284 end_tm.tm_hour = max;
1285 end_tm.tm_min = end_tm.tm_sec = 0;
1286 end_tm.tm_isdst = -1;
1287 end_t = mktime(&end_tm);
1288
1289 /* using search criteria, find an appropriate interval */
1290 for (i = 0; i < daydata->len; i++) {
1291 weather_debug("checking start ts %d", i);
1292
1293 /* try start timeslice for interval */
1294 ts1 = g_array_index(daydata, xml_time *, i);
1295
1296 if (G_UNLIKELY(ts1 == NULL))
1297 continue;
1298 weather_debug("start ts is not null");
1299
1300 /* start timeslice needs to be within max daytime interval */
1301 if (difftime(ts1->start, start_t) < 0 ||
1302 difftime(end_t, ts1->start) < 0)
1303 continue;
1304 weather_debug("start ts is in max daytime interval");
1305
1306 /* start timeslice needs to start at 0, 6, 12, or 18 hours UTC time */
1307 tm1 = *gmtime(&ts1->start);
1308 if (tm1.tm_hour != 0 && tm1.tm_hour % 6 != 0)
1309 continue;
1310 weather_debug("start ts does start at 0, 6, 12, 18 hour UTC time");
1311
1312 for (j = 0; j < daydata->len; j++) {
1313 weather_debug("checking end ts %d", j);
1314
1315 /* find end timeslice for interval */
1316 ts2 = g_array_index(daydata, xml_time *, j);
1317
1318 if (G_UNLIKELY(ts2 == NULL))
1319 continue;
1320 weather_debug("end ts is not null");
1321
1322 /* end timeslice has to be different from the start timeslice */
1323 if (ts1 == ts2)
1324 continue;
1325 weather_debug("start ts is different from end ts");
1326
1327 /* end timeslice needs to be after start timeslice */
1328 if (difftime(ts2->start, ts1->start) <= 0)
1329 continue;
1330 weather_debug("start ts is before end ts");
1331
1332 /* end timeslice needs to be in max daytime interval */
1333 if (difftime(ts2->start, start_t) < 0 ||
1334 difftime(end_t, ts2->start) < 0)
1335 continue;
1336 weather_debug("end ts is in max daytime interval");
1337
1338 /* end timeslice needs to start at 0, 6, 12, or 18 hours UTC time */
1339 tm2 = *gmtime(&ts2->start);
1340 if (tm2.tm_hour != 0 && tm2.tm_hour % 6 != 0)
1341 continue;
1342 weather_debug("end ts does start at 0, 6, 12, 18 hour UTC time");
1343
1344 /* start and end timeslice need to be a 6 hours interval... */
1345 if (difftime(ts2->start, ts1->start) != DAYTIME_LEN * 3600)
1346 /* ...however we may need to take into account possible dst
1347 difference so let's also try DAYTIME_LEN ±1 hour */
1348 if ((difftime(ts2->start, ts1->start) < (DAYTIME_LEN - 1) * 3600 ||
1349 difftime(ts2->start, ts1->start) > (DAYTIME_LEN + 1) * 3600) &&
1350 get_timeslice(wd, ts1->start, ts2->end, NULL) == NULL)
1351 continue;
1352 weather_debug("start and end ts are 6 hours apart");
1353
1354 /* daytime point needs to be within the interval */
1355 if (difftime(point_t, ts1->start) < 0 ||
1356 difftime(ts2->start, point_t) < 0)
1357 continue;
1358 weather_debug("daytime point is within the found interval");
1359
1360 /* check whether the desired interval exists */
1361 interval = get_timeslice(wd, ts1->start, ts2->end, NULL);
1362 if (interval == NULL)
1363 continue;
1364
1365 /* make and return a combined interval with interpolated data */
1366 weather_debug("returning valid interval");
1367 return make_combined_timeslice(wd, interval, &point_t, FALSE);
1368 }
1369 }
1370
1371 /* Finding a 6 hours daytime interval failed; maybe current time
1372 is within this interval and therefore that 6 hours interval is
1373 not available anymore. In that case, simply trying the current
1374 conditions interval is better than nothing. */
1375 if (wd->current_conditions &&
1376 difftime(wd->current_conditions->start, start_t) >= 0 &&
1377 difftime(end_t, wd->current_conditions->end) >= 0) {
1378 interval = get_timeslice(wd, wd->current_conditions->start,
1379 wd->current_conditions->end, NULL);
1380 weather_debug("returning current conditions interval for daytime %d "
1381 "of day %d", dt, day);
1382 return make_combined_timeslice(wd, interval,
1383 &wd->current_conditions->point, FALSE);
1384 }
1385
1386 /* If we got as far as this, it's very unlikely we find a smaller
1387 interval, so just give up. */
1388 weather_debug("no forecast data for daytime %d of day %d", dt, day);
1389 return NULL;
1390 }
1391