Bump version number
[weightgraph] / weightgraph / weightgraphview.cpp
1 #include "weightgraphview.h"
2 #include "settings.h"
3 #include <cmath>
4 #include <limits>
5 #include <QDebug>
6 #include <QPainter>
7 #include <QTimer>
8 #include <QtGui/QX11Info>
9 #include <X11/Xlib.h>
10 #include <X11/Xatom.h>
11
12 WeightGraphView::WeightGraphView(WeightDataModel *wdm,
13                                  const QString &id, QWidget *parent) :
14     QWidget(parent), id(id), wdm(wdm),
15     period(Settings::graphSettings(id).defaultTimeInterval)
16 {
17   setWindowTitle("WeightGraph");
18
19   setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
20
21   connect(wdm, SIGNAL(dataChanged(QModelIndex,QModelIndex)),
22           this, SLOT(update()));
23   connect(wdm, SIGNAL(rowsInserted(const QModelIndex&,int,int)),
24           this, SLOT(update()));
25   connect(wdm, SIGNAL(rowsRemoved(const QModelIndex&,int,int)),
26           this, SLOT(update()));
27   connect(Settings::self(), SIGNAL(settingChanged()),
28           this, SLOT(update()));
29 }
30
31 void WeightGraphView::mousePressEvent(QMouseEvent *)
32 {
33   emit clicked();
34 }
35
36 QSize WeightGraphView::sizeHint() const
37 {
38   return QSize(300, 400);
39 }
40
41 void WeightGraphView::keyPressEvent(QKeyEvent* event)
42 {
43   switch (event->key()) {
44   case Qt::Key_F7:
45       decPeriod();
46       event->accept();
47       break;
48
49   case Qt::Key_F8:
50       incPeriod();
51       event->accept();
52       break;
53   }
54   QWidget::keyPressEvent(event);
55 }
56
57 // X-coordinate of QDate / DateWeigth
58 #define DATE_X(d) ((d)*qreal(width())/days)
59 #define DW_X(dw) DATE_X(f.daysTo((dw).date))
60
61 // Y-coordinate of Weight / DateWeight
62 #define WEIGHT_Y(w) (height()*(maxW-(w))/weightInterval)
63 #define DW_Y(dw) WEIGHT_Y((dw).weight)
64
65 // Coordinate point of DateWeight
66 #define DW_POINT(dw) QPointF(DW_X(dw), DW_Y(dw))
67
68 namespace {
69   inline double weightIntervalToMult(double wi)
70   {
71     if (wi <= 0.2) return 0.01;
72     else if (wi <= 0.5) return 0.025;
73     else if (wi <= 1.0) return 0.05;
74     else if (wi <= 2.0) return 0.1;
75     else if (wi <= 5.0) return 0.25;
76     else if (wi <= 10.0) return 0.5;
77     else if (wi <= 20.0) return 1.0;
78     else if (wi <= 50.0) return 2.5;
79     else if (wi <= 100.0) return 5.0;
80     else if (wi <= 200.0) return 10.0;
81     else if (wi <= 500.0) return 25.0;
82     else if (wi <= 1000.0) return 50.0;
83     else return 100.0;
84   }
85 }
86
87 void WeightGraphView::paintEvent(QPaintEvent *)
88 {
89   // TODO: split this monster function!
90   QPainter painter(this);
91   painter.setRenderHint(QPainter::Antialiasing);
92
93   GraphSettings gs = Settings::graphSettings(id);
94   const QList<DW> &allWeights = wdm->getWeights();
95
96   double min, max;
97   QList<DW> weights;
98   const DW *beforeFirst = NULL;
99   if (allWeights.size() < 2) {
100     min = 0;
101     max = 100;
102   }
103   else {
104     bool firstFound = false;
105     min = std::numeric_limits<double>::max();
106     max = std::numeric_limits<double>::min();
107     const QDate &l = allWeights.last().date;
108     for(int i=0; i < allWeights.size(); i++) {
109       if (period == 0 || firstFound || allWeights[i].date.daysTo(l) <= period) {
110         weights.append(allWeights[i]);
111         if (allWeights[i].weight < min)
112           min = allWeights[i].weight;
113         if (allWeights[i].weight > max)
114           max = allWeights[i].weight;
115         firstFound = true;
116       }
117       if (!firstFound)
118         beforeFirst = &allWeights[i];
119     }
120   }
121
122   if (gs.weightIntervalMode == GraphSettings::AutomaticWithGoalWeight
123       && gs.goalWeightEnabled) {
124     min = qMin(min, Settings::goalWeightMin());
125     max = qMax(max, Settings::goalWeightMax());
126   }
127   else if(gs.weightIntervalMode == GraphSettings::Manual) {
128     if (gs.weightIntervalMax > gs.weightIntervalMin) {
129       min = gs.weightIntervalMin;
130       max = gs.weightIntervalMax;
131     }
132   }
133   // else default is min and max of actual data
134
135   double margin = (max - min)*0.03;
136   double minW = min - margin;
137   double maxW = max + margin;
138   double weightInterval = maxW-minW;
139   if (maxW-minW < 0.1) {
140     minW = min-0.003;
141     maxW = max+0.003;
142     weightInterval = maxW - minW;
143   }
144   QDate f, l;
145   int days;
146   if (weights.size() < 2) {
147     l = QDate::currentDate();
148     f = l.addDays(-7);
149     days = 7;
150   }
151   else if (period==0) {
152     f = weights.first().date;
153     l = weights.last().date;
154     days = f.daysTo(l);
155   } else {
156     l = weights.last().date;
157     f = l.addDays(-period);
158     days = period;
159   }
160
161   // Interpolate to fill gap in left part of the graph
162   if (weights.size() >= 2 && weights.first().date != f && beforeFirst != NULL) {
163     DW dw;
164     dw.date = f;
165     dw.weight = (weights.first().weight - beforeFirst->weight)
166                 /beforeFirst->date.daysTo(weights.first().date)
167                 *beforeFirst->date.daysTo(f)
168                 +beforeFirst->weight;
169     weights.prepend(dw);
170   }
171
172   painter.setWindow(-50, 0, width()+85, height()+25);
173
174   if (gs.goalWeightEnabled
175       && ((Settings::goalWeightMin() > minW && Settings::goalWeightMin() < maxW)
176           ||(Settings::goalWeightMax() > minW && Settings::goalWeightMax() < maxW))) {
177     QPen oldPen = painter.pen();
178     QBrush oldBrush = painter.brush();
179     painter.setPen(Qt::NoPen);
180     painter.setBrush(QColor(0,255,0,100));
181     painter.setClipRect(QRectF(0,0,width(),height()));
182     painter.drawRect(QRectF(QPointF(0, WEIGHT_Y(Settings::goalWeightMax())),
183                             QPointF(width(), WEIGHT_Y(Settings::goalWeightMin()))));
184     painter.setClipping(false);
185     painter.setPen(oldPen);
186     painter.setBrush(oldBrush);
187   }
188
189   // Y-axis
190   QFont font = painter.font();
191   font.setPixelSize(16);
192   painter.setFont(font);
193   painter.drawLine(QPointF(0.0,0.0), QPointF(0,height()));
194   double mult = weightIntervalToMult(weightInterval);
195   int count = 0;
196   for(double w=ceil(minW/mult)*mult; w < maxW; w += mult, count++) {
197     double len = count%5==0 ? 7.0 : 4.0;
198     painter.drawLine(QPointF(-len,WEIGHT_Y(w)),QPointF(len,WEIGHT_Y(w)));
199     if (count%5 == 0) {
200       QPen p = painter.pen();
201       painter.setPen(QColor(50,50,50));
202       painter.drawLine(QPointF(len,WEIGHT_Y(w)),QPointF(width(),WEIGHT_Y(w)));
203       painter.setPen(p);
204       QString text = tr("%1").arg(double(w),0,'f', mult <= 0.25 ? 2 : 1);
205       QSize textSize = painter.fontMetrics().size(0, text);
206       painter.drawText(QPointF(-len-3-textSize.width(), WEIGHT_Y(w)+6), text);
207     }
208   }
209
210   // X-axis
211   font.setPixelSize(13);
212   painter.setFont(font);
213   double endOfLastDate = -1e6;
214   mult = days/30+1;
215   painter.drawLine(QPointF(0.0,height()), QPointF(width(),height()));
216   for(int day=0; day <= days; day+=mult) {
217     QString text = f.addDays(day).toString(Qt::ISODate);
218     QSize textSize = painter.fontMetrics().size(0, text);
219     double tickLen;
220     if (DATE_X(day)-textSize.width()/2 > endOfLastDate + 10) {
221       tickLen = 5.0;
222       painter.drawText(QPointF(DATE_X(day)-textSize.width()/2,
223                                WEIGHT_Y(minW)+18), text);
224       endOfLastDate = DATE_X(day)+textSize.width()/2;
225     }
226     else
227       tickLen = 3.0;
228     painter.drawLine(QPointF(DATE_X(day), WEIGHT_Y(minW)-tickLen),
229                      QPointF(DATE_X(day), WEIGHT_Y(minW)+tickLen));
230   }
231
232   // The weight data
233   QPolygonF linepoints;
234   foreach(DW dw, weights) {
235     linepoints << DW_POINT(dw);
236   }
237   painter.drawPolyline(linepoints);
238
239 }
240 void WeightGraphView::show()
241 {
242   QWidget::show();
243   grabZoomKeysForWindow(winId(), Settings::grabZoomKeys()); //Need to be regrabbed somewhy
244   //Work around a bug: hidden graphs don't update - must wait for the
245   //graph to actually show up, then call update.
246   QTimer *tmp = new QTimer(this);
247   tmp->setSingleShot(true);
248   tmp->setInterval(500);
249   connect(tmp, SIGNAL(timeout()), this, SLOT(update()));
250   tmp->start();
251 }
252
253 void WeightGraphView::update() {
254   grabZoomKeysForWindow(winId(), Settings::grabZoomKeys());
255   QWidget::update();
256 }
257
258 void WeightGraphView::grabZoomKeys(bool grab) {
259   WeightGraphView::grabZoomKeysForWindow(winId(), grab);
260 }
261 void WeightGraphView::grabZoomKeysForWindow(WId winId, bool grab)
262 {
263   if (!winId) {
264     qWarning("Can't grab keys unless we have a window id");
265     return;
266   }
267
268   unsigned long val = (grab) ? 1 : 0;
269   Atom atom = XInternAtom(QX11Info::display(), "_HILDON_ZOOM_KEY_ATOM", False);
270   if (!atom) {
271     qWarning("Unable to obtain _HILDON_ZOOM_KEY_ATOM. Is this Maemo 5?");
272     return;
273   }
274
275   XChangeProperty (QX11Info::display(), winId, atom, XA_INTEGER, 32,
276                    PropModeReplace, reinterpret_cast<unsigned char *>(&val), 1);
277 }
278
279 namespace {
280   int getNumberOfDaysBetweenFirstAndLastEntry(WeightDataModel *wdm) {
281     QDate first = wdm->getWeights().first().date;
282     QDate last = wdm->getWeights().last().date;
283     return first.daysTo(last);
284   }
285 }
286
287 #define PERIOD_CHANGE_FACTOR 0.05
288 void WeightGraphView::incPeriod() {
289   if (period == 0)
290     period = getNumberOfDaysBetweenFirstAndLastEntry(wdm) + 1;
291   else
292     period += qMax(1, int(period*PERIOD_CHANGE_FACTOR));
293   update();
294 }
295 void WeightGraphView::decPeriod() {
296   if (period == 0)
297     period = getNumberOfDaysBetweenFirstAndLastEntry(wdm) - 1;
298   else
299     period = qMax(2, qMin(period-1, int(period*(1-PERIOD_CHANGE_FACTOR))));
300   update();
301 }