Update screenshot of main view
[weightgraph] / weightgraph / weightdata.cpp
1 #include "weightdata.h"
2 #include "settings.h"
3 #include <QTextStream>
4 #include <QStringList>
5 #include <QtAlgorithms>
6 #include <assert.h>
7 #include <limits>
8
9 #include <QDebug>
10
11 WeightDataModel::WeightDataModel(QString &datafilename, QObject *parent) :
12     QAbstractTableModel(parent), datafile(datafilename)
13 {
14   clear();
15   readFromDisk();
16 }
17
18 Qt::ItemFlags WeightDataModel::flags(const QModelIndex &index) const
19 {
20   if(!index.isValid())
21     return Qt::ItemIsEnabled;
22   switch(index.column()) {
23     case 0: return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
24     case 1: return Qt::ItemIsEnabled | Qt::ItemIsEditable;
25     default: return Qt::NoItemFlags;
26   }
27 }
28
29 QVariant WeightDataModel::headerData(int section, Qt::Orientation orientation, int role) const
30 {
31   if (role != Qt::DisplayRole)
32     return QVariant();
33   if (orientation == Qt::Horizontal) {
34     return section == 0 ? tr("Date") : tr("Weight");
35   }
36   else {
37     return QString("%1").arg(section);
38   }
39 }
40
41 bool WeightDataModel::dateExists(const QDate &date) const
42 {
43   return rowOfDate(date) != -1;
44 }
45
46 int WeightDataModel::rowOfDate(const QDate &date) const
47 {
48   // TODO: binary search
49   for(int i=0; i<weights.size(); i++)
50     if (weights[i].date == date)
51       return i;
52   return -1;
53 }
54
55 QModelIndex WeightDataModel::indexOfDate(const QDate &date) const
56 {
57   return index(rowOfDate(date), 0);
58 }
59
60 QVariant WeightDataModel::data(const QModelIndex &index, int role) const
61 {
62   if (!index.isValid() || index.row() >= rowCount()
63       || index.column() >= columnCount())
64     return QVariant();
65
66   if (role != Qt::DisplayRole && role != Qt::EditRole && role != Qt::SizeHintRole)
67     return QVariant();
68
69   if (role == Qt::SizeHintRole) {
70     if (index.column() == 0)
71       return QSize(230,70);
72     else
73       return QSize(280,70);
74   }
75
76   if (index.column() == 0) {
77     return weights.at(index.row()).date.toString(Qt::ISODate);
78   }
79   else {
80     if (role == Qt::DisplayRole)
81       return QString("%1 %2").arg(weights.at(index.row()).weight,5,'f',1)
82                              .arg(Settings::weightUnit());
83     else
84       return weights.at(index.row()).weight;
85   }
86
87   return QVariant();
88 }
89
90 double WeightDataModel::minWeight() const {
91   // TODO: cache minimum and maximum weight on initial read and modifications?
92   double min = std::numeric_limits<double>::max();
93   foreach(const DW& dw, weights) {
94     if (dw.weight < min)
95       min = dw.weight;
96   }
97   return min;
98 }
99
100 double WeightDataModel::maxWeight() const {
101   double max = std::numeric_limits<double>::min();
102   foreach(const DW& dw, weights) {
103     if (dw.weight > max)
104       max = dw.weight;
105   }
106   return max;
107 }
108
109 bool WeightDataModel::setData(const QModelIndex &index, const QVariant &value, int role)
110 {
111   if (!index.isValid() || role != Qt::EditRole)
112     return false;
113   switch (index.column()) {
114   case 0: {
115     QDate date = value.toDate();
116     if (!date.isValid())
117       return false;
118     weights[index.row()].date = date;
119     break;
120   }
121   case 1: {
122     bool ok;
123     double weight = value.toDouble(&ok);
124     if (!ok)
125       return false;
126     weights[index.row()].weight = weight;
127     break;
128   }
129   default: return false;
130   }
131
132   emit(dataChanged(index, index));
133
134   writeToDisk();
135   return true;
136 }
137
138 bool WeightDataModel::setDataForRow(int row, const DateWeight &dw)
139 {
140   if (row < 0 || row >= weights.size())
141     return false;
142
143   weights[row] = dw;
144   QModelIndex a = index(row, 0);
145   QModelIndex b = index(row, 1);
146
147   emit(dataChanged(a, b));
148   writeToDisk();
149   return true;
150 }
151 // Sets data for a date. Adds a new row for the date if it doesn't exist.
152 void WeightDataModel::setWeightForDate(const QDate &date, double weight)
153 {
154   int row = rowOfDate(date);
155   if (row == -1) {
156     row = rowForNewDate(date);
157     insertRows(row, 1);
158   }
159   DateWeight dw = {date, weight};
160   setDataForRow(row, dw);
161 }
162 int WeightDataModel::rowForNewDate(const QDate &date) const
163 {
164   if (weights.size() == 0)
165     return 0;
166   if (date < weights.first().date)
167     return 0;
168   // TODO: binary search
169   for(int i=1; i<weights.size(); i++) {
170     if (weights.at(i-1).date < date && weights.at(i).date > date)
171       return i;
172   }
173   if (date > weights.last().date)
174     return weights.size();
175   assert(0 && "UNREACHABLE");
176   return -1;
177 }
178
179 void WeightDataModel::setWeightForDate(const WeightDataModel::DateWeight &dw)
180 {
181   setWeightForDate(dw.date, dw.weight);
182 }
183
184 // Insert count "empty" rows starting from row.
185 // DOESN'T WRITE DATA BACK TO DISK to allow inserter
186 // to modify the row, then write it.
187 bool WeightDataModel::insertRows(int row, int count, const QModelIndex &/*parent*/)
188 {
189   beginInsertRows(QModelIndex(), row, row+count-1);
190   DateWeight empty;
191   while(count--)
192     weights.insert(row, empty);
193   endInsertRows();
194   return true;
195 }
196
197 bool WeightDataModel::removeRows(int row, int count, const QModelIndex &/*parent*/)
198 {
199   beginRemoveRows(QModelIndex(), row, row+count-1);
200   while(count--)
201     weights.removeAt(row);
202   endRemoveRows();
203   writeToDisk();
204   return true;
205 }
206
207 void WeightDataModel::clear()
208 {
209   weights.clear();
210 }
211
212 void WeightDataModel::writeToDisk()
213 {
214   if (QFile::exists(datafile.fileName())) {
215     QString backupfile = datafile.fileName()+".bak";
216     if (QFile::exists(backupfile))
217       QFile::remove(backupfile);
218     QFile::copy(datafile.fileName(), backupfile);
219   }
220   if (datafile.open(QIODevice::WriteOnly | QIODevice::Text)) {
221     QTextStream stream(&datafile);
222     foreach(DateWeight w, weights)
223       stream << w.date.toString(Qt::ISODate) << ';' << w.weight << "\n";
224     datafile.close();
225   }
226 }
227
228 void WeightDataModel::readFromDisk()
229 {
230   if (!datafile.exists()) {
231     clear();
232     return;
233   }
234   if (datafile.open(QIODevice::ReadOnly | QIODevice::Text)) {
235     clear();
236     QTextStream stream(&datafile);
237     QString line = stream.readLine();
238     while (!line.isNull()) {
239       QStringList parts = line.split(';');
240       if (parts.size() != 2) {
241         throw(QString("Invalid line in file: '%1'").arg(line));
242       }
243       DateWeight w;
244       w.date = QDate::fromString(parts[0], Qt::ISODate);
245       if (!w.date.isValid()) {
246         throw(QString("Invalid date in file: '%1'").arg(parts[0]));
247       }
248       bool ok;
249       w.weight = parts[1].toDouble(&ok);
250       if(!ok) {
251         throw(QString("Invalid weight in file: '%1'").arg(parts[1]));
252       }
253       weights.push_back(w);
254       line = stream.readLine();
255     }
256     datafile.close();
257     qSort(weights);
258   }
259   else {
260     clear();
261     throw(QString("Could not read file '%1'").arg(datafile.fileName()));
262     return;
263   }
264 }