diff --git a/NEWS.md b/NEWS.md index d1f94da..2b870fd 100644 --- a/NEWS.md +++ b/NEWS.md @@ -13,6 +13,9 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Add icons to sunrise and sunset lines [#95](https://github.com/orontee/taranis/issues/95) +- Display dialog with daily conditions details + [#16](https://github.com/orontee/taranis/issues/16) + ### Changed ### Removed diff --git a/src/alerts.cc b/src/alerts.cc index ad6ff45..4a0cffe 100644 --- a/src/alerts.cc +++ b/src/alerts.cc @@ -28,8 +28,9 @@ void AlertViewer::open() { } this->visible = true; - const auto &alert = this->model->alerts.at(this->alert_index); this->update_title_text(); + + const auto &alert = this->model->alerts.at(this->alert_index); this->update_alert_title_text(alert); this->update_description_text(alert); @@ -69,7 +70,7 @@ void AlertViewer::do_paint() { const auto alert_title_height = TextRectHeight( this->content_width, this->alert_title_text.c_str(), ALIGN_LEFT); - DrawTextRect(AlertViewer::horizontal_padding, alert_title_start_y, + DrawTextRect(AlertViewer::horizontal_padding, this->alert_title_start_y, this->content_width, alert_title_height, this->alert_title_text.c_str(), ALIGN_LEFT); diff --git a/src/dailyforecastbox.cc b/src/dailyforecastbox.cc index 5c483d0..d51cd4d 100644 --- a/src/dailyforecastbox.cc +++ b/src/dailyforecastbox.cc @@ -1,15 +1,18 @@ #include "dailyforecastbox.h" +#include + +#include "dailyforecastviewer.h" #include "units.h" #include "util.h" namespace taranis { -DailyForecastBox::DailyForecastBox(int pos_x, int pos_y, int width, int height, - std::shared_ptr model, - std::shared_ptr icons, - std::shared_ptr fonts) +DailyForecastBox::DailyForecastBox( + int pos_x, int pos_y, int width, int height, std::shared_ptr model, + std::shared_ptr icons, std::shared_ptr fonts, + std::shared_ptr daily_forecast_viewer) : Widget{pos_x, pos_y, width, height}, model{model}, icons{icons}, - fonts{fonts} { + fonts{fonts}, viewer{daily_forecast_viewer} { this->row_height = this->bounding_box.h / DailyForecastBox::row_count; } @@ -18,6 +21,16 @@ void DailyForecastBox::do_paint() { this->draw_frame(); } +int DailyForecastBox::handle_pointer_event(int event_type, int pointer_pos_x, + int pointer_pos_y) { + // check pos on frame, identify forecast index, set viewer forecast index + if (event_type == EVT_POINTERUP) { + this->on_clicked_at(pointer_pos_x, pointer_pos_y); + return 1; + } + return 0; +} + std::pair DailyForecastBox::generate_precipitation_column_content( const DailyCondition &forecast) const { @@ -284,4 +297,13 @@ void DailyForecastBox::draw_values() { } } } + +void DailyForecastBox::on_clicked_at(int pointer_pos_x, int pointer_pos_y) { + if (this->viewer) { + const auto row_index = static_cast( + std::max(0, pointer_pos_y - this->bounding_box.y) / this->row_height); + this->viewer->set_forecast_index(row_index); + this->viewer->open(); + } +} } // namespace taranis diff --git a/src/dailyforecastbox.h b/src/dailyforecastbox.h index 51e8ce0..f674911 100644 --- a/src/dailyforecastbox.h +++ b/src/dailyforecastbox.h @@ -18,14 +18,20 @@ using namespace std::string_literals; namespace taranis { +class DailyForecastViewer; + class DailyForecastBox : public Widget { public: DailyForecastBox(int pos_x, int pos_y, int width, int height, std::shared_ptr model, std::shared_ptr icons, - std::shared_ptr fonts); + std::shared_ptr fonts, + std::shared_ptr daily_forecast_viewer); void do_paint() override; + int handle_pointer_event(int event_type, int pointer_pos_x, + int pointer_pos_y) override; + private: static constexpr size_t row_count{8}; static constexpr size_t column_count{9}; @@ -44,6 +50,8 @@ class DailyForecastBox : public Widget { std::shared_ptr icons; std::shared_ptr fonts; + std::shared_ptr viewer; + TableContent table_content; int row_height; @@ -80,6 +88,7 @@ class DailyForecastBox : public Widget { void draw_frame() const; void draw_values(); -}; + void on_clicked_at(int pointer_pos_x, int pointer_pos_y); +}; } // namespace taranis diff --git a/src/dailyforecastviewer.cc b/src/dailyforecastviewer.cc new file mode 100644 index 0000000..2f8e7c7 --- /dev/null +++ b/src/dailyforecastviewer.cc @@ -0,0 +1,383 @@ +#include "dailyforecastviewer.h" + +#include "units.h" +#include "util.h" + +namespace taranis { + +DailyForecastViewer::DailyForecastViewer(std::shared_ptr model, + std::shared_ptr fonts) + : ModalWidget{}, model{model}, fonts{fonts}, + content_width{this->bounding_box.w - + 2 * DailyForecastViewer::horizontal_padding}, + title_height{2 * this->fonts->get_small_font()->height}, + forecast_title_start_y{this->title_height + + DailyForecastViewer::vertical_padding} {} + +void DailyForecastViewer::open() { + if (this->forecast_count() == 0 or + this->forecast_index >= this->forecast_count()) { + this->hide(); + return; + } + this->visible = true; + + this->update_title_text(); + + const auto &condition = this->model->daily_forecast.at(this->forecast_index); + this->update_forecast_title_text(condition); + this->generate_description_data(condition); + + this->update_layout(); + + AddScrolledArea(&this->scrollable_view_rectangle, true); + + const auto event_handler = GetEventHandler(); + SendEvent(event_handler, EVT_SHOW, 0, 0); +} + +void DailyForecastViewer::hide() { + this->visible = false; + RemoveScrolledArea(&this->scrollable_view_rectangle); + + this->forecast_index = 0; + + const auto event_handler = GetEventHandler(); + SendEvent(event_handler, EVT_SHOW, 0, 0); +} + +void DailyForecastViewer::set_forecast_index(size_t index) { + this->forecast_index = index; +} + +void DailyForecastViewer::do_paint() { + // title + const auto default_font = this->fonts->get_small_font(); + SetFont(default_font.get(), BLACK); + + DrawTextRect(DailyForecastViewer::horizontal_padding, + DailyForecastViewer::vertical_padding, this->content_width, + this->title_height, this->title_text.c_str(), ALIGN_CENTER); + + DrawHorizontalSeparator(0, this->title_height, ScreenWidth(), + HORIZONTAL_SEPARATOR_SOLID); + + // forecast title + const auto bold_font = this->fonts->get_small_bold_font(); + SetFont(bold_font.get(), BLACK); + + const auto forecast_title_height = TextRectHeight( + this->content_width, this->forecast_title_text.c_str(), ALIGN_LEFT); + DrawTextRect(DailyForecastViewer::horizontal_padding, + this->forecast_title_start_y, this->content_width, + forecast_title_height, this->forecast_title_text.c_str(), + ALIGN_LEFT); + + // Forecast description + SetFont(default_font.get(), BLACK); + + SetClipRect(&this->scrollable_view_rectangle); + + int current_row_start_y = + this->scrollable_view_rectangle.y + this->scrollable_view_offset; + const int second_column_of_two_x = + this->scrollable_view_rectangle.x + this->scrollable_view_rectangle.w / 2; + const int second_column_of_three_x = + this->scrollable_view_rectangle.x + this->scrollable_view_rectangle.w / 3; + const int third_column_of_three_x = this->scrollable_view_rectangle.x + + 2 * this->scrollable_view_rectangle.w / 3; + + std::string label_text, first_text, second_text, third_text; + + for (auto const &row_values : this->description_data) { + if (row_values.type() == typeid(std::string)) { + + label_text = boost::get(row_values); + + DrawTextRect(DailyForecastViewer::horizontal_padding, current_row_start_y, + StringWidth(label_text.c_str()), default_font.get()->height, + label_text.c_str(), ALIGN_LEFT); + + } else if (row_values.type() == + typeid(std::pair)) { + + std::tie(label_text, first_text) = + boost::get>(row_values); + + DrawTextRect(DailyForecastViewer::horizontal_padding, current_row_start_y, + StringWidth(label_text.c_str()), default_font.get()->height, + label_text.c_str(), ALIGN_LEFT); + + DrawTextRect( + DailyForecastViewer::horizontal_padding + second_column_of_two_x, + current_row_start_y, StringWidth(first_text.c_str()), + default_font.get()->height, first_text.c_str(), ALIGN_CENTER); + + } else if (row_values.type() == + typeid(std::tuple)) { + + std::tie(label_text, first_text, second_text) = + boost::get>( + row_values); + + DrawTextRect(DailyForecastViewer::horizontal_padding, current_row_start_y, + StringWidth(label_text.c_str()), default_font.get()->height, + label_text.c_str(), ALIGN_LEFT); + + DrawTextRect( + DailyForecastViewer::horizontal_padding + second_column_of_three_x, + current_row_start_y, StringWidth(first_text.c_str()), + default_font.get()->height, first_text.c_str(), ALIGN_CENTER); + + DrawTextRect( + DailyForecastViewer::horizontal_padding + third_column_of_three_x, + current_row_start_y, StringWidth(second_text.c_str()), + default_font.get()->height, second_text.c_str(), ALIGN_CENTER); + } else if (row_values.type() == typeid(std::pair)) { + // not implemented + } + + current_row_start_y += default_font->height; + } + + this->forecast_description_height = + current_row_start_y - + (this->scrollable_view_rectangle.y + this->scrollable_view_offset); + + SetClip(0, 0, ScreenWidth(), ScreenHeight()); +} + +void DailyForecastViewer::update_layout() { + const auto bold_font = this->fonts->get_small_bold_font(); + const auto default_font = this->fonts->get_small_font(); + + SetFont(bold_font.get(), BLACK); + + const auto forecast_title_height = TextRectHeight( + this->content_width, this->forecast_title_text.c_str(), ALIGN_LEFT); + + SetFont(default_font.get(), BLACK); + + const auto scrollable_view_start_y = this->forecast_title_start_y + + forecast_title_height + + DailyForecastViewer::vertical_padding; + this->scrollable_view_rectangle = + iRect(DailyForecastViewer::horizontal_padding, scrollable_view_start_y, + ScreenWidth() - 2 * DailyForecastViewer::horizontal_padding, + ScreenHeight() - scrollable_view_start_y - + DailyForecastViewer::vertical_padding, + 0); + + this->scrollable_view_offset = 0; + + const int description_height = + this->description_data.size() * default_font->height; + + this->min_scrollable_view_offset = + -std::max(0, description_height - this->scrollable_view_rectangle.h); +} + +void DailyForecastViewer::update_title_text() { + this->title_text = std::string{GetLangText("DAILY FORECAST")}; + if (this->forecast_count() > 1) { + this->title_text += (" " + std::to_string(this->forecast_index + 1) + "/" + + std::to_string(this->forecast_count())); + } +} + +void DailyForecastViewer::update_forecast_title_text( + const DailyCondition &condition) { + this->forecast_title_text = format_date(condition.date, true); +} + +void DailyForecastViewer::generate_description_data( + const DailyCondition &condition) { + this->description_data.clear(); + + const Units units{this->model}; + + this->description_data.push_back(std::tuple{std::string{GetLangText("Sun")}, + format_time(condition.sunrise), + format_time(condition.sunset)}); + + this->description_data.push_back(std::tuple{std::string{GetLangText("Moon")}, + format_time(condition.moonrise), + format_time(condition.moonset)}); + // TODO Moon phase + + this->description_data.push_back(""); + + std::string description_text = condition.weather_description; + if (not description_text.empty()) { + description_text[0] = std::toupper(description_text[0]); + } + this->description_data.push_back( + std::pair{GetLangText("Weather"), description_text}); + + this->description_data.push_back(""); + + this->description_data.push_back(std::tuple{ + std::string{}, GetLangText("Temperature"), GetLangText("Felt")}); + this->description_data.push_back( + std::tuple{GetLangText("Morning"), + units.format_temperature(condition.temperature_morning), + units.format_temperature(condition.felt_temperature_morning)}); + this->description_data.push_back(std::tuple{ + GetLangText("Day"), units.format_temperature(condition.temperature_day), + units.format_temperature(condition.felt_temperature_day)}); + this->description_data.push_back( + std::tuple{GetLangText("Evening"), + units.format_temperature(condition.temperature_evening), + units.format_temperature(condition.felt_temperature_evening)}); + this->description_data.push_back( + std::tuple{GetLangText("Night"), + units.format_temperature(condition.temperature_night), + units.format_temperature(condition.felt_temperature_night)}); + this->description_data.push_back( + std::pair{GetLangText("Min/Max"), + units.format_temperature(condition.temperature_min) + "/" + + units.format_temperature(condition.temperature_max)}); + + this->description_data.push_back(""); + + this->description_data.push_back( + std::pair{std::string{GetLangText("Pressure")}, + units.format_pressure(condition.pressure)}); + + this->description_data.push_back( + std::pair{std::string{GetLangText("Humidity")}, + std::to_string(condition.humidity) + "%"}); + + if (not std::isnan(condition.dew_point)) { + this->description_data.push_back( + std::pair{GetLangText("Dew point"), + units.format_temperature(condition.uv_index)}); + } + + if (not std::isnan(condition.uv_index)) { + this->description_data.push_back( + std::pair{GetLangText("UV index"), + std::to_string(static_cast(condition.uv_index))}); + } + + if (not std::isnan(condition.clouds)) { + this->description_data.push_back(std::pair{ + GetLangText("Cloudiness"), std::to_string(condition.clouds) + "%"}); + } + + this->description_data.push_back(""); + + if (static_cast(condition.wind_speed) != 0 or + static_cast(condition.wind_gust) != 0) { + const auto wind_speed_text = units.format_speed(condition.wind_speed); + if (static_cast(condition.wind_speed) != 0) { + this->description_data.push_back( + std::pair{GetLangText("Wind speed"), wind_speed_text}); + } + + if (static_cast(condition.wind_gust) != 0) { + const auto wind_gust_text = units.format_speed(condition.wind_gust); + if (condition.wind_gust > condition.wind_speed and + wind_speed_text != wind_gust_text) { + this->description_data.push_back( + std::pair{GetLangText("Wind gust"), wind_gust_text}); + } + } + // TODO wind degree + + this->description_data.push_back(""); + } + + if (not std::isnan(condition.rain) or not std::isnan(condition.snow)) { + if (not std::isnan(condition.rain)) { + this->description_data.push_back( + std::pair{GetLangText("Rain"), + units.format_precipitation(condition.rain, true)}); + } + + if (not std::isnan(condition.snow)) { + this->description_data.push_back( + std::pair{GetLangText("Snow"), + units.format_precipitation(condition.snow, true)}); + } + if (not std::isnan(condition.probability_of_precipitation)) { + this->description_data.push_back( + std::pair{GetLangText("Probability"), + std::to_string(static_cast( + condition.probability_of_precipitation * 100)) + + "%"}); + } + } +} + +void DailyForecastViewer::display_previous_forecast_maybe() { + if (this->forecast_index != 0) { + --this->forecast_index; + + const auto &condition = + this->model->daily_forecast.at(this->forecast_index); + this->update_title_text(); + this->update_forecast_title_text(condition); + this->generate_description_data(condition); + + this->update_layout(); + + this->paint_and_update_screen(); + } else { + this->hide(); + } +} + +void DailyForecastViewer::display_next_forecast_maybe() { + if (this->forecast_index + 1 < this->forecast_count()) { + ++this->forecast_index; + + const auto &condition = + this->model->daily_forecast.at(this->forecast_index); + this->update_title_text(); + this->update_forecast_title_text(condition); + this->generate_description_data(condition); + + this->update_layout(); + + this->paint_and_update_screen(); + } else { + this->hide(); + } +} + +bool DailyForecastViewer::handle_key_release(int key) { + if (key == IV_KEY_PREV) { + this->display_previous_forecast_maybe(); + } else if (key == IV_KEY_NEXT) { + this->display_next_forecast_maybe(); + } + return true; +} + +int DailyForecastViewer::handle_pointer_event(int event_type, int pointer_pos_x, + int pointer_pos_y) { + if (event_type == EVT_SCROLL) { + auto *const scroll_area = reinterpret_cast(pointer_pos_x); + if (scroll_area == &this->scrollable_view_rectangle) { + const int delta_x = pointer_pos_y >> 16; + const int delta_y = (pointer_pos_y << 16) >> 16; + BOOST_LOG_TRIVIAL(debug) + << "Daily forecast viewer received a scrolling event " + << "delta: " << delta_x << " " << delta_y + << " current offset: " << this->scrollable_view_offset + << " min. offset: " << this->min_scrollable_view_offset; + + this->scrollable_view_offset = + std::min(0, std::max(this->scrollable_view_offset + delta_y, + this->min_scrollable_view_offset)); + + this->paint_and_update_screen(); + + return 1; + } + } + return 0; +} + +} // namespace taranis diff --git a/src/dailyforecastviewer.h b/src/dailyforecastviewer.h new file mode 100644 index 0000000..60d5d0a --- /dev/null +++ b/src/dailyforecastviewer.h @@ -0,0 +1,79 @@ +#pragma once + +#include +#include +#include + +#include "fonts.h" +#include "model.h" +#include "widget.h" + +namespace taranis { +class DailyForecastViewer : public ModalWidget { +public: + DailyForecastViewer(std::shared_ptr model, + std::shared_ptr fonts); + + void open(); + + void hide(); + + void set_forecast_index(size_t index); + + void do_paint() override; + + bool handle_key_release(int key) override; + + int handle_pointer_event(int event_type, int pointer_pos_x, + int pointer_pos_y) override; + +private: + static constexpr int horizontal_padding{25}; + static constexpr int vertical_padding{25}; + + std::shared_ptr model; + std::shared_ptr fonts; + + size_t forecast_index{0}; + + const int content_width; + const int title_height; + const int forecast_title_start_y; + + irect scrollable_view_rectangle; + int scrollable_view_offset{0}; + int min_scrollable_view_offset{0}; + + typedef boost::variant, + std::tuple, + std::pair> + CellContent; + // one text column, two text columns, three text columns, two mixed + // columns + + typedef std::vector DescriptionRows; + + DescriptionRows description_data; + + int forecast_description_height; + + std::string title_text; + std::string forecast_title_text; + + inline size_t forecast_count() const { + return this->model->daily_forecast.size(); + } + + void update_layout(); + + void update_title_text(); + + void update_forecast_title_text(const DailyCondition &condition); + + void generate_description_data(const DailyCondition &condition); + + void display_previous_forecast_maybe(); + + void display_next_forecast_maybe(); +}; +} // namespace taranis diff --git a/src/forecast.cc b/src/forecast.cc index c5a1153..2c34d69 100644 --- a/src/forecast.cc +++ b/src/forecast.cc @@ -7,10 +7,10 @@ namespace taranis { -ForecastStack::ForecastStack(int pos_x, int pos_y, int width, int height, - std::shared_ptr model, - std::shared_ptr icons, - std::shared_ptr fonts) +ForecastStack::ForecastStack( + int pos_x, int pos_y, int width, int height, std::shared_ptr model, + std::shared_ptr icons, std::shared_ptr fonts, + std::shared_ptr daily_forecast_viewer) : Widget{pos_x, pos_y, width, height}, model{model}, pager{std::make_shared( pos_x, pos_y + height - ForecastStack::pager_height, width, @@ -23,7 +23,8 @@ ForecastStack::ForecastStack(int pos_x, int pos_y, int width, int height, pos_x, pos_y, width, stack_height, model, icons, fonts); auto daily_forecast_box = std::make_shared( - pos_x, pos_y, width, stack_height, model, icons, fonts); + pos_x, pos_y, width, stack_height, model, icons, fonts, + daily_forecast_viewer); this->stack->add("hourly-forecast", this->hourly_forecast_box); this->stack->add("daily-forecast", daily_forecast_box); diff --git a/src/forecast.h b/src/forecast.h index 7a418c1..56e7f7e 100644 --- a/src/forecast.h +++ b/src/forecast.h @@ -2,6 +2,7 @@ #include +#include "dailyforecastviewer.h" #include "fonts.h" #include "hourlyforecastbox.h" #include "icons.h" @@ -14,7 +15,8 @@ namespace taranis { struct ForecastStack : Widget, Paginated { ForecastStack(int pos_x, int pos_y, int width, int height, std::shared_ptr model, std::shared_ptr icons, - std::shared_ptr fonts); + std::shared_ptr fonts, + std::shared_ptr daily_forecast_viewer); void switch_forecast(); diff --git a/src/logo.cc b/src/logo.cc index 5cb9590..f174133 100644 --- a/src/logo.cc +++ b/src/logo.cc @@ -21,7 +21,8 @@ Logo::Logo(std::shared_ptr model, std::shared_ptr icons, this->status_bar.get_height(), model, icons, - fonts} {} + fonts, + nullptr} {} void Logo::do_paint() { this->location_box.do_paint(); diff --git a/src/meson.build b/src/meson.build index ef3cead..a3ea0b4 100644 --- a/src/meson.build +++ b/src/meson.build @@ -66,6 +66,7 @@ sources = [about_cc, icons_cc, l10n_cc] + files( 'convert.cc', 'currentconditionbox.cc', 'dailyforecastbox.cc', + 'dailyforecastviewer.cc', 'events.cc', 'fonts.cc', 'forecast.cc', diff --git a/src/ui.cc b/src/ui.cc index e47c95b..b4e72cd 100644 --- a/src/ui.cc +++ b/src/ui.cc @@ -5,6 +5,7 @@ #include #include "currentconditionbox.h" +#include "dailyforecastviewer.h" #include "events.h" #include "history.h" #include "keys.h" @@ -31,6 +32,8 @@ Ui::Ui(std::shared_ptr config, std::shared_ptr model) menu_button->set_menu_handler(&handle_menu_item_selected); this->alert_viewer = std::make_shared(model, this->fonts); + this->daily_forecast_viewer = + std::make_shared(model, this->fonts); auto alerts_button = std::make_shared( Ui::alert_button_icon_size, model, this->icons, this->alert_viewer); @@ -56,12 +59,14 @@ Ui::Ui(std::shared_ptr config, std::shared_ptr model) this->forecast_stack = std::make_shared( 0, current_condition_box->get_pos_y() + current_condition_box->get_height(), - ScreenWidth(), remaining_height, this->model, this->icons, this->fonts); + ScreenWidth(), remaining_height, this->model, this->icons, this->fonts, + this->daily_forecast_viewer); this->location_selector = std::make_shared( this->location_selector_icon_size, this->fonts, this->icons); this->modals.push_back(this->alert_viewer); + this->modals.push_back(this->daily_forecast_viewer); this->children.push_back(this->location_box); this->children.push_back(alerts_button); @@ -71,6 +76,7 @@ Ui::Ui(std::shared_ptr config, std::shared_ptr model) this->children.push_back(status_bar); this->register_key_event_consumer(this->alert_viewer); + this->register_key_event_consumer(this->daily_forecast_viewer); this->register_key_event_consumer(menu_button); this->register_key_event_consumer(this->forecast_stack); } diff --git a/src/ui.h b/src/ui.h index 2bdd689..5e7ef3b 100644 --- a/src/ui.h +++ b/src/ui.h @@ -7,6 +7,7 @@ #include "alerts.h" #include "config.h" +#include "dailyforecastviewer.h" #include "fonts.h" #include "forecast.h" #include "icons.h" @@ -63,6 +64,7 @@ class Ui : public KeyEventDispatcher { std::shared_ptr location_box; std::shared_ptr alert_viewer; + std::shared_ptr daily_forecast_viewer; std::shared_ptr forecast_stack;