diff --git a/src/pages/battery/batteryhistory.cpp b/src/pages/battery/batteryhistory.cpp index 0b90876..f7b38c2 100644 --- a/src/pages/battery/batteryhistory.cpp +++ b/src/pages/battery/batteryhistory.cpp @@ -1,5 +1,4 @@ // SPDX-FileCopyrightText: 2026 Reion Wong -// SPDX-FileCopyrightText: 2015 Kai Uwe Broulik // SPDX-License-Identifier: GPL-3.0-or-later #include "pages/battery/batteryhistory.h" @@ -20,108 +19,74 @@ BatteryHistory::BatteryHistory() = default; BatteryHistory::~BatteryHistory() = default; -void BatteryHistory::setDevice(const std::string& device) +void BatteryHistory::load(const Query& query) { - if (device == m_device) + if (m_loaded && query == m_query) return; - m_device = device; - m_upowerDevice.reset(); - load(); -} - -void BatteryHistory::setDuration(uint32_t duration) -{ - if (duration == m_duration) - return; - - m_duration = duration; - load(); -} - -void BatteryHistory::setType(Type type) -{ - if (type == m_type) - return; - - m_type = type; - load(); -} - -void BatteryHistory::start() -{ - m_started = true; - load(); + if (query.device != m_query.device) + m_device.reset(); + m_query = query; + m_loaded = true; + request(); } void BatteryHistory::refresh() { - load(); -} - -uint32_t BatteryHistory::lastTime() const -{ - return m_values.empty() ? 0 : m_values.back().time; + if (m_loaded) + request(); } -int BatteryHistory::largestValue() const +void BatteryHistory::request() { - int max = 0; - for (const HistorySample& sample : m_values) { - if (sample.value > max) - max = int(sample.value); - } - return max; -} - -void BatteryHistory::load() -{ - if (!m_started || m_device.empty()) + if (m_query.device.empty()) return; if (options().mockBattery) { - loadMock(); + m_samples = mockSamples(); + samplesChanged.emit(); return; } - if (!m_upowerDevice) - m_upowerDevice = std::make_unique(m_device); + if (!m_device) + m_device = std::make_unique(m_query.device); - const Device::HistoryType type = m_type == Type::Rate ? Device::HistoryType::Rate : Device::HistoryType::Charge; - m_window.reset(); - m_all.reset(); - m_pending = 2; + const Device::HistoryType type + = m_query.type == Type::Rate ? Device::HistoryType::Rate : Device::HistoryType::Charge; + // A query that changes meanwhile leaves these answers unused. + auto answers = std::make_shared(); + m_answers = answers; - m_windowCall = m_upowerDevice->history(type, m_duration, 100, - [this](const std::optional>& samples) { - m_window = samples; - if (--m_pending == 0) - finish(); - }); + m_windowCall = m_device->history(type, m_query.duration, 100, + [this, answers](const std::optional>& samples) { + answers->window = samples; + answered(answers); + }); // UPower returns only the samples inside the timespan and records one only // when the value changes, so the value at the window's start is the newest // sample before it, found in the whole history. - m_allCall = m_upowerDevice->history(type, std::numeric_limits::max(), - std::numeric_limits::max(), - [this](const std::optional>& samples) { - m_all = samples; - if (--m_pending == 0) - finish(); - }); + m_allCall = m_device->history(type, std::numeric_limits::max(), std::numeric_limits::max(), + [this, answers](const std::optional>& samples) { + answers->all = samples; + answered(answers); + }); } -void BatteryHistory::finish() +void BatteryHistory::answered(const std::shared_ptr& answers) { - m_values.clear(); + if (--answers->outstanding > 0 || answers != m_answers) + return; - if (!m_window) { + m_answers.reset(); + if (!answers->window) { fprintf(stderr, "Failed to get device history from UPower\n"); + m_samples.clear(); return; } - m_values = plotted(*m_window, m_all, uint32_t(std::time(nullptr)), m_duration); - dataChanged.emit(); + m_samples = plotted(*answers->window, answers->all, uint32_t(std::time(nullptr)), m_query.duration); + samplesChanged.emit(); } std::vector BatteryHistory::plotted(const std::vector& window, @@ -183,9 +148,9 @@ std::vector BatteryHistory::plotted(const std::vector BatteryHistory::mockSamples() const { - m_values.clear(); + std::vector samples; const uint32_t now = uint32_t(std::time(nullptr)); // Each day drains on battery, charges, is held at 90% by the charge // limit and is unplugged again, ending where the next day starts. @@ -196,29 +161,29 @@ void BatteryHistory::loadMock() static const std::vector states{2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 5, 2, 2, 2, 2}; // Days drain a little deeper or shallower than one another. static const std::vector depths{0, -6, 4, -3, 6, -4, 2}; - const std::vector& values = m_type == Type::Rate ? rateValues : chargeValues; - const uint32_t days = std::max(1u, m_duration / 86400); - const uint32_t interval = m_duration / days / uint32_t(values.size()); + const std::vector& values = m_query.type == Type::Rate ? rateValues : chargeValues; + const uint32_t duration = m_query.duration; + const uint32_t days = std::max(1u, duration / 86400); + const uint32_t interval = duration / days / uint32_t(values.size()); for (uint32_t day = 0; day < days; ++day) { for (size_t i = 0; i < values.size(); ++i) { HistorySample sample; - sample.time = now - m_duration + (day * uint32_t(values.size()) + uint32_t(i)) * interval; + sample.time = now - duration + (day * uint32_t(values.size()) + uint32_t(i)) * interval; sample.value = values[i]; sample.state = states[i]; - if (m_type == Type::Charge && i >= 3 && i <= 8) + if (m_query.type == Type::Charge && i >= 3 && i <= 8) sample.value += depths[(days - 1 - day) % depths.size()]; - m_values.push_back(sample); + samples.push_back(sample); } } - HistorySample current = m_values.back(); + HistorySample current = samples.back(); current.time = now; - m_values.push_back(current); - - dataChanged.emit(); + samples.push_back(current); + return samples; } } // namespace settings diff --git a/src/pages/battery/batteryhistory.h b/src/pages/battery/batteryhistory.h index 422ff45..fa77708 100644 --- a/src/pages/battery/batteryhistory.h +++ b/src/pages/battery/batteryhistory.h @@ -1,5 +1,4 @@ // SPDX-FileCopyrightText: 2026 Reion Wong -// SPDX-FileCopyrightText: 2015 Kai Uwe Broulik // SPDX-License-Identifier: GPL-3.0-or-later #pragma once @@ -28,29 +27,31 @@ public: Charge, }; + struct Query { + // The UPower device object path; nothing loads while it is empty. + std::string device; + // Seconds back from now. + uint32_t duration = 120; + Type type = Type::Charge; + + bool operator==(const Query&) const = default; + }; + BatteryHistory(); ~BatteryHistory(); BatteryHistory(const BatteryHistory&) = delete; BatteryHistory& operator=(const BatteryHistory&) = delete; - // The UPower device object path. - void setDevice(const std::string& device); - // Seconds back from now. - void setDuration(uint32_t duration); - uint32_t duration() const { return m_duration; } - void setType(Type type); - - // Loads once set up, and again on every change of the above. - void start(); + // Loads unless `query` is already the one loaded. + void load(const Query& query); + // Loads the current query again, for samples recorded since. void refresh(); + const Query& query() const { return m_query; } - const std::vector& samples() const { return m_values; } - int count() const { return int(m_values.size()); } - uint32_t lastTime() const; - int largestValue() const; - - cutefish::base::Signal<> dataChanged; + // Empty until the first answer arrives. + const std::vector& samples() const { return m_samples; } + cutefish::base::Signal<> samplesChanged; // The samples to plot from UPower's answers: the span's samples, newest // first, and the whole history when it came, over `duration` seconds up @@ -61,22 +62,23 @@ public: uint32_t duration); private: - void load(); - void loadMock(); - void finish(); + struct Answers { + std::optional> window; + std::optional> all; + int outstanding = 2; + }; - std::string m_device; - uint32_t m_duration = 120; - Type m_type = Type::Charge; - bool m_started = false; - std::unique_ptr m_upowerDevice; - std::vector m_values; + void request(); + void answered(const std::shared_ptr& answers); + std::vector mockSamples() const; + Query m_query; + bool m_loaded = false; + std::unique_ptr m_device; + std::vector m_samples; + std::shared_ptr m_answers; cutefish::base::dbus::PendingCall m_windowCall; cutefish::base::dbus::PendingCall m_allCall; - int m_pending = 0; - std::optional> m_window; - std::optional> m_all; }; } // namespace settings diff --git a/src/pages/battery/batterypage.cpp b/src/pages/battery/batterypage.cpp index a8285db..05ae25f 100644 --- a/src/pages/battery/batterypage.cpp +++ b/src/pages/battery/batterypage.cpp @@ -86,7 +86,9 @@ public: m_rangePicker = title->picker(); m_rangePicker->setName("battery:range"); m_rangePicker->onCurrentChanged = [this](int index) { - m_history.setDuration(kHistoryDurations[index]); + BatteryHistory::Query query = m_history.query(); + query.duration = kHistoryDurations[index]; + m_history.load(query); }; Card* graphCard = m_historySection->addChild(); graphCard->setSpacing(0); @@ -125,19 +127,20 @@ public: m_subscriptions.push_back(m_battery.capacityChanged.connect([this] { updateHealth(); })); m_subscriptions.push_back(m_battery.lastChargedChanged.connect([this] { updateHealth(); })); m_subscriptions.push_back(m_battery.chargeLimitChanged.connect([this] { updateChargeLimit(); })); - m_subscriptions.push_back(m_battery.udiChanged.connect([this] { m_history.setDevice(m_battery.udi()); })); + m_subscriptions.push_back(m_battery.udiChanged.connect([this] { + BatteryHistory::Query query = m_history.query(); + query.device = m_battery.udi(); + m_history.load(query); + })); m_subscriptions.push_back(m_battery.availableChanged.connect([this] { updateRefreshTimer(); })); m_subscriptions.push_back(window.activeChanged.connect([this] { updateRefreshTimer(); })); - m_subscriptions.push_back(m_history.dataChanged.connect([this] { historyChanged(); })); + m_subscriptions.push_back(m_history.samplesChanged.connect([this] { historyChanged(); })); // Picks up new samples and moves the plot's right edge along with the // clock, only while the page is in front. m_refresh.onTimeout = [this] { m_history.refresh(); }; - m_history.setDevice(m_battery.udi()); - m_history.setDuration(kHistoryDurations[0]); - m_history.setType(BatteryHistory::Type::Charge); - m_history.start(); + m_history.load({m_battery.udi(), kHistoryDurations[0], BatteryHistory::Type::Charge}); m_historySection->setVisible(false); updateHealth(); @@ -150,15 +153,15 @@ private: { // The range the plotted samples cover follows the picker only once // they arrive, so the old samples are never drawn across the new range. - m_plottedDuration = m_history.duration(); + m_plottedDuration = m_history.query().duration; std::vector points; points.reserve(m_history.samples().size()); for (const auto& sample : m_history.samples()) points.push_back({double(sample.time), sample.value, sample.state}); m_graph->setPoints(std::move(points)); - m_graph->setEnd(m_history.lastTime()); + m_graph->setEnd(m_history.samples().empty() ? 0 : m_history.samples().back().time); m_graph->setDuration(m_plottedDuration); - m_historySection->setVisible(m_history.count() > 2); + m_historySection->setVisible(m_history.samples().size() > 2); } void updateRefreshTimer() @@ -172,7 +175,7 @@ private: } m_refresh.start(std::chrono::minutes(1)); // The history loads by itself at first, so only a return needs a refresh. - if (m_history.count() > 0) + if (!m_history.samples().empty()) m_history.refresh(); } diff --git a/src/pages/datetime/timezonemap.cpp b/src/pages/datetime/timezonemap.cpp index 2f6734b..59cc134 100644 --- a/src/pages/datetime/timezonemap.cpp +++ b/src/pages/datetime/timezonemap.cpp @@ -23,46 +23,43 @@ using namespace cutefish::ui; namespace settings { -// gnome-control-center: cc-timezone-map.c -static double radians(double degrees) +// Miller's cylindrical projection: the height of a latitude on the map, +// in units of the earth's radius. +static double millerY(double latitude) { - return (degrees / 360.0) * M_PI * 2; + return 1.25 * std::log(std::tan(M_PI / 4 + 0.4 * latitude * M_PI / 180)); } -static double convertLatitudeToY(double latitude) +// The world map's top edge lies 81/180 of the projection's full height above +// the equator, its bottom edge at 59 degrees south, and its left edge at 168 +// degrees west; positions are fractions of its size. +static double mapY(double latitude) { - const double bottomLatitude = -59; - const double topLatitude = 81; - const double fullRange = 4.6068250867599998; - - const double topPer = topLatitude / 180.0; - double y = 1.25 * log(tan(M_PI_4 + 0.4 * radians(latitude))); - const double topOffset = fullRange * topPer; - const double mapRange = fabs(1.25 * log(tan(M_PI_4 + 0.4 * radians(bottomLatitude))) - topOffset); - y = fabs(y - topOffset); - return y / mapRange; + const double top = 2 * millerY(90) * 81 / 180; + return (top - millerY(latitude)) / (top - millerY(-59)); } -static double convertLongitudeToX(double longitude) +static double mapX(double longitude) { - const double xDegreeOffset = -6; - return (180.0 + longitude) / 360.0 + xDegreeOffset / 180.0; + return (longitude + 168) / 360; } -// The zone's principal location in ISO 6709 sign-degrees-minutes(-seconds), -// +-DDMM+-DDDMM or +-DDMMSS+-DDDMMSS; `digits` is 2 for the latitude, 3 for -// the longitude. Read the way the map has always read it. -static double convertPosition(const std::string& position, int digits) +// One coordinate of zone.tab's ISO 6709 location, sign then degrees, minutes +// and optional seconds: "+3130" or "-0583506", with `degreeDigits` of 2 for a +// latitude and 3 for a longitude. +static double iso6709Degrees(const std::string& text, size_t degreeDigits) { - if (position.size() < 4 || digits > 9) + if (text.empty() || (text[0] != '+' && text[0] != '-')) return 0.0; - const std::string integer = position.substr(0, digits + 1); - const std::string fraction = position.substr(digits + 1); - const double t1 = strtod(integer.c_str(), nullptr); - const double t2 = strtod(fraction.c_str(), nullptr); - if (t1 > 0.0) - return t1 + t2 / pow(10.0, double(fraction.size())); - return t1 - t2 / pow(10.0, double(fraction.size())); + const size_t digits = text.size() - 1; + if (digits != degreeDigits + 2 && digits != degreeDigits + 4) + return 0.0; + + const auto field = [&text](size_t at, size_t length) { return std::atoi(text.substr(at, length).c_str()); }; + const double minutes = field(1 + degreeDigits, 2); + const double seconds = digits == degreeDigits + 4 ? field(3 + degreeDigits, 2) : 0; + const double degrees = field(1, degreeDigits) + minutes / 60 + seconds / 3600; + return text[0] == '-' ? -degrees : degrees; } static const std::chrono::time_zone* locate(const std::string& id) @@ -143,8 +140,8 @@ TimeZoneMap::TimeZoneMap(timedate::TimeDate& timeDate) size_t index = coordinate.find('+', 3); if (index == std::string::npos) index = coordinate.find('-', 3); - m_list.push_back({parts[0], parts[2], convertPosition(coordinate.substr(0, index), 2), - index == std::string::npos ? 0.0 : convertPosition(coordinate.substr(index), 3)}); + m_list.push_back({parts[0], parts[2], iso6709Degrees(coordinate.substr(0, index), 2), + index == std::string::npos ? 0.0 : iso6709Degrees(coordinate.substr(index), 3)}); } } @@ -171,8 +168,8 @@ int TimeZoneMap::nearestIndex(int x, int y, int width, int height) const int nearest = -1; for (size_t i = 0; i < m_list.size(); ++i) { const Item& item = m_list[i]; - const double dx = convertLongitudeToX(item.longitude) * width - x; - const double dy = convertLatitudeToY(item.latitude) * height - y; + const double dx = mapX(item.longitude) * width - x; + const double dy = mapY(item.latitude) * height - y; const double distance = dx * dx + dy * dy; if (distance < minimumDistance) { minimumDistance = distance; @@ -186,8 +183,8 @@ void TimeZoneMap::clicked(int x, int y, int width, int height) { std::vector items; for (const Item& item : m_list) { - const double dx = convertLongitudeToX(item.longitude) * width - x; - const double dy = convertLatitudeToY(item.latitude) * height - y; + const double dx = mapX(item.longitude) * width - x; + const double dy = mapY(item.latitude) * height - y; if (dx * dx + dy * dy <= 20) items.push_back(&item); } @@ -233,7 +230,7 @@ void TimeZoneMap::select(const std::string& id) PointF TimeZoneMap::position(const std::string& id, int width, int height) const { if (const Item* item = find(id)) - return {convertLongitudeToX(item->longitude) * width, convertLatitudeToY(item->latitude) * height}; + return {mapX(item->longitude) * width, mapY(item->latitude) * height}; return {-1, -1}; } @@ -249,7 +246,7 @@ std::vector TimeZoneMap::points() const std::vector result; result.reserve(m_list.size()); for (const Item& item : m_list) { - result.push_back({item.timeZone, convertLongitudeToX(item.longitude), convertLatitudeToY(item.latitude), + result.push_back({item.timeZone, mapX(item.longitude), mapY(item.latitude), offsetAt(item.timeZone, time)}); } return result; diff --git a/src/pages/datetime/timezonemap.h b/src/pages/datetime/timezonemap.h index b3e7d70..02b3b74 100644 --- a/src/pages/datetime/timezonemap.h +++ b/src/pages/datetime/timezonemap.h @@ -16,7 +16,7 @@ class TimeDate; namespace settings { // Every zone of zone.tab placed on the world map, and the zones a click on -// the map picks, as gnome-control-center's map does. +// the map picks. class TimeZoneMap { public: