diff --git a/src/hourlyforecastbox.cc b/src/hourlyforecastbox.cc new file mode 100644 index 0000000..af509f9 --- /dev/null +++ b/src/hourlyforecastbox.cc @@ -0,0 +1,279 @@ +#include "hourlyforecastbox.h" + +#include +#include +#include +#include + +#include "events.h" +#include "units.h" +#include "util.h" + +namespace taranis { + +HourlyForecastBox::HourlyForecastBox(int pos_x, int pos_y, int width, + int height, std::shared_ptr model, + std::shared_ptr icons, + std::shared_ptr fonts) + : Widget{pos_x, pos_y, width, height}, model{model}, icons{icons}, + fonts{fonts} { + this->visible_bars = 8; + + this->bar_width = static_cast(std::ceil(width / this->visible_bars)); + + this->bars_height = this->bounding_box.h; + + this->frame_start_x = this->bounding_box.x; + this->frame_start_y = this->bounding_box.y; + + auto normal_font = this->fonts->get_normal_font(); + auto small_bold_font = this->fonts->get_small_bold_font(); + auto tiny_font = this->fonts->get_tiny_font(); + + this->time_y = this->frame_start_y + this->vertical_padding / 2; + this->icon_y = this->time_y + tiny_font->height; + this->temperature_y = this->frame_start_y + this->bars_height - + this->vertical_padding / 2 - normal_font->height - + 2 * tiny_font->height; + this->wind_speed_y = this->temperature_y + normal_font->height; + this->humidity_y = this->wind_speed_y + tiny_font->height; + + this->curve_y_offset = this->temperature_y - this->vertical_padding; + this->curve_height = this->temperature_y - this->icon_y - this->icon_size - + 2 * this->vertical_padding; +} + +void HourlyForecastBox::show() { + this->fill_bounding_box(); + + this->draw_frame_and_values(); + this->draw_precipitation_histogram(); + this->draw_temperature_curve(); +} + +int HourlyForecastBox::handle_key_pressed(int key) { + if (key == IV_KEY_PREV) { + this->decrease_forecast_offset(); + return 1; + } + + if (key == IV_KEY_NEXT) { + this->increase_forecast_offset(); + return 1; + } + return 0; +} + +void HourlyForecastBox::increase_forecast_offset() { + const size_t max_forecast_offset{this->model->hourly_forecast.size() - + this->visible_bars}; + const auto updated_forecast_offset = + std::min(this->forecast_offset + this->visible_bars, max_forecast_offset); + if (updated_forecast_offset != this->forecast_offset) { + this->forecast_offset = updated_forecast_offset; + this->draw_and_update(); + } else { + this->forecast_offset = 0; + const auto event_handler = GetEventHandler(); + SendEvent(event_handler, EVT_CUSTOM, + CustomEvent::change_daily_forecast_display, 0); + } +} + +void HourlyForecastBox::decrease_forecast_offset() { + const size_t min_forecast_offset{0}; + const auto updated_forecast_offset = + (this->forecast_offset > this->visible_bars) + ? this->forecast_offset - this->visible_bars + : min_forecast_offset; + + // don't use std::max since we're working with unsigned integers! + + if (updated_forecast_offset != this->forecast_offset) { + this->forecast_offset = updated_forecast_offset; + this->draw_and_update(); + } else { + const auto event_handler = GetEventHandler(); + SendEvent(event_handler, EVT_CUSTOM, + CustomEvent::change_daily_forecast_display, 0); + } +} + +void HourlyForecastBox::draw_and_update() { + this->show(); + + PartialUpdate(this->bounding_box.x, this->bounding_box.y, + this->bounding_box.w, this->bounding_box.h); +} + +void HourlyForecastBox::draw_frame_and_values() const { + DrawLine(this->frame_start_x, this->frame_start_y, this->bounding_box.w, + this->frame_start_y, LGRAY); + DrawLine(this->frame_start_x, this->frame_start_y + this->bars_height, + this->bounding_box.w, this->frame_start_y + this->bars_height, + LGRAY); + + auto normal_font = this->fonts->get_normal_font(); + auto small_bold_font = this->fonts->get_small_bold_font(); + auto tiny_font = this->fonts->get_tiny_font(); + + const auto separator_start_y = this->frame_start_y; + const auto separator_stop_y = this->frame_start_y + this->bars_height; + + const Units units{this->model}; + + for (size_t bar_index = 0; bar_index < this->visible_bars; ++bar_index) { + const auto bar_center_x = (bar_index + 1.0 / 2) * this->bar_width; + + const auto forecast_index = this->forecast_offset + bar_index; + if (forecast_index < this->model->hourly_forecast.size()) { + const auto forecast = this->model->hourly_forecast[forecast_index]; + + SetFont(tiny_font.get(), BLACK); + + const auto time_text = format_time(forecast.date, true); + DrawString(bar_center_x - StringWidth(time_text.c_str()) / 2.0, + this->time_y, time_text.c_str()); + + DrawBitmap(bar_center_x - this->icon_size / 2.0, this->icon_y, + this->icons->get(forecast.weather_icon_name)); + + SetFont(small_bold_font.get(), BLACK); + + const auto temperature_text = + units.format_temperature(forecast.temperature); + DrawString(bar_center_x - StringWidth(temperature_text.c_str()) / 2.0, + this->temperature_y, temperature_text.c_str()); + + SetFont(tiny_font.get(), DGRAY); + + const auto wind_speed_text = units.format_speed(forecast.wind_speed); + DrawString(bar_center_x - StringWidth(wind_speed_text.c_str()) / 2.0, + this->wind_speed_y, wind_speed_text.c_str()); + + const auto humidity_text = + std::to_string(static_cast(forecast.humidity)) + "%"; + DrawString(bar_center_x - StringWidth(humidity_text.c_str()) / 2.0, + this->humidity_y, humidity_text.c_str()); + } + + if (bar_index < this->visible_bars - 1) { + const auto separator_x = (bar_index + 1) * this->bar_width; + DrawLine(separator_x, separator_start_y, separator_x, separator_stop_y, + LGRAY); + } + } +} + +void HourlyForecastBox::draw_temperature_curve() const { + const auto ya = + normalize_temperatures(this->model->hourly_forecast, this->curve_height); + if (ya.size() < gsl_interp_type_min_size(gsl_interp_cspline)) { + return; + } + + const auto step = this->bar_width; + std::vector xa; + xa.reserve(ya.size()); + for (size_t index = 0; index < ya.size(); ++index) { + xa.push_back(index * step + step / 2.0); + } + + std::unique_ptr accelerator{ + gsl_interp_accel_alloc(), &gsl_interp_accel_free}; + std::unique_ptr state{ + gsl_interp_alloc(gsl_interp_cspline, xa.size()), &gsl_interp_free}; + + const auto error = + gsl_interp_init(state.get(), xa.data(), ya.data(), xa.size()); + if (error) { + // TODO log + return; + } + + const auto width = this->bounding_box.w; + for (int x_screen = this->bounding_box.x; x_screen < width; ++x_screen) { + const double x = + this->forecast_offset * step + (x_screen - this->bounding_box.x); + if (x < xa.front() or x > xa.back()) { + continue; + } + double y; + const auto error = gsl_interp_eval_e(state.get(), xa.data(), ya.data(), x, + accelerator.get(), &y); + if (error) { + // TODO log + break; + } + + const auto y_screen = this->curve_y_offset - y; + DrawPixel(x_screen, y_screen, BLACK); + DrawPixel(x_screen, y_screen + 1, BLACK); + DrawPixel(x_screen, y_screen + 2, DGRAY); + DrawPixel(x_screen, y_screen + 3, DGRAY); + } +} + +void HourlyForecastBox::draw_precipitation_histogram() const { + auto tiny_font = this->fonts->get_tiny_font(); + const auto min_bars_height = tiny_font.get()->height + 10; + // 2 pixels of padding in bar + const auto max_bars_height = this->curve_height - tiny_font.get()->height; + // 1 pixel of padding between bar top and + // probability_of_precipitation text + + if (max_bars_height <= min_bars_height) { + return; + } + + const auto normalized_precipitations = normalize_precipitations( + this->model->hourly_forecast, min_bars_height, max_bars_height); + + const Units units{this->model}; + + SetFont(tiny_font.get(), DGRAY); + + for (size_t bar_index = 0; bar_index < this->visible_bars; ++bar_index) { + const auto forecast_index = this->forecast_offset + bar_index; + if (forecast_index < this->model->hourly_forecast.size()) { + const auto forecast = this->model->hourly_forecast[forecast_index]; + if (std::isnan(forecast.rain)) { + continue; + } + const auto bar_center_x = (bar_index + 1.0 / 2) * this->bar_width; + const auto x_screen = this->bounding_box.x + bar_index * this->bar_width; + const auto bar_height = normalized_precipitations.at(forecast_index); + const auto y_screen = this->curve_y_offset - bar_height; + FillArea(x_screen, y_screen, this->bar_width, bar_height, LGRAY); + DrawRect(x_screen, y_screen, this->bar_width, bar_height, 0x777777); + + const auto precipitation_text = units.format_precipitation( + max_number(forecast.rain, forecast.snow), false); + // rain and snow are in mm/h but save space with shortest unit + + SetFont(tiny_font.get(), DGRAY); + DrawString(bar_center_x - StringWidth(precipitation_text.c_str()) / 2.0, + y_screen - tiny_font.get()->height - 2, + precipitation_text.c_str()); + + const auto probability_of_precipitation = + forecast.probability_of_precipitation; + if (not std::isnan(probability_of_precipitation)) { + + const auto probability_of_precipitation_text = + std::to_string( + static_cast(probability_of_precipitation * 100)) + + "%"; + + SetFont(tiny_font.get(), BLACK); + DrawString(bar_center_x - + StringWidth(probability_of_precipitation_text.c_str()) / + 2.0, + this->curve_y_offset - tiny_font.get()->height - 2, + probability_of_precipitation_text.c_str()); + } + } + } +} + +} // namespace taranis diff --git a/src/hourlyforecastbox.h b/src/hourlyforecastbox.h index a482f12..b8a18ae 100644 --- a/src/hourlyforecastbox.h +++ b/src/hourlyforecastbox.h @@ -1,20 +1,10 @@ #pragma once -#pragma once - -#include -#include -#include -#include #include -#include -#include "events.h" #include "fonts.h" #include "icons.h" #include "model.h" -#include "units.h" -#include "util.h" #include "widget.h" namespace taranis { @@ -23,99 +13,24 @@ class HourlyForecastBox : public Widget { public: HourlyForecastBox(int pos_x, int pos_y, int width, int height, std::shared_ptr model, std::shared_ptr icons, - std::shared_ptr fonts) - : Widget{pos_x, pos_y, width, height}, model{model}, icons{icons}, - fonts{fonts} { - this->visible_bars = 8; - - this->bar_width = static_cast(std::ceil(width / this->visible_bars)); - - this->bars_height = this->bounding_box.h; - - this->frame_start_x = this->bounding_box.x; - this->frame_start_y = this->bounding_box.y; - - auto normal_font = this->fonts->get_normal_font(); - auto small_bold_font = this->fonts->get_small_bold_font(); - auto tiny_font = this->fonts->get_tiny_font(); - - this->time_y = this->frame_start_y + this->vertical_padding / 2; - this->icon_y = this->time_y + tiny_font->height; - this->temperature_y = this->frame_start_y + this->bars_height - - this->vertical_padding / 2 - normal_font->height - - 2 * tiny_font->height; - this->wind_speed_y = this->temperature_y + normal_font->height; - this->humidity_y = this->wind_speed_y + tiny_font->height; - - this->curve_y_offset = this->temperature_y - this->vertical_padding; - this->curve_height = this->temperature_y - this->icon_y - this->icon_size - - 2 * this->vertical_padding; - } - - void show() override { - this->fill_bounding_box(); - - this->draw_frame_and_values(); - this->draw_precipitation_histogram(); - this->draw_temperature_curve(); - } - - int handle_key_pressed(int key) override { - if (key == IV_KEY_PREV) { - this->decrease_forecast_offset(); - return 1; - } + std::shared_ptr fonts); - if (key == IV_KEY_NEXT) { - this->increase_forecast_offset(); - return 1; - } - return 0; - } + void show() override; - void increase_forecast_offset() { - const size_t max_forecast_offset{this->model->hourly_forecast.size() - - this->visible_bars}; - const auto updated_forecast_offset = std::min( - this->forecast_offset + this->visible_bars, max_forecast_offset); - if (updated_forecast_offset != this->forecast_offset) { - this->forecast_offset = updated_forecast_offset; - this->draw_and_update(); - } else { - this->forecast_offset = 0; - const auto event_handler = GetEventHandler(); - SendEvent(event_handler, EVT_CUSTOM, - CustomEvent::change_daily_forecast_display, 0); - } - } + int handle_key_pressed(int key) override; - void decrease_forecast_offset() { - const size_t min_forecast_offset{0}; - const auto updated_forecast_offset = - (this->forecast_offset > this->visible_bars) - ? this->forecast_offset - this->visible_bars - : min_forecast_offset; + void increase_forecast_offset(); - // don't use std::max since we're working with unsigned integers! - - if (updated_forecast_offset != this->forecast_offset) { - this->forecast_offset = updated_forecast_offset; - this->draw_and_update(); - } else { - const auto event_handler = GetEventHandler(); - SendEvent(event_handler, EVT_CUSTOM, - CustomEvent::change_daily_forecast_display, 0); - } - } + void decrease_forecast_offset(); private: + static constexpr int vertical_padding{25}; + static constexpr int icon_size{100}; + std::shared_ptr model; std::shared_ptr icons; std::shared_ptr fonts; - const int vertical_padding{25}; - const int icon_size{100}; - size_t visible_bars; int bar_width; @@ -134,183 +49,13 @@ class HourlyForecastBox : public Widget { size_t forecast_offset{0}; - void draw_and_update() { - this->show(); - - PartialUpdate(this->bounding_box.x, this->bounding_box.y, - this->bounding_box.w, this->bounding_box.h); - } - - void draw_frame_and_values() const { - DrawLine(this->frame_start_x, this->frame_start_y, this->bounding_box.w, - this->frame_start_y, LGRAY); - DrawLine(this->frame_start_x, this->frame_start_y + this->bars_height, - this->bounding_box.w, this->frame_start_y + this->bars_height, - LGRAY); - - auto normal_font = this->fonts->get_normal_font(); - auto small_bold_font = this->fonts->get_small_bold_font(); - auto tiny_font = this->fonts->get_tiny_font(); - - const auto separator_start_y = this->frame_start_y; - const auto separator_stop_y = this->frame_start_y + this->bars_height; - - const Units units{this->model}; - - for (size_t bar_index = 0; bar_index < this->visible_bars; ++bar_index) { - const auto bar_center_x = (bar_index + 1.0 / 2) * this->bar_width; - - const auto forecast_index = this->forecast_offset + bar_index; - if (forecast_index < this->model->hourly_forecast.size()) { - const auto forecast = this->model->hourly_forecast[forecast_index]; - - SetFont(tiny_font.get(), BLACK); - - const auto time_text = format_time(forecast.date, true); - DrawString(bar_center_x - StringWidth(time_text.c_str()) / 2.0, - this->time_y, time_text.c_str()); - - DrawBitmap(bar_center_x - this->icon_size / 2.0, this->icon_y, - this->icons->get(forecast.weather_icon_name)); - - SetFont(small_bold_font.get(), BLACK); - - const auto temperature_text = - units.format_temperature(forecast.temperature); - DrawString(bar_center_x - StringWidth(temperature_text.c_str()) / 2.0, - this->temperature_y, temperature_text.c_str()); - - SetFont(tiny_font.get(), DGRAY); - - const auto wind_speed_text = units.format_speed(forecast.wind_speed); - DrawString(bar_center_x - StringWidth(wind_speed_text.c_str()) / 2.0, - this->wind_speed_y, wind_speed_text.c_str()); - - const auto humidity_text = - std::to_string(static_cast(forecast.humidity)) + "%"; - DrawString(bar_center_x - StringWidth(humidity_text.c_str()) / 2.0, - this->humidity_y, humidity_text.c_str()); - } - - if (bar_index < this->visible_bars - 1) { - const auto separator_x = (bar_index + 1) * this->bar_width; - DrawLine(separator_x, separator_start_y, separator_x, separator_stop_y, - LGRAY); - } - } - } - - void draw_temperature_curve() const { - const auto ya = normalize_temperatures(this->model->hourly_forecast, - this->curve_height); - if (ya.size() < gsl_interp_type_min_size(gsl_interp_cspline)) { - return; - } - - const auto step = this->bar_width; - std::vector xa; - xa.reserve(ya.size()); - for (size_t index = 0; index < ya.size(); ++index) { - xa.push_back(index * step + step / 2.0); - } - - std::unique_ptr accelerator{ - gsl_interp_accel_alloc(), &gsl_interp_accel_free}; - std::unique_ptr state{ - gsl_interp_alloc(gsl_interp_cspline, xa.size()), &gsl_interp_free}; - - const auto error = - gsl_interp_init(state.get(), xa.data(), ya.data(), xa.size()); - if (error) { - // TODO log - return; - } - - const auto width = this->bounding_box.w; - for (int x_screen = this->bounding_box.x; x_screen < width; ++x_screen) { - const double x = - this->forecast_offset * step + (x_screen - this->bounding_box.x); - if (x < xa.front() or x > xa.back()) { - continue; - } - double y; - const auto error = gsl_interp_eval_e(state.get(), xa.data(), ya.data(), x, - accelerator.get(), &y); - if (error) { - // TODO log - break; - } - - const auto y_screen = this->curve_y_offset - y; - DrawPixel(x_screen, y_screen, BLACK); - DrawPixel(x_screen, y_screen + 1, BLACK); - DrawPixel(x_screen, y_screen + 2, DGRAY); - DrawPixel(x_screen, y_screen + 3, DGRAY); - } - } - - void draw_precipitation_histogram() const { - auto tiny_font = this->fonts->get_tiny_font(); - const auto min_bars_height = tiny_font.get()->height + 10; - // 2 pixels of padding in bar - const auto max_bars_height = this->curve_height - tiny_font.get()->height; - // 1 pixel of padding between bar top and - // probability_of_precipitation text - - if (max_bars_height <= min_bars_height) { - return; - } - - const auto normalized_precipitations = normalize_precipitations( - this->model->hourly_forecast, min_bars_height, max_bars_height); - - const Units units{this->model}; - - SetFont(tiny_font.get(), DGRAY); - - for (size_t bar_index = 0; bar_index < this->visible_bars; ++bar_index) { - const auto forecast_index = this->forecast_offset + bar_index; - if (forecast_index < this->model->hourly_forecast.size()) { - const auto forecast = this->model->hourly_forecast[forecast_index]; - if (std::isnan(forecast.rain)) { - continue; - } - const auto bar_center_x = (bar_index + 1.0 / 2) * this->bar_width; - const auto x_screen = - this->bounding_box.x + bar_index * this->bar_width; - const auto bar_height = normalized_precipitations.at(forecast_index); - const auto y_screen = this->curve_y_offset - bar_height; - FillArea(x_screen, y_screen, this->bar_width, bar_height, LGRAY); - DrawRect(x_screen, y_screen, this->bar_width, bar_height, 0x777777); - - const auto precipitation_text = units.format_precipitation( - max_number(forecast.rain, forecast.snow), false); - // rain and snow are in mm/h but save space with shortest unit - - SetFont(tiny_font.get(), DGRAY); - DrawString(bar_center_x - StringWidth(precipitation_text.c_str()) / 2.0, - y_screen - tiny_font.get()->height - 2, - precipitation_text.c_str()); + void draw_and_update(); - const auto probability_of_precipitation = - forecast.probability_of_precipitation; - if (not std::isnan(probability_of_precipitation)) { + void draw_frame_and_values() const; - const auto probability_of_precipitation_text = - std::to_string( - static_cast(probability_of_precipitation * 100)) + - "%"; + void draw_temperature_curve() const; - SetFont(tiny_font.get(), BLACK); - DrawString( - bar_center_x - - StringWidth(probability_of_precipitation_text.c_str()) / 2.0, - this->curve_y_offset - tiny_font.get()->height - 2, - probability_of_precipitation_text.c_str()); - } - } - } - } + void draw_precipitation_histogram() const; }; } // namespace taranis diff --git a/src/meson.build b/src/meson.build index 8785aef..b12e46e 100644 --- a/src/meson.build +++ b/src/meson.build @@ -25,6 +25,7 @@ sources = [about_cc] + files( 'convert.cc', 'currentconditionbox.cc', 'events.cc', + 'hourlyforecastbox.cc', 'http.cc', 'locationbox.cc', 'locationlist.cc',