refactor(settings): rewrite the battery history model and the time zone map projection

main
reionwong 1 week ago
parent e0868c107e
commit c996c1973b

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: 2026 Reion Wong <reionwong@gmail.com>
// SPDX-FileCopyrightText: 2015 Kai Uwe Broulik <kde@privat.broulik.de>
// 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<Device>(m_device);
if (!m_device)
m_device = std::make_unique<Device>(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<Answers>();
m_answers = answers;
m_windowCall = m_upowerDevice->history(type, m_duration, 100,
[this](const std::optional<std::vector<HistorySample>>& samples) {
m_window = samples;
if (--m_pending == 0)
finish();
});
m_windowCall = m_device->history(type, m_query.duration, 100,
[this, answers](const std::optional<std::vector<HistorySample>>& 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<uint32_t>::max(),
std::numeric_limits<uint32_t>::max(),
[this](const std::optional<std::vector<HistorySample>>& samples) {
m_all = samples;
if (--m_pending == 0)
finish();
});
m_allCall = m_device->history(type, std::numeric_limits<uint32_t>::max(), std::numeric_limits<uint32_t>::max(),
[this, answers](const std::optional<std::vector<HistorySample>>& samples) {
answers->all = samples;
answered(answers);
});
}
void BatteryHistory::finish()
void BatteryHistory::answered(const std::shared_ptr<Answers>& 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<HistorySample> BatteryHistory::plotted(const std::vector<HistorySample>& window,
@ -183,9 +148,9 @@ std::vector<HistorySample> BatteryHistory::plotted(const std::vector<HistorySamp
return values;
}
void BatteryHistory::loadMock()
std::vector<HistorySample> BatteryHistory::mockSamples() const
{
m_values.clear();
std::vector<HistorySample> 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<uint32_t> 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<double> depths{0, -6, 4, -3, 6, -4, 2};
const std::vector<double>& 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<double>& 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

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: 2026 Reion Wong <reionwong@gmail.com>
// SPDX-FileCopyrightText: 2015 Kai Uwe Broulik <kde@privat.broulik.de>
// 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<cutefish::upower::HistorySample>& 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<cutefish::upower::HistorySample>& 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<std::vector<cutefish::upower::HistorySample>> window;
std::optional<std::vector<cutefish::upower::HistorySample>> 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<cutefish::upower::Device> m_upowerDevice;
std::vector<cutefish::upower::HistorySample> m_values;
void request();
void answered(const std::shared_ptr<Answers>& answers);
std::vector<cutefish::upower::HistorySample> mockSamples() const;
Query m_query;
bool m_loaded = false;
std::unique_ptr<cutefish::upower::Device> m_device;
std::vector<cutefish::upower::HistorySample> m_samples;
std::shared_ptr<Answers> m_answers;
cutefish::base::dbus::PendingCall m_windowCall;
cutefish::base::dbus::PendingCall m_allCall;
int m_pending = 0;
std::optional<std::vector<cutefish::upower::HistorySample>> m_window;
std::optional<std::vector<cutefish::upower::HistorySample>> m_all;
};
} // namespace settings

@ -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<Card>();
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<HistoryGraph::Point> 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();
}

@ -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<const Item*> 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::Point> TimeZoneMap::points() const
std::vector<Point> 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;

@ -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:

Loading…
Cancel
Save